From 6efd22527add30159398f0f89175d5b139fc5ac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Wed, 25 Feb 2026 16:54:21 +0100 Subject: [PATCH] fix re-enable token on closed camapign + refacto war_event attributes --- .../controller/campaign_controller.py | 17 +++-- src/warchron/controller/closure_workflow.py | 18 ++--- src/warchron/controller/round_controller.py | 12 ++-- src/warchron/controller/war_controller.py | 22 ++++-- src/warchron/model/closure_service.py | 5 +- src/warchron/model/result_checker.py | 7 +- src/warchron/model/tie_manager.py | 67 +++++++++++++------ src/warchron/model/war_event.py | 17 ++++- 8 files changed, 108 insertions(+), 57 deletions(-) diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 147bd93..125d59b 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -1,4 +1,4 @@ -from typing import List, Dict, TYPE_CHECKING +from typing import List, Dict, Tuple, TYPE_CHECKING from PyQt6.QtWidgets import QMessageBox, QDialog from PyQt6.QtGui import QIcon @@ -54,9 +54,8 @@ class CampaignController: icon_map = {} for rank, group, token_map in ranking: base_icon = RANK_TO_ICON.get(rank, IconName.VPNTH) - tie_id = f"{campaign.id}:score:{scores[group[0]].victory_points}" tie_resolved = TieResolver.is_tie_resolved( - war, ContextType.CAMPAIGN, tie_id + war, ContextType.CAMPAIGN, campaign.id, scores[group[0]].victory_points ) for pid in group: spent = token_map.get(pid, 0) @@ -189,7 +188,7 @@ class CampaignController: def resolve_ties( self, war: War, contexts: List[TieContext] - ) -> Dict[str, Dict[str, bool]]: + ) -> Dict[Tuple[ContextType, str, int | None], Dict[str, bool]]: bids_map = {} for ctx in contexts: active = TieResolver.get_active_participants( @@ -208,11 +207,17 @@ class CampaignController: context_id=ctx.context_id, ) if not dialog.exec(): - TieResolver.cancel_tie_break(war, ContextType.CAMPAIGN, ctx.context_id) + TieResolver.cancel_tie_break( + war, ContextType.CAMPAIGN, ctx.context_id, ctx.score_value + ) raise ForbiddenOperation("Tie resolution cancelled") - bids_map[ctx.context_id] = dialog.get_bids() + bids_map[(ctx.context_type, ctx.context_id, ctx.score_value)] = ( + dialog.get_bids() + ) return bids_map + # Campaign participant methods + def create_campaign_participant(self) -> CampaignParticipant | None: if not self.app.navigation.selected_campaign_id: return None diff --git a/src/warchron/controller/closure_workflow.py b/src/warchron/controller/closure_workflow.py index d668338..fe368ac 100644 --- a/src/warchron/controller/closure_workflow.py +++ b/src/warchron/controller/closure_workflow.py @@ -24,11 +24,9 @@ class RoundClosureWorkflow(ClosureWorkflow): while ties: bids_map = self.app.rounds.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.context_id] + bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)] TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids) - TieResolver.resolve_tie_state( - war, tie.context_type, tie.context_id, tie.participants, bids - ) + TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_battle_ties(war, round.id) for battle in round.battles.values(): ClosureService.apply_battle_outcomes(war, campaign, battle) @@ -43,11 +41,9 @@ class CampaignClosureWorkflow(ClosureWorkflow): while ties: bids_map = self.app.campaigns.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.context_id] + bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)] TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids) - TieResolver.resolve_tie_state( - war, tie.context_type, tie.context_id, tie.participants, bids - ) + TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_campaign_ties(war, campaign.id) ClosureService.finalize_campaign(campaign) @@ -60,10 +56,8 @@ class WarClosureWorkflow(ClosureWorkflow): while ties: bids_map = self.app.wars.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.context_id] + bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)] TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids) - TieResolver.resolve_tie_state( - war, tie.context_type, tie.context_id, tie.participants, bids - ) + TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_war_ties(war) ClosureService.finalize_war(war) diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 13bb664..5af114b 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -1,4 +1,4 @@ -from typing import List, Dict, TYPE_CHECKING +from typing import List, Dict, Tuple, TYPE_CHECKING from PyQt6.QtWidgets import QDialog from PyQt6.QtWidgets import QMessageBox @@ -179,7 +179,7 @@ class RoundController: def resolve_ties( self, war: War, contexts: List[TieContext] - ) -> Dict[str, Dict[str, bool]]: + ) -> Dict[Tuple[ContextType, str, int | None], Dict[str, bool]]: bids_map = {} for ctx in contexts: players = [ @@ -198,9 +198,13 @@ class RoundController: context_id=ctx.context_id, ) if not dialog.exec(): - TieResolver.cancel_tie_break(war, ContextType.BATTLE, ctx.context_id) + TieResolver.cancel_tie_break( + war, ContextType.BATTLE, ctx.context_id, ctx.score_value + ) raise ForbiddenOperation("Tie resolution cancelled") - bids_map[ctx.context_id] = dialog.get_bids() + bids_map[(ctx.context_type, ctx.context_id, ctx.score_value)] = ( + dialog.get_bids() + ) return bids_map # Choice methods diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 5ebf567..c950363 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -1,4 +1,4 @@ -from typing import List, TYPE_CHECKING, Dict +from typing import List, Tuple, TYPE_CHECKING, Dict from PyQt6.QtWidgets import QMessageBox, QDialog from PyQt6.QtGui import QIcon @@ -53,8 +53,9 @@ class WarController: icon_map = {} for rank, group, token_map in ranking: base_icon = RANK_TO_ICON.get(rank, IconName.VPNTH) - tie_id = f"{war.id}:score:{scores[group[0]].victory_points}" - tie_resolved = TieResolver.is_tie_resolved(war, ContextType.WAR, tie_id) + tie_resolved = TieResolver.is_tie_resolved( + war, ContextType.WAR, war.id, scores[group[0]].victory_points + ) for pid in group: spent = token_map.get(pid, 0) if not tie_resolved and spent == 0: @@ -169,11 +170,14 @@ class WarController: # TODO fix ignored campaign tie-breaks def resolve_ties( self, war: War, contexts: List[TieContext] - ) -> Dict[str, Dict[str, bool]]: + ) -> Dict[Tuple[ContextType, str, int | None], Dict[str, bool]]: bids_map = {} for ctx in contexts: active = TieResolver.get_active_participants( - war, ctx.context_type, ctx.context_id, ctx.participants + war, + ctx.context_type, + ctx.context_id, + ctx.participants, ) players = [ ParticipantOption(id=pid, name=self.app.model.get_participant_name(pid)) @@ -188,9 +192,13 @@ class WarController: context_id=ctx.context_id, ) if not dialog.exec(): - TieResolver.cancel_tie_break(war, ContextType.WAR, ctx.context_id) + TieResolver.cancel_tie_break( + war, ContextType.WAR, ctx.context_id, ctx.score_value + ) raise ForbiddenOperation("Tie resolution cancelled") - bids_map[ctx.context_id] = dialog.get_bids() + bids_map[(ctx.context_type, ctx.context_id, ctx.score_value)] = ( + dialog.get_bids() + ) return bids_map def set_major_value(self, value: int) -> None: diff --git a/src/warchron/model/closure_service.py b/src/warchron/model/closure_service.py index 21a3b5f..0cc16f3 100644 --- a/src/warchron/model/closure_service.py +++ b/src/warchron/model/closure_service.py @@ -27,8 +27,7 @@ class ClosureService: from warchron.model.result_checker import ResultChecker already_granted = any( - isinstance(e, InfluenceGained) - and e.context_id == f"battle:{battle.sector_id}" + isinstance(e, InfluenceGained) and e.context_id == battle.sector_id for e in war.events ) if already_granted: @@ -51,7 +50,7 @@ class ClosureService: participant_id=effective_winner, amount=1, context_type=ContextType.BATTLE, - context_id=f"battle:{battle.sector_id}", + context_id=battle.sector_id, ) ) diff --git a/src/warchron/model/result_checker.py b/src/warchron/model/result_checker.py index d69003a..a8c7661 100644 --- a/src/warchron/model/result_checker.py +++ b/src/warchron/model/result_checker.py @@ -45,10 +45,9 @@ class ResultChecker: current_rank = 1 for vp in sorted_vps: participants = vp_buckets[vp] - tie_id = f"{context_id}:score:{vp}" # no tie if len(participants) == 1 or not TieResolver.is_tie_resolved( - war, context_type, tie_id + war, context_type, context_id, vp ): ranking.append( (current_rank, participants, {pid: 0 for pid in participants}) @@ -59,11 +58,11 @@ class ResultChecker: groups = TieResolver.rank_by_tokens( war, context_type, - tie_id, + context_id, participants, ) tokens_spent = TieResolver.tokens_spent_map( - war, context_type, tie_id, participants + war, context_type, context_id, participants ) for group in groups: group_tokens = {pid: tokens_spent[pid] for pid in group} diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index c398592..4275ae2 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -14,6 +14,7 @@ class TieContext: context_type: ContextType context_id: str participants: List[str] # war_participant_ids + score_value: int | None = None class TieResolver: @@ -47,6 +48,7 @@ class TieResolver: context_type=ContextType.BATTLE, context_id=battle.sector_id, participants=[p1, p2], + score_value=None, ) ) return ties @@ -63,19 +65,23 @@ class TieResolver: for score_value, participants in buckets.items(): if len(participants) <= 1: continue - tie_id = f"{campaign_id}:score:{score_value}" - if TieResolver.is_tie_resolved(war, ContextType.CAMPAIGN, tie_id): + if TieResolver.is_tie_resolved( + war, ContextType.CAMPAIGN, campaign_id, score_value + ): continue if not TieResolver.can_tie_be_resolved( - war, ContextType.CAMPAIGN, tie_id, participants + war, ContextType.CAMPAIGN, campaign_id, participants ): - war.events.append(TieResolved(None, ContextType.CAMPAIGN, tie_id)) + war.events.append( + TieResolved(None, ContextType.CAMPAIGN, campaign_id, score_value) + ) continue ties.append( TieContext( context_type=ContextType.CAMPAIGN, - context_id=tie_id, + context_id=campaign_id, participants=participants, + score_value=score_value, ) ) return ties @@ -92,19 +98,21 @@ class TieResolver: for score_value, participants in buckets.items(): if len(participants) <= 1: continue - tie_id = f"{war.id}:score:{score_value}" - if TieResolver.is_tie_resolved(war, ContextType.WAR, tie_id): + if TieResolver.is_tie_resolved(war, ContextType.WAR, war.id, score_value): continue if not TieResolver.can_tie_be_resolved( - war, ContextType.WAR, tie_id, participants + war, ContextType.WAR, war.id, participants ): - war.events.append(TieResolved(None, ContextType.WAR, tie_id)) + war.events.append( + TieResolved(None, ContextType.WAR, war.id, score_value) + ) continue ties.append( TieContext( context_type=ContextType.WAR, - context_id=tie_id, + context_id=war.id, participants=participants, + score_value=score_value, ) ) return ties @@ -135,6 +143,7 @@ class TieResolver: war: War, context_type: ContextType, context_id: str, + score_value: int | None = None, ) -> None: war.events = [ ev @@ -149,6 +158,7 @@ class TieResolver: isinstance(ev, TieResolved) and ev.context_type == context_type and ev.context_id == context_id + and ev.score_value == score_value ) ) ] @@ -210,26 +220,37 @@ class TieResolver: @staticmethod def resolve_tie_state( war: War, - context_type: ContextType, - context_id: str, - participants: List[str], + ctx: TieContext, bids: dict[str, bool] | None = None, ) -> None: active = TieResolver.get_active_participants( - war, context_type, context_id, participants + war, + ctx.context_type, + ctx.context_id, + ctx.participants, ) # confirmed draw if non had bid if not active: - war.events.append(TieResolved(None, context_type, context_id)) + war.events.append( + TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value) + ) return # confirmed draw if current bids are 0 if bids is not None and not any(bids.values()): - war.events.append(TieResolved(None, context_type, context_id)) + war.events.append( + TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value) + ) return # else rank_by_tokens - groups = TieResolver.rank_by_tokens(war, context_type, context_id, participants) + groups = TieResolver.rank_by_tokens( + war, ctx.context_type, ctx.context_id, ctx.participants + ) if len(groups[0]) == 1: - war.events.append(TieResolved(groups[0][0], context_type, context_id)) + war.events.append( + TieResolved( + groups[0][0], ctx.context_type, ctx.context_id, ctx.score_value + ) + ) return # if tie persists, do nothing, workflow will call again @@ -247,21 +268,29 @@ class TieResolver: war: War, context_type: ContextType, context_id: str, + score_value: int | None = None, ) -> bool: for ev in reversed(war.events): if ( isinstance(ev, TieResolved) and ev.context_type == context_type and ev.context_id == context_id + and ev.score_value == score_value ): return ev.participant_id is not None return False @staticmethod - def is_tie_resolved(war: War, context_type: ContextType, context_id: str) -> bool: + def is_tie_resolved( + war: War, + context_type: ContextType, + context_id: str, + score_value: int | None = None, + ) -> bool: return any( isinstance(ev, TieResolved) and ev.context_type == context_type and ev.context_id == context_id + and ev.score_value == score_value for ev in war.events ) diff --git a/src/warchron/model/war_event.py b/src/warchron/model/war_event.py index 62209cd..3f3f4db 100644 --- a/src/warchron/model/war_event.py +++ b/src/warchron/model/war_event.py @@ -63,19 +63,32 @@ class WarEvent: class TieResolved(WarEvent): TYPE = "TieResolved" - def __init__(self, participant_id: str | None, context_type: str, context_id: str): + def __init__( + self, + participant_id: str | None, + context_type: str, + context_id: str, + score_value: int | None = None, + ): super().__init__(participant_id, context_type, context_id) + self.score_value = score_value def toDict(self) -> Dict[str, Any]: d = super().toDict() + d.update( + { + "score_value": self.score_value or None, + } + ) return d @classmethod def fromDict(cls, data: Dict[str, Any]) -> TieResolved: ev = cls( - data["participant_id"], + data["participant_id"] or None, data["context_type"], data["context_id"], + data["score_value"] or None, ) return cls._base_fromDict(ev, data)