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