tie dialog title with context details

This commit is contained in:
Maxime Réaux 2026-03-18 14:30:57 +01:00
parent aa75a5b84f
commit 9e602e8ca4
8 changed files with 109 additions and 46 deletions

View file

@ -23,6 +23,7 @@ from warchron.model.tie_manager import TieContext, TieResolver
from warchron.model.score_service import ScoreService from warchron.model.score_service import ScoreService
from warchron.controller.closure_workflow import CampaignClosureWorkflow from warchron.controller.closure_workflow import CampaignClosureWorkflow
from warchron.controller.ranking_icon import RankingIcon from warchron.controller.ranking_icon import RankingIcon
from warchron.controller.presenter import TiePresenter
from warchron.view.campaign_dialog import CampaignDialog from warchron.view.campaign_dialog import CampaignDialog
from warchron.view.campaign_participant_dialog import CampaignParticipantDialog from warchron.view.campaign_participant_dialog import CampaignParticipantDialog
from warchron.view.sector_dialog import SectorDialog from warchron.view.sector_dialog import SectorDialog
@ -176,24 +177,17 @@ class CampaignController:
for pid in active for pid in active
] ]
counters = [war.get_influence_tokens(pid) for pid in active] counters = [war.get_influence_tokens(pid) for pid in active]
data = TiePresenter.build_dialog_data(
war, ctx, campaign=war.get_campaign(ctx.context_id)
)
dialog = TieDialog( dialog = TieDialog(
parent=self.app.view, parent=self.app.view,
players=players, players=players,
counters=counters, counters=counters,
context_type=ctx.context_type, context_type=ctx.context_type,
context_id=ctx.context_id, context_id=ctx.context_id,
context_name=None, context_name=data.title,
) )
if ctx.objective_id:
objective = war.objectives[ctx.objective_id]
dialog = TieDialog(
parent=self.app.view,
players=players,
counters=counters,
context_type=ctx.context_type,
context_id=ctx.context_id,
context_name=f"Objective tie: {objective.name}",
)
if not dialog.exec(): if not dialog.exec():
TieResolver.cancel_tie_break(war, ctx) TieResolver.cancel_tie_break(war, ctx)
raise AbortedOperation("Tie resolution cancelled") raise AbortedOperation("Tie resolution cancelled")

View file

@ -132,3 +132,8 @@ class WarParticipantScoreDTO:
tokens: int tokens: int
rank_icon: QIcon | None = None rank_icon: QIcon | None = None
objective_icons: Dict[str, QIcon] = field(default_factory=dict) objective_icons: Dict[str, QIcon] = field(default_factory=dict)
@dataclass
class TieDialogData:
title: str

View file

@ -0,0 +1,52 @@
from warchron.constants import ContextType
from warchron.controller.dtos import TieDialogData
from warchron.model.tie_manager import TieContext
from warchron.model.war import War
from warchron.model.campaign import Campaign
from warchron.model.round import Round
class TiePresenter:
@staticmethod
def build_dialog_data(
war: War,
ctx: TieContext,
campaign: Campaign | None = None,
round: Round | None = None,
) -> TieDialogData:
if ctx.context_type == ContextType.WAR:
if ctx.objective_id:
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")
if ctx.context_type == ContextType.BATTLE:
if campaign:
sector = campaign.sectors[ctx.context_id]
return TieDialogData(f"Battle tie — {sector.name}")
return TieDialogData("Battle tie")
if ctx.context_type == ContextType.CHOICE:
if ctx.sector_id and campaign and round:
sector = campaign.sectors[ctx.sector_id]
kind = TiePresenter._choice_kind(round, ctx)
return TieDialogData(f"Choice tie — {sector.name} ({kind})")
return TieDialogData("Choice tie")
return TieDialogData("Tie")
@staticmethod
def _choice_kind(round: Round, ctx: TieContext) -> str:
for pid in ctx.participants:
camp_pid = round.campaign.war_to_campaign_part_id(pid)
choice = round.choices.get(camp_pid)
if not choice:
continue
if choice.priority_sector_id == ctx.sector_id:
return "priority"
if choice.secondary_sector_id == ctx.sector_id:
return "secondary"
return "choice"

View file

