diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py
index cc9b211..ddf777e 100644
--- a/src/warchron/controller/dtos.py
+++ b/src/warchron/controller/dtos.py
@@ -135,11 +135,6 @@ class WarParticipantScoreDTO:
objective_icons: Dict[str, QIcon] = field(default_factory=dict)
-@dataclass
-class TieDialogData:
- title: str
-
-
@dataclass
class WarSettingsDTO:
major_value: int
diff --git a/src/warchron/controller/presenter.py b/src/warchron/controller/presenter.py
index d41a336..a7d8f44 100644
--- a/src/warchron/controller/presenter.py
+++ b/src/warchron/controller/presenter.py
@@ -1,4 +1,5 @@
from typing import Dict
+from dataclasses import dataclass
from PyQt6.QtGui import QIcon
@@ -11,16 +12,20 @@ from warchron.constants import (
ScoreKind,
)
-from warchron.controller.dtos import TieDialogData
from warchron.model.tiebreaking import TieContext, TieBreaker
from warchron.model.war import War
from warchron.model.campaign import Campaign
from warchron.model.round import Round
-from warchron.model.scoring import ParticipantScore
+from warchron.model.scoring import ParticipantScore, ScoreComputer
from warchron.model.checking import ResultChecker
from warchron.model.exception import DomainError
+@dataclass
+class TieDialogData:
+ title: str
+
+
class Presenter:
@staticmethod
@@ -30,17 +35,16 @@ class Presenter:
campaign: Campaign | None = None,
round: Round | None = None,
) -> TieDialogData:
- # TODO display Nth place
- if ctx.context_type == ContextType.WAR:
- if ctx.objective_id:
+ if ctx.context_type in (ContextType.WAR, ContextType.CAMPAIGN):
+ rank = Presenter._get_tie_rank(war, ctx)
+ rank_label = Presenter._build_rank_label(rank)
+ level = str(ctx.context_type).capitalize()
+ if ctx.objective_id is not None:
obj = war.objectives[ctx.objective_id]
- return TieDialogData(f"War objective tie — {obj.name}")
- return TieDialogData("War tie")
- if ctx.context_type == ContextType.CAMPAIGN:
- if ctx.objective_id:
- obj = war.objectives[ctx.objective_id]
- return TieDialogData(f"Campaign objective tie — {obj.name}")
- return TieDialogData("Campaign tie")
+ return TieDialogData(
+ f"{level} objective tie for {rank_label} — {obj.name}"
+ )
+ return TieDialogData(f"{level} tie for {rank_label}")
if ctx.context_type == ContextType.BATTLE:
if campaign:
sector = campaign.sectors[ctx.context_id]
@@ -177,3 +181,57 @@ class Presenter:
compute_icon(battle.player_1_id),
compute_icon(battle.player_2_id),
)
+
+ @staticmethod
+ def _ordinal(n: int) -> str:
+ if 10 <= n % 100 <= 20:
+ suffix = "th"
+ else:
+ suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
+ return f"{n}{suffix}"
+
+ @staticmethod
+ def _build_rank_label(rank: int | None) -> str:
+ if rank is None:
+ return ""
+ return f"{Presenter._ordinal(rank)} place"
+
+ @staticmethod
+ def _get_tie_rank(
+ war: War,
+ ctx: TieContext,
+ ) -> int | None:
+ from warchron.model.checking import ResultChecker
+
+ scores = ScoreComputer.compute_scores(
+ war,
+ ctx.context_type,
+ ctx.context_id,
+ )
+ if ctx.objective_id is None:
+
+ def value_getter(score: ParticipantScore) -> int:
+ return score.victory_points
+
+ score_kind = ScoreKind.VP
+ else:
+ obj_id = ctx.objective_id
+
+ def value_getter(score: ParticipantScore) -> int:
+ return score.narrative_points.get(obj_id, 0)
+
+ score_kind = ScoreKind.NP
+ ranking = ResultChecker.get_effective_ranking(
+ war,
+ ctx.context_type,
+ ctx.context_id,
+ score_kind,
+ scores,
+ value_getter,
+ ctx.objective_id,
+ )
+ tied = set(ctx.participants)
+ for rank, group, _ in ranking:
+ if tied.intersection(group):
+ return rank
+ return None
diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py
index 3c8efae..ef7dcd4 100644
--- a/src/warchron/model/pairing.py
+++ b/src/warchron/model/pairing.py
@@ -334,6 +334,7 @@ class Pairing:
raise DomainError("Campaign not found")
match_counts = Pairing.build_match_count(campaign)
available_battles = round.get_battles_with_places()
+ # FIXME no error when all participants allocated
if not available_battles:
raise DomainError("No available battle remaining")
occupancy = {
diff --git a/src/warchron/view/ui/ui_tie_dialog.py b/src/warchron/view/ui/ui_tie_dialog.py
index 0361373..f6af573 100644
--- a/src/warchron/view/ui/ui_tie_dialog.py
+++ b/src/warchron/view/ui/ui_tie_dialog.py
@@ -67,7 +67,7 @@ class Ui_tieDialog(object):
def retranslateUi(self, tieDialog):
_translate = QtCore.QCoreApplication.translate
- tieDialog.setWindowTitle(_translate("tieDialog", "Tie"))
+ tieDialog.setWindowTitle(_translate("tieDialog", "Tie-break"))
self.tieContext.setText(_translate("tieDialog", "Battle tie"))
diff --git a/src/warchron/view/ui/ui_tie_dialog.ui b/src/warchron/view/ui/ui_tie_dialog.ui
index 38e6744..69b9fef 100644
--- a/src/warchron/view/ui/ui_tie_dialog.ui
+++ b/src/warchron/view/ui/ui_tie_dialog.ui
@@ -14,7 +14,7 @@
- Tie
+ Tie-break