diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 97586b2..2477378 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -167,7 +167,7 @@ class CampaignController: def resolve_ties( self, war: War, contexts: List[TieContext] - ) -> Dict[tuple[str, str, int | None, str | None, str | None], Dict[str, bool]]: + ) -> Dict[tuple[str, str, int | None], Dict[str, bool]]: bids_map = {} for ctx in contexts: active = TieResolver.get_active_participants(war, ctx, ctx.participants) @@ -180,7 +180,7 @@ class CampaignController: parent=self.app.view, players=players, counters=counters, - context_type=ctx.context_type, + context_type=ContextType.CAMPAIGN, context_id=ctx.context_id, context_name=None, ) @@ -192,7 +192,7 @@ class CampaignController: counters=counters, context_type=ctx.context_type, context_id=ctx.context_id, - context_name=f"Objective tie: {objective.name}", + context_name=objective.name, ) if not dialog.exec(): TieResolver.cancel_tie_break(war, ctx) diff --git a/src/warchron/controller/closure_workflow.py b/src/warchron/controller/closure_workflow.py index f824d26..ba02870 100644 --- a/src/warchron/controller/closure_workflow.py +++ b/src/warchron/controller/closure_workflow.py @@ -1,5 +1,4 @@ from typing import TYPE_CHECKING -from uuid import uuid4 if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -26,9 +25,8 @@ class RoundClosureWorkflow(ClosureWorkflow): bids_map = self.app.rounds.resolve_ties(war, ties) for tie in ties: bids = bids_map[tie.key()] - tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4()) - TieResolver.apply_bids(war, tie, tie_id, bids) - TieResolver.resolve_tie_state(war, tie, tie_id, bids) + TieResolver.apply_bids(war, tie, 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) @@ -44,9 +42,8 @@ class CampaignClosureWorkflow(ClosureWorkflow): bids_map = self.app.campaigns.resolve_ties(war, ties) for tie in ties: bids = bids_map[tie.key()] - tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4()) - TieResolver.apply_bids(war, tie, tie_id, bids) - TieResolver.resolve_tie_state(war, tie, tie_id, bids) + TieResolver.apply_bids(war, tie, bids) + TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_campaign_ties(war, campaign.id) for obj in war.get_objectives_used_as_maj_or_min(): objective_id = obj.id @@ -59,9 +56,8 @@ class CampaignClosureWorkflow(ClosureWorkflow): bids_map = self.app.campaigns.resolve_ties(war, ties) for tie in ties: bids = bids_map[tie.key()] - tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4()) - TieResolver.apply_bids(war, tie, tie_id, bids) - TieResolver.resolve_tie_state(war, tie, tie_id, bids) + TieResolver.apply_bids(war, tie, bids) + TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_campaign_objective_ties( war, campaign.id, @@ -79,9 +75,8 @@ class WarClosureWorkflow(ClosureWorkflow): bids_map = self.app.wars.resolve_ties(war, ties) for tie in ties: bids = bids_map[tie.key()] - tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4()) - TieResolver.apply_bids(war, tie, tie_id, bids) - TieResolver.resolve_tie_state(war, tie, tie_id, bids) + TieResolver.apply_bids(war, tie, bids) + TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_war_ties(war) for obj in war.get_objectives_used_as_maj_or_min(): objective_id = obj.id @@ -93,9 +88,8 @@ class WarClosureWorkflow(ClosureWorkflow): bids_map = self.app.wars.resolve_ties(war, ties) for tie in ties: bids = bids_map[tie.key()] - tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4()) - TieResolver.apply_bids(war, tie, tie_id, bids) - TieResolver.resolve_tie_state(war, tie, tie_id, bids) + TieResolver.apply_bids(war, tie, bids) + TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_war_objective_ties( war, objective_id, @@ -109,7 +103,7 @@ class RoundPairingWorkflow: self.app = controller def start(self, war: War, round: Round) -> None: - Pairing.check_round_pairable(war, round) + Pairing.check_round_pairable(round) Pairing.assign_battles_to_participants( war, round, diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 3b7f5e6..aeb86a2 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -72,7 +72,6 @@ class RoundController: comment=choice.comment, ) ) - # TODO display allocated sectors and used token self.app.view.display_round_choices(choices_for_display) battles_for_display: List[BattleDTO] = [] for sect in sectors: @@ -90,7 +89,6 @@ class RoundController: player_1_name = self.app.model.get_participant_name( camp_part.war_participant_id ) - p1_id = battle.player_1_id else: player_1_name = "" if battle.player_2_id: @@ -98,7 +96,6 @@ class RoundController: player_2_name = self.app.model.get_participant_name( camp_part.war_participant_id ) - p2_id = battle.player_2_id else: player_2_name = "" if battle.winner_id: @@ -115,11 +112,7 @@ class RoundController: if battle.is_draw(): p1_icon = Icons.get(IconName.DRAW) p2_icon = Icons.get(IconName.DRAW) - context = TieContext( - ContextType.BATTLE, - battle.sector_id, - [p1_id, p2_id], - ) + context = TieContext(ContextType.BATTLE, battle.sector_id) if TieResolver.was_tie_broken_by_tokens(war, context): effective_winner = ResultChecker.get_effective_winner_id( war, ContextType.BATTLE, battle.sector_id, None @@ -204,7 +197,7 @@ class RoundController: str(e), ) for bat in rnd.battles.values(): - bat.clear_battle_players() + bat.cleanup_battle_players() return except DomainError as e: QMessageBox.warning( @@ -232,7 +225,7 @@ class RoundController: def resolve_ties( self, war: War, contexts: List[TieContext] - ) -> Dict[tuple[str, str, int | None, str | None, str | None], Dict[str, bool]]: + ) -> Dict[tuple[str, str, int | None], Dict[str, bool]]: bids_map = {} for ctx in contexts: players = [ @@ -243,12 +236,11 @@ 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 dialog = TieDialog( parent=self.app.view, players=players, counters=counters, - context_type=ctx.context_type, + context_type=ContextType.BATTLE, context_id=ctx.context_id, ) if not dialog.exec(): diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 2c000e9..8f065ad 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -58,7 +58,7 @@ class WarController: ] scores = ScoreService.compute_scores(war, ContextType.WAR, war.id) rows: List[WarParticipantScoreDTO] = [] - vp_icon_map: Dict[str, QIcon] = {} + vp_icon_map: dict[str, QIcon] = {} objective_icon_maps: Dict[str, Dict[str, QIcon]] = {} if war.is_over: vp_icon_map = RankingIcon.compute_icons( @@ -157,7 +157,7 @@ class WarController: def resolve_ties( self, war: War, contexts: List[TieContext] - ) -> Dict[tuple[str, str, int | None, str | None, str | None], Dict[str, bool]]: + ) -> Dict[tuple[str, str, int | None], Dict[str, bool]]: bids_map = {} for ctx in contexts: active = TieResolver.get_active_participants( @@ -174,7 +174,7 @@ class WarController: parent=self.app.view, players=players, counters=counters, - context_type=ctx.context_type, + context_type=ContextType.WAR, context_id=ctx.context_id, context_name=None, ) diff --git a/src/warchron/model/battle.py b/src/warchron/model/battle.py index 02a9e90..a34a3b3 100644 --- a/src/warchron/model/battle.py +++ b/src/warchron/model/battle.py @@ -57,7 +57,7 @@ class Battle: return False def get_available_places(self) -> List[str]: - places: List[str] = [] + places: list[str] = [] if self.player_1_id is None: places.append("player_1") if self.player_2_id is None: @@ -73,7 +73,7 @@ class Battle: return raise DomainError("Battle has no available places") - def clear_battle_players(self) -> None: + def cleanup_battle_players(self) -> None: self.player_1_id = None self.player_2_id = None diff --git a/src/warchron/model/campaign.py b/src/warchron/model/campaign.py index 4efc374..9f88f5b 100644 --- a/src/warchron/model/campaign.py +++ b/src/warchron/model/campaign.py @@ -1,9 +1,7 @@ from __future__ import annotations from uuid import uuid4 -from typing import Any, Dict, List, Set, TYPE_CHECKING +from typing import Any, Dict, List, Set -if TYPE_CHECKING: - from warchron.model.war import War from warchron.model.exception import ForbiddenOperation, RequiresConfirmation from warchron.model.campaign_participant import CampaignParticipant from warchron.model.sector import Sector @@ -21,7 +19,6 @@ class Campaign: self.sectors: Dict[str, Sector] = {} self.rounds: List[Round] = [] self.is_over = False - self._war: War | None = None # private link def set_id(self, new_id: str) -> None: self.id = new_id @@ -63,8 +60,8 @@ class Campaign: # Campaign participant methods - def get_all_campaign_participants_ids(self) -> List[str]: - return list(self.participants.keys()) + def get_all_campaign_participants_ids(self) -> set[str]: + return set(self.participants.keys()) def has_participant(self, participant_id: str) -> bool: return participant_id in self.participants @@ -354,7 +351,6 @@ class Campaign: if self.is_over: raise ForbiddenOperation("Can't add round in a closed campaign.") round = Round() - round._campaign = self self.rounds.append(round) return round diff --git a/src/warchron/model/model.py b/src/warchron/model/model.py index ca6dc23..65a4a7b 100644 --- a/src/warchron/model/model.py +++ b/src/warchron/model/model.py @@ -360,8 +360,8 @@ class Model: ) def remove_sector(self, sector_id: str) -> None: - war = self.get_war_by_sector(sector_id) - war.remove_sector(sector_id) + camp = self.get_campaign_by_sector(sector_id) + camp.remove_sector(sector_id) # Campaign participant methods diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py index 5936e36..4ec0641 100644 --- a/src/warchron/model/pairing.py +++ b/src/warchron/model/pairing.py @@ -1,7 +1,5 @@ from __future__ import annotations from typing import Dict, List, Callable, Tuple -from uuid import uuid4 - import random from warchron.constants import ContextType, ScoreKind @@ -20,7 +18,7 @@ from warchron.model.score_service import ParticipantScore ResolveTiesCallback = Callable[ ["War", List["TieContext"]], - Dict[Tuple[str, str, int | None, str | None, str | None], Dict[str, bool]], + Dict[Tuple[str, str, int | None], Dict[str, bool]], ] @@ -28,7 +26,6 @@ class Pairing: @staticmethod def check_round_pairable( - war: War, round: Round, ) -> None: if round.is_over: @@ -51,9 +48,8 @@ class Pairing: def cleanup() -> None: for bat in round.battles.values(): - bat.clear_battle_players() - bat.set_winner(None) - war.revert_choice_ties(round.id) + bat.cleanup_battle_players() + # FIXME cancel TieResolved + TokenSpent if any( bat.player_1_id is not None or bat.player_2_id is not None @@ -62,7 +58,6 @@ class Pairing: raise RequiresConfirmation( "Battle(s) already have player(s) assigned for this round.\n" "Battle players will be cleared.\n" - "Choice tokens and tie-breaks will be deleted.\n" "Do you want to continue?", action=cleanup, ) @@ -95,20 +90,20 @@ class Pairing: campaign.war_to_campaign_part_id(pid) for pid in group ] Pairing._run_phase( - war=war, - round=round, - remaining=remaining, - sector_to_battle=sector_to_battle, - resolve_ties_callback=resolve_ties_callback, + war, + round, + remaining, + sector_to_battle, + resolve_ties_callback, use_priority=True, score_value=score_value, ) Pairing._run_phase( - war=war, - round=round, - remaining=remaining, - sector_to_battle=sector_to_battle, - resolve_ties_callback=resolve_ties_callback, + war, + round, + remaining, + sector_to_battle, + resolve_ties_callback, use_priority=False, score_value=score_value, ) @@ -138,13 +133,13 @@ class Pairing: if places <= 0: continue winners = Pairing._resolve_sector_allocation( - war=war, - round=round, - sector_id=sector_id, - participants=participants, - places=places, - resolve_ties_callback=resolve_ties_callback, - score_value=score_value, + war, + round, + sector_id, + participants, + places, + resolve_ties_callback, + score_value, ) for pid in winners: battle.assign_participant(pid) @@ -192,59 +187,26 @@ class Pairing: participants=[ campaign.campaign_to_war_part_id(pid) for pid in participants ], - score_value=score_value, score_kind=ScoreKind.VP, sector_id=sector_id, ) # ---- resolve tie loop ---- - tie_id = TieResolver.find_active_tie_id(war, context) or str(uuid4()) while not TieResolver.is_tie_resolved(war, context): - active = TieResolver.get_active_participants( - war, context, context.participants - ) - if len(active) <= 1: - break - current_context = TieContext( - context_type=context.context_type, - context_id=context.context_id, - participants=active, - score_value=context.score_value, - score_kind=context.score_kind, - sector_id=context.sector_id, - ) - if not TieResolver.can_tie_be_resolved( - war, context, current_context.participants - ): + if not TieResolver.can_tie_be_resolved(war, context, context.participants): war.events.append( TieResolved( None, context.context_type, context.context_id, - participants, - tie_id=tie_id, score_value=score_value, sector_id=sector_id, ) ) break - bids_map = resolve_ties_callback(war, [current_context]) - bids = bids_map[current_context.key()] - # confirmed draw if current bids are 0 - if bids is not None and not any(bids.values()): - war.events.append( - TieResolved( - None, - context.context_type, - context.context_id, - participants=context.participants, - tie_id=tie_id, - score_value=context.score_value, - objective_id=context.objective_id, - ) - ) - break - TieResolver.apply_bids(war, context, tie_id, bids) - TieResolver.resolve_tie_state(war, context, tie_id, bids) + bids_map = resolve_ties_callback(war, [context]) + bids = bids_map[context.key()] + TieResolver.apply_bids(war, context, bids) + TieResolver.resolve_tie_state(war, context, bids) ranked_groups = TieResolver.rank_by_tokens( war, context, @@ -253,8 +215,6 @@ class Pairing: ordered: List[str] = [] for group in ranked_groups: shuffled_group = list(group) - # TODO improve tie break with history parsing - # TODO avoid rematch random.shuffle(shuffled_group) ordered.extend( campaign.war_to_campaign_part_id(pid) for pid in shuffled_group diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py index f7d8039..df9c927 100644 --- a/src/warchron/model/round.py +++ b/src/warchron/model/round.py @@ -1,9 +1,7 @@ from __future__ import annotations from uuid import uuid4 -from typing import Any, Dict, List, TYPE_CHECKING +from typing import Any, Dict, List -if TYPE_CHECKING: - from warchron.model.campaign import Campaign from warchron.model.exception import ForbiddenOperation from warchron.model.choice import Choice from warchron.model.battle import Battle @@ -15,7 +13,6 @@ class Round: self.choices: Dict[str, Choice] = {} self.battles: Dict[str, Battle] = {} self.is_over: bool = False - self._campaign: Campaign | None = None # private link def set_id(self, new_id: str) -> None: self.id = new_id @@ -63,7 +60,6 @@ class Round: def create_choice(self, participant_id: str) -> Choice: if self.is_over: - # TODO catch me if you can raise ForbiddenOperation("Can't create choice in a closed round.") if participant_id not in self.choices: choice = Choice( @@ -82,38 +78,23 @@ class Round: comment: str | None, ) -> None: if self.is_over: - # TODO catch me if you can raise ForbiddenOperation("Can't update choice in a closed round.") - # TODO prevent if battles already assigned choice = self.get_choice(participant_id) if choice: choice.set_priority(priority_sector_id) choice.set_secondary(secondary_sector_id) choice.set_comment(comment) - # FIXME remove corresponding InfluenceSpent and TieResolved def clear_sector_references(self, sector_id: str) -> None: for choice in self.choices.values(): - trigger_revert_ties = False if choice.priority_sector_id == sector_id: choice.priority_sector_id = None - trigger_revert_ties = True if choice.secondary_sector_id == sector_id: choice.secondary_sector_id = None - trigger_revert_ties = True - if trigger_revert_ties: - if self._campaign and self._campaign._war: - self._campaign._war.revert_choice_ties(self.id, sector_id=sector_id) def remove_choice(self, participant_id: str) -> None: if self.is_over: - # TODO catch me if you can (inner) raise ForbiddenOperation("Can't remove choice in a closed round.") - # TODO prevent if battles already assigned - if self._campaign and self._campaign._war: - self._campaign._war.revert_choice_ties( - self.id, participants=[participant_id] - ) del self.choices[participant_id] # Battle methods @@ -145,7 +126,6 @@ class Round: def create_battle(self, sector_id: str) -> Battle: if self.is_over: - # TODO catch me if you can raise ForbiddenOperation("Can't create battle in a closed round.") if sector_id not in self.battles: battle = Battle(sector_id=sector_id, player_1_id=None, player_2_id=None) @@ -163,10 +143,8 @@ class Round: comment: str | None, ) -> None: if self.is_over: - # TODO catch me if you can raise ForbiddenOperation("Can't update battle in a closed round.") bat = self.get_battle(sector_id) - # TODO require confirmation if there was choice tie to clear it if bat: bat.set_player_1(player_1_id) bat.set_player_2(player_2_id) @@ -177,27 +155,17 @@ class Round: def clear_participant_references(self, participant_id: str) -> None: for battle in self.battles.values(): - trigger_revert_ties = False if battle.player_1_id == participant_id: battle.player_1_id = None - trigger_revert_ties = True if battle.player_2_id == participant_id: battle.player_2_id = None - trigger_revert_ties = True if battle.winner_id == participant_id: battle.winner_id = None - if trigger_revert_ties: - if self._campaign and self._campaign._war: - self._campaign._war.revert_battle_ties(battle.sector_id) def remove_battle(self, sector_id: str) -> None: if self.is_over: - # TODO catch me if you can raise ForbiddenOperation("Can't remove battle in a closed round.") bat = self.battles[sector_id] if bat and bat.is_finished(): - # TODO catch me if you can raise ForbiddenOperation("Can't remove finished battle.") - if self._campaign and self._campaign._war: - self._campaign._war.revert_battle_ties(sector_id) del self.battles[sector_id] diff --git a/src/warchron/model/score_service.py b/src/warchron/model/score_service.py index 40b0891..f061729 100644 --- a/src/warchron/model/score_service.py +++ b/src/warchron/model/score_service.py @@ -44,17 +44,11 @@ class ScoreService: ) -> Dict[str, ParticipantScore]: from warchron.model.result_checker import ResultChecker - if context_type == ContextType.CAMPAIGN: - camp = war.get_campaign(context_id) - camp_pids = camp.get_all_campaign_participants_ids() - participant_ids = [(camp.campaign_to_war_part_id(pid)) for pid in camp_pids] - elif context_type == ContextType.WAR: - participant_ids = war.get_all_war_participants_ids() scores = { pid: ParticipantScore( narrative_points={obj_id: 0 for obj_id in war.objectives} ) - for pid in participant_ids + for pid in war.participants } battles = ScoreService._get_battles_for_context(war, context_type, context_id) for battle in battles: diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index 4694f57..47ca576 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -1,5 +1,5 @@ -from typing import List, Dict, DefaultDict, Tuple -from dataclasses import dataclass +from typing import List, Dict, DefaultDict +from dataclasses import dataclass, field from collections import defaultdict from warchron.constants import ContextType, ScoreKind @@ -13,70 +13,41 @@ from warchron.model.score_service import ScoreService, ParticipantScore class TieContext: context_type: ContextType context_id: str - participants: List[str] + participants: List[str] = field(default_factory=list) # war_participant_ids score_value: int | None = None score_kind: ScoreKind | None = None objective_id: str | None = None sector_id: str | None = None - def key(self) -> Tuple[str, str, int | None, str | None, str | None]: - return ( - self.context_type, - self.context_id, - self.score_value, - self.objective_id, - self.sector_id, - ) + def key(self) -> tuple[str, str, int | None]: + return (self.context_type, self.context_id, self.score_value) class TieResolver: - @staticmethod - def find_active_tie_id( - war: War, - context: TieContext, - ) -> str | None: - for ev in reversed(war.events): - if ( - isinstance(ev, InfluenceSpent) - and ev.context_type == context.context_type - and ev.context_id == context.context_id - and ev.objective_id == context.objective_id - and ev.sector_id == context.sector_id - and ev.participant_id in context.participants - ): - return ev.tie_id - return None - @staticmethod def find_battle_ties(war: War, round_id: str) -> List[TieContext]: round = war.get_round(round_id) campaign = war.get_campaign_by_round(round_id) ties = [] for battle in round.battles.values(): + if not battle.is_draw(): + continue + context: TieContext = TieContext( + ContextType.BATTLE, + battle.sector_id, + ) + if TieResolver.is_tie_resolved(war, context): + continue if campaign is None: raise DomainError("No campaign for this battle tie") if battle.player_1_id is None or battle.player_2_id is None: raise DomainError("Missing player(s) in this battle context.") p1_id = campaign.campaign_to_war_part_id(battle.player_1_id) p2_id = campaign.campaign_to_war_part_id(battle.player_2_id) - if not battle.is_draw(): - continue - context: TieContext = TieContext( - ContextType.BATTLE, battle.sector_id, [p1_id, p2_id] - ) - if TieResolver.is_tie_resolved(war, context): - continue - tie_id = TieResolver.find_active_tie_id(war, context) if not TieResolver.can_tie_be_resolved(war, context, [p1_id, p2_id]): war.events.append( - TieResolved( - participant_id=None, - context_type=ContextType.BATTLE, - context_id=battle.sector_id, - participants=[p1_id, p2_id], - tie_id=tie_id, - ) + TieResolved(None, ContextType.BATTLE, battle.sector_id) ) continue ties.append( @@ -109,16 +80,13 @@ class TieResolver: ) if TieResolver.is_tie_resolved(war, context): continue - tie_id = TieResolver.find_active_tie_id(war, context) if not TieResolver.can_tie_be_resolved(war, context, participants): war.events.append( TieResolved( - participant_id=None, - context_type=ContextType.CAMPAIGN, - context_id=campaign_id, - participants=participants, - tie_id=tie_id, - score_value=score_value, + None, + ContextType.CAMPAIGN, + campaign_id, + score_value, ) ) continue @@ -135,7 +103,9 @@ class TieResolver: @staticmethod def find_campaign_objective_ties( - war: War, campaign_id: str, objective_id: str + war: War, + campaign_id: str, + objective_id: str, ) -> List[TieContext]: scores = ScoreService.compute_scores( war, @@ -161,7 +131,6 @@ class TieResolver: ) if TieResolver.is_tie_resolved(war, context): continue - tie_id = TieResolver.find_active_tie_id(war, context) if not TieResolver.can_tie_be_resolved( war, context, @@ -169,13 +138,7 @@ class TieResolver: ): war.events.append( TieResolved( - participant_id=None, - context_type=ContextType.CAMPAIGN, - context_id=context_id, - participants=participants, - tie_id=tie_id, - score_value=np_value, - objective_id=objective_id, + None, ContextType.CAMPAIGN, context_id, np_value, objective_id ) ) continue @@ -218,17 +181,9 @@ class TieResolver: ) if TieResolver.is_tie_resolved(war, context): continue - tie_id = TieResolver.find_active_tie_id(war, context) if not TieResolver.can_tie_be_resolved(war, context, group): war.events.append( - TieResolved( - participant_id=None, - context_type=ContextType.WAR, - context_id=war.id, - participants=group, - tie_id=tie_id, - score_value=score_value, - ) + TieResolved(None, ContextType.WAR, war.id, score_value) ) continue ties.append( @@ -243,7 +198,10 @@ class TieResolver: return ties @staticmethod - def find_war_objective_ties(war: War, objective_id: str) -> List[TieContext]: + def find_war_objective_ties( + war: War, + objective_id: str, + ) -> List[TieContext]: from warchron.model.result_checker import ResultChecker scores = ScoreService.compute_scores( @@ -277,22 +235,13 @@ class TieResolver: context, ): continue - tie_id = TieResolver.find_active_tie_id(war, context) if not TieResolver.can_tie_be_resolved( war, context, group, ): war.events.append( - TieResolved( - participant_id=None, - context_type=ContextType.WAR, - context_id=war.id, - participants=group, - tie_id=tie_id, - score_value=np_value, - objective_id=objective_id, - ) + TieResolved(None, ContextType.WAR, war.id, np_value, objective_id) ) continue ties.append( @@ -311,7 +260,6 @@ class TieResolver: def apply_bids( war: War, context: TieContext, - tie_id: str, bids: Dict[str, bool], # war_participant_id -> spend? ) -> None: for war_part_id, spend in bids.items(): @@ -325,7 +273,6 @@ class TieResolver: amount=1, context_type=context.context_type, context_id=context.context_id, - tie_id=tie_id, objective_id=context.objective_id, ) ) @@ -340,7 +287,12 @@ class TieResolver: for ev in war.events if not ( ( - (isinstance(ev, InfluenceSpent) or isinstance(ev, TieResolved)) + isinstance(ev, InfluenceSpent) + and ev.context_type == context.context_type + and ev.context_id == context.context_id + ) + or ( + isinstance(ev, TieResolved) and ev.context_type == context.context_type and ev.context_id == context.context_id ) @@ -404,9 +356,25 @@ class TieResolver: def resolve_tie_state( war: War, context: TieContext, - tie_id: str, - bids: Dict[str, bool] | None = None, + bids: dict[str, bool] | None = None, ) -> None: + active = TieResolver.get_active_participants( + war, + context, + context.participants, + ) + # confirmed draw if none had bid + if not active: + war.events.append( + TieResolved( + None, + context.context_type, + context.context_id, + score_value=context.score_value, + objective_id=context.objective_id, + ) + ) + return # confirmed draw if current bids are 0 if bids is not None and not any(bids.values()): war.events.append( @@ -414,13 +382,12 @@ class TieResolver: None, context.context_type, context.context_id, - participants=context.participants, - tie_id=tie_id, score_value=context.score_value, objective_id=context.objective_id, ) ) return + # else rank_by_tokens groups = TieResolver.rank_by_tokens(war, context, context.participants) if len(groups[0]) == 1: war.events.append( @@ -428,10 +395,8 @@ class TieResolver: groups[0][0], context.context_type, context.context_id, - participants=context.participants, - tie_id=tie_id, - score_value=context.score_value, - objective_id=context.objective_id, + context.score_value, + context.objective_id, ) ) return @@ -462,24 +427,11 @@ class TieResolver: @staticmethod def is_tie_resolved(war: War, context: TieContext) -> bool: - for ev in war.events: - if not isinstance(ev, TieResolved): - continue - if ev.context_type != context.context_type: - continue - if ev.context_id != context.context_id: - continue - if ( - context.score_value is not None - and ev.score_value != context.score_value - ): - continue - if ( - context.objective_id is not None - and ev.objective_id != context.objective_id - ): - continue - if context.sector_id is not None and ev.sector_id != context.sector_id: - continue - return True - return False + return any( + isinstance(ev, TieResolved) + and ev.context_type == context.context_type + and ev.context_id == context.context_id + and ev.score_value == context.score_value + and ev.objective_id == context.objective_id + for ev in war.events + ) diff --git a/src/warchron/model/war.py b/src/warchron/model/war.py index b7bc964..62767e4 100644 --- a/src/warchron/model/war.py +++ b/src/warchron/model/war.py @@ -1,7 +1,7 @@ from __future__ import annotations from uuid import uuid4 from datetime import datetime -from typing import Any, Dict, List, Set +from typing import Any, Dict, List from warchron.constants import ContextType from warchron.model.war_event import ( @@ -62,7 +62,7 @@ class War: if self.is_over: raise ForbiddenOperation("Can't set influence token of a closed war.") - def remove_token_and_draw_tie() -> None: + def cleanup_token_and_tie() -> None: new_events: List[WarEvent] = [] for ev in self.events: if isinstance(ev, (InfluenceSpent, InfluenceGained)): @@ -73,10 +73,11 @@ class War: self.events = new_events self.influence_token = new_state - def remove_tiebreak_and_not_over() -> None: + def reset_tie_break() -> None: new_events: List[WarEvent] = [] for ev in self.events: if isinstance(ev, (TieResolved)): + # FIXME cancel TieResolved + TokenSpent if ev.context_type == ContextType.BATTLE: battle = self.get_battle(ev.context_id) campaign = self.get_campaign_by_sector(battle.sector_id) @@ -85,7 +86,6 @@ class War: elif ev.context_type == ContextType.CAMPAIGN: campaign = self.get_campaign(ev.context_id) campaign.is_over = False - # nothing specific to do with CHOICE (can retrigger pairing) else: new_events.append(ev) self.events = new_events @@ -104,7 +104,7 @@ class War: "Some influence tokens already exist in this war.\n" "All tokens will be deleted and tie-breaks will become draw.\n" "Do you want to continue?", - action=remove_token_and_draw_tie, + action=cleanup_token_and_tie, ) if new_state is True: has_tie_resolved = any(isinstance(ev, TieResolved) for ev in self.events) @@ -120,7 +120,7 @@ class War: "Some influence tokens and draws exist in this war.\n" "All influence outcomes and tie-breaks will be reset.\n" "Do you want to continue?", - action=remove_tiebreak_and_not_over, + action=reset_tie_break, ) def set_state(self, new_state: bool) -> None: @@ -216,8 +216,8 @@ class War: # War participant methods - def get_all_war_participants_ids(self) -> List[str]: - return list(self.participants.keys()) + def get_all_war_participants_ids(self) -> set[str]: + return set(self.participants.keys()) def has_participant(self, participant_id: str) -> bool: return participant_id in self.participants @@ -288,7 +288,6 @@ class War: if month is None: month = self.get_default_campaign_values()["month"] campaign = Campaign(name, month) - campaign._war = self self.campaigns.append(campaign) return campaign @@ -551,47 +550,3 @@ class War: if isinstance(e, InfluenceSpent) and e.participant_id == participant_id ) return gained - spent - - def get_events_by_ties_session(self, tie_id: str) -> List[WarEvent]: - return [ev for ev in self.events if ev.tie_id == tie_id] - - def remove_ties_session(self, tie_id: str) -> None: - self.events = [ev for ev in self.events if ev.tie_id != tie_id] - - def revert_choice_ties( - self, - round_id: str, - *, - sector_id: str | None = None, - participants: List[str] | None = None, - ) -> None: - removed_ties: Set[str] = set() - for ev in self.events: - if ( - isinstance(ev, TieResolved) - and ev.context_type == ContextType.CHOICE - and ev.context_id == round_id - ): - if ( - sector_id is None - or ev.sector_id == sector_id - or participants is None - or any(p in ev.participants for p in participants) - ): - if ev.tie_id: - removed_ties.add(ev.tie_id) - self.events = [ev for ev in self.events if ev.tie_id not in removed_ties] - - def revert_battle_ties(self, sector_id: str) -> None: - removed_ties = { - ev.tie_id - for ev in self.events - if isinstance(ev, TieResolved) - and ev.context_type == ContextType.BATTLE - and ev.context_id == sector_id - and ev.tie_id - } - self.events = [ev for ev in self.events if ev.tie_id not in removed_ties] - - def revert_tie(self, tie_id: str) -> None: - self.events = [ev for ev in self.events if ev.tie_id != tie_id] diff --git a/src/warchron/model/war_event.py b/src/warchron/model/war_event.py index 4d2ae93..a717241 100644 --- a/src/warchron/model/war_event.py +++ b/src/warchron/model/war_event.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Dict, Any, TypeVar, Type, cast, List +from typing import Dict, Any, TypeVar, Type, cast from datetime import datetime from uuid import uuid4 @@ -22,14 +22,12 @@ class WarEvent: participant_id: str | None, context_type: str, context_id: str, - tie_id: str | None = None, ): self.id: str = str(uuid4()) self.participant_id: str | None = participant_id self.context_type = context_type # battle, round, campaign, war self.context_id = context_id self.timestamp: datetime = datetime.now() - self.tie_id = tie_id def set_id(self, new_id: str) -> None: self.id = new_id @@ -48,7 +46,6 @@ class WarEvent: "context_type": self.context_type, "context_id": self.context_id, "timestamp": self.timestamp.isoformat(), - "tie_id": self.tie_id, } @classmethod @@ -58,7 +55,6 @@ class WarEvent: ev.context_type = data["context_type"] ev.context_id = data["context_id"] ev.timestamp = datetime.fromisoformat(data["timestamp"]) - ev.tie_id = data.get("tie_id") return ev @staticmethod @@ -76,17 +72,14 @@ class TieResolved(WarEvent): def __init__( self, - participant_id: str | None, # winner_id or None if confirmed draw + participant_id: str | None, context_type: str, context_id: str, - participants: List[str], - tie_id: str | None = None, # None if draw without tie-break score_value: int | None = None, objective_id: str | None = None, sector_id: str | None = None, ): - super().__init__(participant_id, context_type, context_id, tie_id) - self.participants = participants + super().__init__(participant_id, context_type, context_id) self.score_value = score_value self.objective_id = objective_id self.sector_id = sector_id @@ -95,7 +88,6 @@ class TieResolved(WarEvent): d = super().toDict() d.update( { - "participants": self.participants, "score_value": self.score_value or None, "objective_id": self.objective_id or None, "sector_id": self.sector_id or None, @@ -109,8 +101,6 @@ class TieResolved(WarEvent): JsonHelper.none_if_empty(data["participant_id"]), data["context_type"], data["context_id"], - data["participants"], - data["tie_id"], JsonHelper.none_if_empty(data["score_value"]), JsonHelper.none_if_empty(data["objective_id"]), JsonHelper.none_if_empty(data["sector_id"]), @@ -166,14 +156,11 @@ class InfluenceSpent(WarEvent): amount: int, context_type: str, context_id: str, - tie_id: str, objective_id: str | None = None, - sector_id: str | None = None, ): - super().__init__(participant_id, context_type, context_id, tie_id) + super().__init__(participant_id, context_type, context_id) self.amount = amount self.objective_id = objective_id - self.sector_id = sector_id def toDict(self) -> Dict[str, Any]: d = super().toDict() @@ -181,7 +168,6 @@ class InfluenceSpent(WarEvent): { "amount": self.amount, "objective_id": self.objective_id, - "sector_id": self.sector_id or None, } ) return d @@ -193,8 +179,6 @@ class InfluenceSpent(WarEvent): int(data["amount"]), data["context_type"], data["context_id"], - data["tie_id"], JsonHelper.none_if_empty(data["objective_id"]), - JsonHelper.none_if_empty(data["sector_id"]), ) return cls._base_fromDict(ev, data) diff --git a/test_data/example.json b/test_data/example.json index b902826..d40dfa2 100644 --- a/test_data/example.json +++ b/test_data/example.json @@ -338,12 +338,7 @@ "participant_id": null, "context_type": "battle", "context_id": "79accf7c-2d93-4ac3-b747-e7092bfe3feb", - "participants": [ - "602e2eaf-297e-490b-b0e9-efec818e466a", - "1f6b4e7c-b1e4-4a2e-9aea-7bb75b20b4de" - ], "timestamp": "2026-02-26T16:11:44.346337", - "tie_id": null, "score_value": null, "objective_id": null, "sector_id": null