From 81626171c8ba8132ce057f854add9f619e5b3a6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Tue, 24 Feb 2026 09:06:13 +0100 Subject: [PATCH 1/4] display tokens in participant tables --- src/warchron/controller/campaign_controller.py | 2 ++ src/warchron/controller/dtos.py | 2 ++ src/warchron/controller/war_controller.py | 2 ++ src/warchron/view/view.py | 16 ++++++++++++---- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 89805db..9228a97 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -111,6 +111,7 @@ class CampaignController: theme=camp_part.theme or "", victory_points=score.victory_points, narrative_points=dict(score.narrative_points), + tokens=war.get_influence_tokens(war_part.id), rank_icon=icon_map.get(war_part_id), ) ) @@ -185,6 +186,7 @@ class CampaignController: RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=campaign_id ) + # TODO fix spent tokens on tie cancel def resolve_ties( self, war: War, contexts: List[TieContext] ) -> Dict[str, Dict[str, bool]]: diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py index 3632e2f..dcbf05e 100644 --- a/src/warchron/controller/dtos.py +++ b/src/warchron/controller/dtos.py @@ -124,6 +124,7 @@ class CampaignParticipantScoreDTO: theme: str victory_points: int narrative_points: Dict[str, int] + tokens: int rank_icon: QIcon | None = None @@ -135,4 +136,5 @@ class WarParticipantScoreDTO: faction: str victory_points: int narrative_points: Dict[str, int] + tokens: int rank_icon: QIcon | None = None diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index fcb5f0b..a12962a 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -96,6 +96,7 @@ class WarController: faction=war_part.faction or "", victory_points=score.victory_points, narrative_points=dict(score.narrative_points), + tokens=war.get_influence_tokens(war_part.id), rank_icon=icon_map.get(war_part.id), ) ) @@ -160,6 +161,7 @@ class WarController: RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id ) + # TODO fix spent tokens on tie cancel def resolve_ties( self, war: War, contexts: List[TieContext] ) -> Dict[str, Dict[str, bool]]: diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 5cbd294..5e87ce3 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -369,8 +369,10 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): ) -> None: table = self.warParticipantsTable table.clearContents() - base_cols = ["Player", "Faction", "Victory"] - headers = base_cols + [obj.name for obj in objectives] + base_cols = ["Player", "Faction", "Victory pts"] + headers = ( + base_cols + [str(obj.name + " pts") for obj in objectives] + ["Tokens"] + ) table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) table.setRowCount(len(participants)) @@ -382,6 +384,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): faction_item = QtWidgets.QTableWidgetItem(part.faction) VP_item = QtWidgets.QTableWidgetItem(str(part.victory_points)) name_item.setData(Qt.ItemDataRole.UserRole, part.war_participant_id) + token_item = QtWidgets.QTableWidgetItem(str(part.tokens)) table.setItem(row, 0, name_item) table.setItem(row, 1, faction_item) table.setItem(row, 2, VP_item) @@ -391,6 +394,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): NP_item = QtWidgets.QTableWidgetItem(str(value)) table.setItem(row, col, NP_item) col += 1 + table.setItem(row, col, token_item) table.resizeColumnsToContents() def _on_major_changed(self, value: int) -> None: @@ -489,8 +493,10 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): ) -> None: table = self.campaignParticipantsTable table.clearContents() - base_cols = ["Player", "Leader", "Theme", "Victory"] - headers = base_cols + [obj.name for obj in objectives] + base_cols = ["Player", "Leader", "Theme", "Victory pts"] + headers = ( + base_cols + [str(obj.name + " pts") for obj in objectives] + ["Tokens"] + ) table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) table.setRowCount(len(participants)) @@ -502,6 +508,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): lead_item = QtWidgets.QTableWidgetItem(part.leader) theme_item = QtWidgets.QTableWidgetItem(part.theme) VP_item = QtWidgets.QTableWidgetItem(str(part.victory_points)) + token_item = QtWidgets.QTableWidgetItem(str(part.tokens)) name_item.setData(Qt.ItemDataRole.UserRole, part.campaign_participant_id) table.setItem(row, 0, name_item) table.setItem(row, 1, lead_item) @@ -513,6 +520,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): NP_item = QtWidgets.QTableWidgetItem(str(value)) table.setItem(row, col, NP_item) col += 1 + table.setItem(row, col, token_item) table.resizeColumnsToContents() # Round page From 31a2ebb9dc877a4a5c0f5a0e60c603e594d0aaf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Tue, 24 Feb 2026 09:11:37 +0100 Subject: [PATCH 2/4] fix disable end war button --- src/warchron/controller/war_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index a12962a..3fd8a2d 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -101,7 +101,7 @@ class WarController: ) ) self.app.view.display_war_participants(rows, objectives_for_display) - self.app.view.endCampaignBtn.setEnabled(not war.is_over) + self.app.view.endWarBtn.setEnabled(not war.is_over) def _validate_war_inputs(self, name: str, year: int) -> bool: if not name.strip(): @@ -162,6 +162,7 @@ class WarController: ) # TODO fix spent tokens on tie cancel + # TODO fix ignored campaign tie-breaks def resolve_ties( self, war: War, contexts: List[TieContext] ) -> Dict[str, Dict[str, bool]]: From 097823fab09dd4d5d8193f3c0917d809b27f61a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Tue, 24 Feb 2026 10:04:16 +0100 Subject: [PATCH 3/4] fix canceled tie spent tokens --- .../controller/campaign_controller.py | 2 +- src/warchron/controller/war_controller.py | 2 +- src/warchron/model/result_checker.py | 1 - src/warchron/model/tie_manager.py | 23 +++++++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 9228a97..d66154e 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -186,7 +186,6 @@ class CampaignController: RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=campaign_id ) - # TODO fix spent tokens on tie cancel def resolve_ties( self, war: War, contexts: List[TieContext] ) -> Dict[str, Dict[str, bool]]: @@ -208,6 +207,7 @@ class CampaignController: context_id=ctx.context_id, ) if not dialog.exec(): + TieResolver.cancel_tie_break(war, ContextType.CAMPAIGN, ctx.context_id) raise ForbiddenOperation("Tie resolution cancelled") bids_map[ctx.context_id] = dialog.get_bids() return bids_map diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 3fd8a2d..54f28f6 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -161,7 +161,6 @@ class WarController: RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id ) - # TODO fix spent tokens on tie cancel # TODO fix ignored campaign tie-breaks def resolve_ties( self, war: War, contexts: List[TieContext] @@ -184,6 +183,7 @@ class WarController: context_id=ctx.context_id, ) if not dialog.exec(): + TieResolver.cancel_tie_break(war, ContextType.WAR, ctx.context_id) raise ForbiddenOperation("Tie resolution cancelled") bids_map[ctx.context_id] = dialog.get_bids() return bids_map diff --git a/src/warchron/model/result_checker.py b/src/warchron/model/result_checker.py index 978b0ce..d69003a 100644 --- a/src/warchron/model/result_checker.py +++ b/src/warchron/model/result_checker.py @@ -28,7 +28,6 @@ class ResultChecker: and ev.context_id == context_id ): return ev.participant_id # None if confirmed draw - return None @staticmethod diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index 8ea0e75..c398592 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -130,6 +130,29 @@ class TieResolver: ) ) + @staticmethod + def cancel_tie_break( + war: War, + context_type: ContextType, + context_id: str, + ) -> None: + war.events = [ + ev + for ev in war.events + if not ( + ( + isinstance(ev, InfluenceSpent) + and ev.context_type == context_type + and ev.context_id == context_id + ) + or ( + isinstance(ev, TieResolved) + and ev.context_type == context_type + and ev.context_id == context_id + ) + ) + ] + @staticmethod def rank_by_tokens( war: War, From 789756d586fb0c48d381e357e1861cb61b1d67fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Tue, 24 Feb 2026 11:07:06 +0100 Subject: [PATCH 4/4] fix forbidden sector add/update/remove in closed rounds --- src/warchron/model/campaign.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/warchron/model/campaign.py b/src/warchron/model/campaign.py index eb55bd3..55bc899 100644 --- a/src/warchron/model/campaign.py +++ b/src/warchron/model/campaign.py @@ -107,7 +107,7 @@ class Campaign: def remove_campaign_participant(self, participant_id: str) -> None: if self.is_over: raise ForbiddenOperation("Can't remove participant in a closed campaign.") - rounds_blocking: list[Round] = [] + rounds_blocking: List[Round] = [] for rnd in self.rounds: if rnd.has_choice_with_participant( participant_id @@ -158,6 +158,10 @@ class Campaign: ) -> Sector: if self.is_over: raise ForbiddenOperation("Can't add sector in a closed campaign.") + if round_id is not None: + round = self.get_round(round_id) + if round.is_over: + raise ForbiddenOperation("Can't add sector with a closed round.") sect = Sector( name, round_id, major_id, minor_id, influence_id, mission, description ) @@ -187,11 +191,18 @@ class Campaign: mission: str | None, description: str | None, ) -> None: - # TODO raise error if sector used in a closed round (potential tokens) if self.is_over: raise ForbiddenOperation("Can't update sector in a closed campaign.") sect = self.get_sector(sector_id) old_round_id = sect.round_id + if old_round_id is not None: + old_round = self.get_round(old_round_id) + if old_round.is_over: + raise ForbiddenOperation("Can't update sector used in a closed round.") + if round_id is not None: + new_round = self.get_round(round_id) + if new_round.is_over: + raise ForbiddenOperation("Can't update sector with a closed round.") def apply_update() -> None: sect.set_name(name) @@ -205,7 +216,7 @@ class Campaign: if old_round_id == round_id: apply_update() return - affected_rounds: list[Round] = [] + affected_rounds: List[Round] = [] for rnd in self.rounds: if rnd.id == old_round_id and ( rnd.has_choice_with_sector(sector_id) @@ -235,7 +246,14 @@ class Campaign: def remove_sector(self, sector_id: str) -> None: if self.is_over: raise ForbiddenOperation("Can't remove sector in a closed campaign.") - rounds_blocking: list[Round] = [] + sect = self.get_sector(sector_id) + round_id = sect.round_id + if round_id is not None: + round = self.get_round(round_id) + if round.is_over: + raise ForbiddenOperation("Can't remove sector used in a closed round.") + + rounds_blocking: List[Round] = [] for rnd in self.rounds: if rnd.has_battle_with_sector(sector_id) or rnd.has_choice_with_sector( sector_id