@ -28,6 +28,7 @@ from warchron.controller.closure_workflow import (
RoundClosureWorkflow, RoundClosureWorkflow,
RoundPairingWorkflow, RoundPairingWorkflow,
) )
from warchron.controller.presenter import TiePresenter
from warchron.view.choice_dialog import ChoiceDialog from warchron.view.choice_dialog import ChoiceDialog
from warchron.view.battle_dialog import BattleDialog from warchron.view.battle_dialog import BattleDialog
from warchron.view.tie_dialog import TieDialog from warchron.view.tie_dialog import TieDialog
@ -243,13 +244,26 @@ class RoundController:
for pid in ctx.participants for pid in ctx.participants
] ]
counters = [war.get_influence_tokens(pid) for pid in ctx.participants] counters = [war.get_influence_tokens(pid) for pid in ctx.participants]
# TODO display sector name for BATTLE or CHOICE if ctx.context_type == ContextType.BATTLE:
# context_id = battle.sector_id
campaign = war.get_campaign_by_sector(ctx.context_id)
if campaign:
round = campaign.get_round_by_battle(ctx.context_id)
if ctx.context_type == ContextType.CHOICE:
# context_id = round.id
campaign = war.get_campaign_by_round(ctx.context_id)
if campaign:
round = war.get_round(ctx.context_id)
data = TiePresenter.build_dialog_data(
war, ctx, round=round, campaign=campaign
)
dialog = TieDialog( dialog = TieDialog(
parent=self.app.view, parent=self.app.view,
players=players, players=players,
counters=counters, counters=counters,
context_type=ctx.context_type, context_type=ctx.context_type,
context_id=ctx.context_id, context_id=ctx.context_id,
context_name=data.title,
) )
if not dialog.exec(): if not dialog.exec():
TieResolver.cancel_tie_break(war, ctx) TieResolver.cancel_tie_break(war, ctx)

View file

@ -28,6 +28,7 @@ from warchron.model.tie_manager import TieContext, TieResolver
from warchron.model.score_service import ScoreService from warchron.model.score_service import ScoreService
from warchron.controller.closure_workflow import WarClosureWorkflow from warchron.controller.closure_workflow import WarClosureWorkflow
from warchron.controller.ranking_icon import RankingIcon from warchron.controller.ranking_icon import RankingIcon
from warchron.controller.presenter import TiePresenter
from warchron.view.war_dialog import WarDialog from warchron.view.war_dialog import WarDialog
from warchron.view.objective_dialog import ObjectiveDialog from warchron.view.objective_dialog import ObjectiveDialog
from warchron.view.war_participant_dialog import WarParticipantDialog from warchron.view.war_participant_dialog import WarParticipantDialog
@ -170,24 +171,18 @@ class WarController:
for pid in active for pid in active
] ]
counters = [war.get_influence_tokens(pid) for pid in active] counters = [war.get_influence_tokens(pid) for pid in active]
data = TiePresenter.build_dialog_data(
war,
ctx,
)
dialog = TieDialog( dialog = TieDialog(
parent=self.app.view, parent=self.app.view,
players=players, players=players,
counters=counters, counters=counters,
context_type=ctx.context_type, context_type=ctx.context_type,
context_id=ctx.context_id, context_id=ctx.context_id,
context_name=None, context_name=data.title,
) )
if ctx.objective_id:
objective = war.objectives[ctx.objective_id]
dialog = TieDialog(
parent=self.app.view,
players=players,
counters=counters,
context_type=ctx.context_type,
context_id=ctx.context_id,
context_name=f"Objective tie: {objective.name}",
)
if not dialog.exec(): if not dialog.exec():
TieResolver.cancel_tie_break(war, ctx) TieResolver.cancel_tie_break(war, ctx)
raise AbortedOperation("Tie resolution cancelled") raise AbortedOperation("Tie resolution cancelled")

View file

@ -4,7 +4,9 @@ from collections import defaultdict
from warchron.constants import ContextType from warchron.constants import ContextType
from warchron.model.war import War from warchron.model.war import War
from warchron.model.campaign import Campaign
from warchron.model.battle import Battle from warchron.model.battle import Battle
from warchron.model.exception import DomainError
@dataclass(slots=True) @dataclass(slots=True)
@ -26,14 +28,17 @@ class ScoreService:
continue continue
yield from rnd.battles.values() yield from rnd.battles.values()
elif context_type == ContextType.CAMPAIGN: elif context_type == ContextType.CAMPAIGN:
campaign = war.get_campaign(context_id) campaign: Campaign | None = war.get_campaign(context_id)
for rnd in campaign.rounds: if campaign:
if not rnd.is_over: for rnd in campaign.rounds:
continue if not rnd.is_over:
yield from rnd.battles.values() continue
yield from rnd.battles.values()
elif context_type == ContextType.BATTLE: elif context_type == ContextType.BATTLE:
battle = war.get_battle(context_id) battle = war.get_battle(context_id)
campaign = war.get_campaign_by_sector(battle.sector_id) campaign = war.get_campaign_by_sector(battle.sector_id)
if not campaign:
raise DomainError(f"No campaign found for secor {battle.sector_id}")
rnd = campaign.get_round_by_battle(context_id) rnd = campaign.get_round_by_battle(context_id)
if rnd and rnd.is_over: if rnd and rnd.is_over:
yield battle yield battle

