display tiebreak place

This commit is contained in:
Maxime Réaux 2026-03-24 16:26:52 +01:00
parent c144845376
commit a3144dc3c9
5 changed files with 73 additions and 19 deletions

View file

@ -135,11 +135,6 @@ class WarParticipantScoreDTO:
objective_icons: Dict[str, QIcon] = field(default_factory=dict) objective_icons: Dict[str, QIcon] = field(default_factory=dict)
@dataclass
class TieDialogData:
title: str
@dataclass @dataclass
class WarSettingsDTO: class WarSettingsDTO:
major_value: int major_value: int

View file

@ -1,4 +1,5 @@
from typing import Dict from typing import Dict
from dataclasses import dataclass
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon
@ -11,16 +12,20 @@ from warchron.constants import (
ScoreKind, ScoreKind,
) )
from warchron.controller.dtos import TieDialogData
from warchron.model.tiebreaking import TieContext, TieBreaker from warchron.model.tiebreaking import TieContext, TieBreaker
from warchron.model.war import War from warchron.model.war import War
from warchron.model.campaign import Campaign from warchron.model.campaign import Campaign
from warchron.model.round import Round 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.checking import ResultChecker
from warchron.model.exception import DomainError from warchron.model.exception import DomainError
@dataclass
class TieDialogData:
title: str
class Presenter: class Presenter:
@staticmethod @staticmethod
@ -30,17 +35,16 @@ class Presenter:
campaign: Campaign | None = None, campaign: Campaign | None = None,
round: Round | None = None, round: Round | None = None,
) -> TieDialogData: ) -> TieDialogData:
# TODO display Nth place if ctx.context_type in (ContextType.WAR, ContextType.CAMPAIGN):
if ctx.context_type == ContextType.WAR: rank = Presenter._get_tie_rank(war, ctx)
if ctx.objective_id: 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] obj = war.objectives[ctx.objective_id]
return TieDialogData(f"War objective tie — {obj.name}") return TieDialogData(
return TieDialogData("War tie") f"{level} objective tie for {rank_label}{obj.name}"
if ctx.context_type == ContextType.CAMPAIGN: )
if ctx.objective_id: return TieDialogData(f"{level} tie for {rank_label}")
obj = war.objectives[ctx.objective_id]
return TieDialogData(f"Campaign objective tie — {obj.name}")
return TieDialogData("Campaign tie")
if ctx.context_type == ContextType.BATTLE: if ctx.context_type == ContextType.BATTLE:
if campaign: if campaign:
sector = campaign.sectors[ctx.context_id] sector = campaign.sectors[ctx.context_id]
@ -177,3 +181,57 @@ class Presenter:
compute_icon(battle.player_1_id), compute_icon(battle.player_1_id),
compute_icon(battle.player_2_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

View file

@ -334,6 +334,7 @@ class Pairing:
raise DomainError("Campaign not found") raise DomainError("Campaign not found")
match_counts = Pairing.build_match_count(campaign) match_counts = Pairing.build_match_count(campaign)
available_battles = round.get_battles_with_places() available_battles = round.get_battles_with_places()
# FIXME no error when all participants allocated
if not available_battles: if not available_battles:
raise DomainError("No available battle remaining") raise DomainError("No available battle remaining")
occupancy = { occupancy = {

View file

@ -67,7 +67,7 @@ class Ui_tieDialog(object):
def retranslateUi(self, tieDialog): def retranslateUi(self, tieDialog):
_translate = QtCore.QCoreApplication.translate _translate = QtCore.QCoreApplication.translate
tieDialog.setWindowTitle(_translate("tieDialog", "Tie")) tieDialog.setWindowTitle(_translate("tieDialog", "Tie-break"))
self.tieContext.setText(_translate("tieDialog", "Battle tie")) self.tieContext.setText(_translate("tieDialog", "Battle tie"))

View file

@ -14,7 +14,7 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Tie</string> <string>Tie-break</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset> <iconset>