diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 97586b2..42855c1 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -23,6 +23,7 @@ from warchron.model.tie_manager import TieContext, TieResolver from warchron.model.score_service import ScoreService from warchron.controller.closure_workflow import CampaignClosureWorkflow from warchron.controller.ranking_icon import RankingIcon +from warchron.controller.presenter import TiePresenter from warchron.view.campaign_dialog import CampaignDialog from warchron.view.campaign_participant_dialog import CampaignParticipantDialog from warchron.view.sector_dialog import SectorDialog @@ -176,24 +177,17 @@ class CampaignController: 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( parent=self.app.view, players=players, counters=counters, context_type=ctx.context_type, 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(): TieResolver.cancel_tie_break(war, ctx) raise AbortedOperation("Tie resolution cancelled") diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py index 9451bc6..7882d08 100644 --- a/src/warchron/controller/dtos.py +++ b/src/warchron/controller/dtos.py @@ -132,3 +132,8 @@ class WarParticipantScoreDTO: tokens: int rank_icon: QIcon | None = None objective_icons: Dict[str, QIcon] = field(default_factory=dict) + + +@dataclass +class TieDialogData: + title: str diff --git a/src/warchron/controller/presenter.py b/src/warchron/controller/presenter.py new file mode 100644 index 0000000..665028a --- /dev/null +++ b/src/warchron/controller/presenter.py @@ -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" diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 3b7f5e6..79a9729 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -28,6 +28,7 @@ from warchron.controller.closure_workflow import ( RoundClosureWorkflow, RoundPairingWorkflow, ) +from warchron.controller.presenter import TiePresenter from warchron.view.choice_dialog import ChoiceDialog from warchron.view.battle_dialog import BattleDialog from warchron.view.tie_dialog import TieDialog @@ -243,13 +244,26 @@ class RoundController: 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( parent=self.app.view, players=players, counters=counters, context_type=ctx.context_type, context_id=ctx.context_id, + context_name=data.title, ) if not dialog.exec(): TieResolver.cancel_tie_break(war, ctx) diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 2c000e9..8ebd690 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -28,6 +28,7 @@ from warchron.model.tie_manager import TieContext, TieResolver from warchron.model.score_service import ScoreService from warchron.controller.closure_workflow import WarClosureWorkflow from warchron.controller.ranking_icon import RankingIcon +from warchron.controller.presenter import TiePresenter from warchron.view.war_dialog import WarDialog from warchron.view.objective_dialog import ObjectiveDialog from warchron.view.war_participant_dialog import WarParticipantDialog @@ -170,24 +171,18 @@ class WarController: for pid in active ] counters = [war.get_influence_tokens(pid) for pid in active] + data = TiePresenter.build_dialog_data( + war, + ctx, + ) dialog = TieDialog( parent=self.app.view, players=players, counters=counters, context_type=ctx.context_type, 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(): TieResolver.cancel_tie_break(war, ctx) raise AbortedOperation("Tie resolution cancelled") diff --git a/src/warchron/model/score_service.py b/src/warchron/model/score_service.py index 40b0891..58fc07e 100644 --- a/src/warchron/model/score_service.py +++ b/src/warchron/model/score_service.py @@ -4,7 +4,9 @@ from collections import defaultdict from warchron.constants import ContextType from warchron.model.war import War +from warchron.model.campaign import Campaign from warchron.model.battle import Battle +from warchron.model.exception import DomainError @dataclass(slots=True) @@ -26,14 +28,17 @@ class ScoreService: continue yield from rnd.battles.values() elif context_type == ContextType.CAMPAIGN: - campaign = war.get_campaign(context_id) - for rnd in campaign.rounds: - if not rnd.is_over: - continue - yield from rnd.battles.values() + campaign: Campaign | None = war.get_campaign(context_id) + if campaign: + for rnd in campaign.rounds: + if not rnd.is_over: + continue + yield from rnd.battles.values() elif context_type == ContextType.BATTLE: battle = war.get_battle(context_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) if rnd and rnd.is_over: yield battle diff --git a/src/warchron/model/war.py b/src/warchron/model/war.py index d7c0f28..398d742 100644 --- a/src/warchron/model/war.py +++ b/src/warchron/model/war.py @@ -10,7 +10,11 @@ from warchron.model.war_event import ( InfluenceSpent, 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.objective import Objective from warchron.model.campaign_participant import CampaignParticipant @@ -80,6 +84,10 @@ class War: if ev.context_type == ContextType.BATTLE: battle = self.get_battle(ev.context_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.is_over = False elif ev.context_type == ContextType.CAMPAIGN: @@ -313,12 +321,12 @@ class War: return None # 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 sect in camp.sectors.values(): if sect.id == sector_id: return camp - raise KeyError(f"Sector {sector_id} not found in any Campaign") + return None def get_campaign_by_campaign_participant( self, participant_id: str @@ -396,6 +404,8 @@ class War: description: str | None, ) -> None: camp = self.get_campaign_by_sector(sector_id) + if not camp: + raise DomainError(f"No campaign found for sector {sector_id}") camp.update_sector( sector_id, name=name, @@ -409,6 +419,8 @@ class War: def remove_sector(self, sector_id: str) -> None: 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) # Campaign participant methods diff --git a/src/warchron/view/tie_dialog.py b/src/warchron/view/tie_dialog.py index fd08f34..344f7af 100644 --- a/src/warchron/view/tie_dialog.py +++ b/src/warchron/view/tie_dialog.py @@ -34,7 +34,7 @@ class TieDialog(QDialog): self.ui: Ui_tieDialog = Ui_tieDialog() self.ui.setupUi(self) # type: ignore 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 icon_path = (RESOURCES_DIR / Icons._paths[IconName.TOKENS]).as_posix() token_html = ( @@ -71,17 +71,3 @@ class TieDialog(QDialog): def get_bids(self) -> Dict[str, bool]: 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")