View file

@ -10,7 +10,11 @@ from warchron.model.war_event import (
InfluenceSpent, InfluenceSpent,
TieResolved, TieResolved,
) )
from warchron.model.exception import ForbiddenOperation, RequiresConfirmation from warchron.model.exception import (
ForbiddenOperation,
RequiresConfirmation,
DomainError,
)
from warchron.model.war_participant import WarParticipant from warchron.model.war_participant import WarParticipant
from warchron.model.objective import Objective from warchron.model.objective import Objective
from warchron.model.campaign_participant import CampaignParticipant from warchron.model.campaign_participant import CampaignParticipant
@ -80,6 +84,10 @@ class War:
if ev.context_type == ContextType.BATTLE: if ev.context_type == ContextType.BATTLE:
battle = self.get_battle(ev.context_id) battle = self.get_battle(ev.context_id)
campaign = self.get_campaign_by_sector(battle.sector_id) campaign = self.get_campaign_by_sector(battle.sector_id)
if not campaign:
raise DomainError(
f"No campaign found for sector {battle.sector_id}"
)
round = campaign.get_round_by_battle(ev.context_id) round = campaign.get_round_by_battle(ev.context_id)
round.is_over = False round.is_over = False
elif ev.context_type == ContextType.CAMPAIGN: elif ev.context_type == ContextType.CAMPAIGN:
@ -313,12 +321,12 @@ class War:
return None return None
# TODO replace multiloops by internal has_* method # TODO replace multiloops by internal has_* method
def get_campaign_by_sector(self, sector_id: str) -> Campaign: def get_campaign_by_sector(self, sector_id: str) -> Campaign | None:
for camp in self.campaigns: for camp in self.campaigns:
for sect in camp.sectors.values(): for sect in camp.sectors.values():
if sect.id == sector_id: if sect.id == sector_id:
return camp return camp
raise KeyError(f"Sector {sector_id} not found in any Campaign") return None
def get_campaign_by_campaign_participant( def get_campaign_by_campaign_participant(
self, participant_id: str self, participant_id: str
@ -396,6 +404,8 @@ class War:
description: str | None, description: str | None,
) -> None: ) -> None:
camp = self.get_campaign_by_sector(sector_id) camp = self.get_campaign_by_sector(sector_id)
if not camp:
raise DomainError(f"No campaign found for sector {sector_id}")
camp.update_sector( camp.update_sector(
sector_id, sector_id,
name=name, name=name,
@ -409,6 +419,8 @@ class War:
def remove_sector(self, sector_id: str) -> None: def remove_sector(self, sector_id: str) -> None:
camp = self.get_campaign_by_sector(sector_id) camp = self.get_campaign_by_sector(sector_id)
if not camp:
raise DomainError(f"No campaign found for sector {sector_id}")
camp.remove_sector(sector_id) camp.remove_sector(sector_id)
# Campaign participant methods # Campaign participant methods

View file

@ -34,7 +34,7 @@ class TieDialog(QDialog):
self.ui: Ui_tieDialog = Ui_tieDialog() self.ui: Ui_tieDialog = Ui_tieDialog()
self.ui.setupUi(self) # type: ignore self.ui.setupUi(self) # type: ignore
self.setWindowIcon(Icons.get(IconName.WARCHRONICO)) self.setWindowIcon(Icons.get(IconName.WARCHRONICO))
self.ui.tieContext.setText(self._get_context_title(context_type, context_name)) self.ui.tieContext.setText(context_name)
grid = self.ui.playersGridLayout grid = self.ui.playersGridLayout
icon_path = (RESOURCES_DIR / Icons._paths[IconName.TOKENS]).as_posix() icon_path = (RESOURCES_DIR / Icons._paths[IconName.TOKENS]).as_posix()
token_html = ( token_html = (
@ -71,17 +71,3 @@ class TieDialog(QDialog):
def get_bids(self) -> Dict[str, bool]: def get_bids(self) -> Dict[str, bool]:
return {pid: checkbox.isChecked() for pid, checkbox in self._checkboxes.items()} return {pid: checkbox.isChecked() for pid, checkbox in self._checkboxes.items()}
@staticmethod
def _get_context_title(
context_type: ContextType, context_name: str | None = None
) -> str:
if context_name:
return f"{context_name} tie"
titles = {
ContextType.BATTLE: "Battle tie",
ContextType.CAMPAIGN: "Campaign tie",
ContextType.WAR: "War tie",
ContextType.CHOICE: "Choice tie",
}
return titles.get(context_type, "Tie")