diff --git a/src/warchron/constants.py b/src/warchron/constants.py index 495565d..d2188a5 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -22,6 +22,7 @@ class IconName(StrEnum): PAIRING = auto() DRAW = auto() TIEBREAK = auto() + DRAWTOKEN = auto() DELETE = auto() SAVE_AS = auto() SAVE = auto() @@ -149,6 +150,11 @@ class Icons: cls.get_pixmap(IconName.TIEBREAK), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.DRAWTOKEN: + pix = cls._compose( + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) elif name == IconName.WINTOKEN: pix = cls._compose( cls.get_pixmap(IconName.WIN), diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 097e0c8..393a76b 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -24,6 +24,7 @@ from warchron.model.war import War if TYPE_CHECKING: from warchron.controller.app_controller import AppController + from warchron.model.campaign import Campaign from warchron.controller.dtos import ( ParticipantOption, @@ -254,13 +255,15 @@ class RoundController: for pid in ctx.participants ] counters = [war.get_influence_tokens(pid) for pid in ctx.participants] + round: Round | None = None + campaign: Campaign | None = None if ctx.context_type == ContextType.BATTLE: - # context_id = battle.sector_id + # context_id corresponds to 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 + # context_id corresponds to round.id campaign = war.get_campaign_by_round(ctx.context_id) if campaign: round = war.get_round(ctx.context_id) diff --git a/src/warchron/controller/workflows.py b/src/warchron/controller/workflows.py index 37a692f..ea0a4b3 100644 --- a/src/warchron/controller/workflows.py +++ b/src/warchron/controller/workflows.py @@ -1,5 +1,4 @@ from typing import TYPE_CHECKING -from uuid import uuid4 if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -23,13 +22,12 @@ class RoundClosureWorkflow(Workflow): Closer.check_round_closable(round) ties = TieBreaker.find_battle_ties(war, round.id) while ties: - bids_map = self.app.rounds.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.key()] - tie_id = TieBreaker.find_active_tie_id(war, tie) or str(uuid4()) - # TODO finish tiebreak by group (like choice) not by turn - TieBreaker.apply_bids(war, tie, tie_id, bids) - TieBreaker.resolve_tie_state(war, tie, tie_id, bids) + TieBreaker.resolve_group( + war, + tie, + self.app.rounds.resolve_ties, + ) ties = TieBreaker.find_battle_ties(war, round.id) for battle in round.battles.values(): Closer.apply_battle_outcomes(war, campaign, battle) @@ -42,13 +40,12 @@ class CampaignClosureWorkflow(Workflow): Closer.check_campaign_closable(campaign) ties = TieBreaker.find_campaign_ties(war, campaign.id) while ties: - bids_map = self.app.campaigns.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.key()] - tie_id = TieBreaker.find_active_tie_id(war, tie) or str(uuid4()) - # TODO finish tiebreak by group (like choice) not by turn - TieBreaker.apply_bids(war, tie, tie_id, bids) - TieBreaker.resolve_tie_state(war, tie, tie_id, bids) + TieBreaker.resolve_group( + war, + tie, + self.app.rounds.resolve_ties, + ) ties = TieBreaker.find_campaign_ties(war, campaign.id) for obj in war.get_objectives_used_as_maj_or_min(): objective_id = obj.id @@ -58,13 +55,12 @@ class CampaignClosureWorkflow(Workflow): objective_id, ) while ties: - bids_map = self.app.campaigns.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.key()] - tie_id = TieBreaker.find_active_tie_id(war, tie) or str(uuid4()) - # TODO finish tiebreak by group (like choice) not by turn - TieBreaker.apply_bids(war, tie, tie_id, bids) - TieBreaker.resolve_tie_state(war, tie, tie_id, bids) + TieBreaker.resolve_group( + war, + tie, + self.app.rounds.resolve_ties, + ) ties = TieBreaker.find_campaign_objective_ties( war, campaign.id, @@ -79,13 +75,12 @@ class WarClosureWorkflow(Workflow): Closer.check_war_closable(war) ties = TieBreaker.find_war_ties(war) while ties: - bids_map = self.app.wars.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.key()] - tie_id = TieBreaker.find_active_tie_id(war, tie) or str(uuid4()) - # TODO finish tiebreak by group (like choice) not by turn - TieBreaker.apply_bids(war, tie, tie_id, bids) - TieBreaker.resolve_tie_state(war, tie, tie_id, bids) + TieBreaker.resolve_group( + war, + tie, + self.app.rounds.resolve_ties, + ) ties = TieBreaker.find_war_ties(war) for obj in war.get_objectives_used_as_maj_or_min(): objective_id = obj.id @@ -94,13 +89,12 @@ class WarClosureWorkflow(Workflow): objective_id, ) while ties: - bids_map = self.app.wars.resolve_ties(war, ties) for tie in ties: - bids = bids_map[tie.key()] - tie_id = TieBreaker.find_active_tie_id(war, tie) or str(uuid4()) - # TODO finish tiebreak by group (like choice) not by turn - TieBreaker.apply_bids(war, tie, tie_id, bids) - TieBreaker.resolve_tie_state(war, tie, tie_id, bids) + TieBreaker.resolve_group( + war, + tie, + self.app.rounds.resolve_ties, + ) ties = TieBreaker.find_war_objective_ties( war, objective_id, diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py index a186220..a8c0613 100644 --- a/src/warchron/model/pairing.py +++ b/src/warchron/model/pairing.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Dict, List, Callable, Tuple +from typing import Dict, List from dataclasses import dataclass from uuid import uuid4 @@ -15,15 +15,10 @@ from warchron.model.war import War from warchron.model.round import Round from warchron.model.battle import Battle from warchron.model.scoring import ScoreComputer -from warchron.model.tiebreaking import TieBreaker, TieContext +from warchron.model.tiebreaking import TieBreaker, TieContext, ResolveTiesCallback from warchron.model.war_event import TieResolved from warchron.model.scoring import ParticipantScore -ResolveTiesCallback = Callable[ - ["War", List["TieContext"]], - Dict[Tuple[str, str, int | None, str | None, str | None], Dict[str, bool]], -] - @dataclass(frozen=True, slots=True) class AllocationResult: diff --git a/src/warchron/model/tiebreaking.py b/src/warchron/model/tiebreaking.py index 8b47ec8..ed11ed0 100644 --- a/src/warchron/model/tiebreaking.py +++ b/src/warchron/model/tiebreaking.py @@ -1,6 +1,7 @@ -from typing import List, Dict, DefaultDict, Tuple +from __future__ import annotations +from typing import List, Dict, Tuple, Callable, TypeAlias from dataclasses import dataclass -from collections import defaultdict +from uuid import uuid4 from warchron.constants import ContextType, ScoreKind from warchron.model.exception import ForbiddenOperation, DomainError @@ -29,6 +30,12 @@ class TieContext: ) +ResolveTiesCallback: TypeAlias = Callable[ + [War, List[TieContext]], + Dict[Tuple[str, str, int | None, str | None, str | None], Dict[str, bool]], +] + + class TieBreaker: @staticmethod @@ -92,14 +99,22 @@ class TieBreaker: @staticmethod def find_campaign_ties(war: War, campaign_id: str) -> List[TieContext]: + from warchron.model.checking import ResultChecker + scores = ScoreComputer.compute_scores(war, ContextType.CAMPAIGN, campaign_id) - buckets: DefaultDict[int, List[str]] = defaultdict(list) - for pid, score in scores.items(): - buckets[score.victory_points].append(pid) + ranking = ResultChecker.get_effective_ranking( + war, + ContextType.CAMPAIGN, + campaign_id, + ScoreKind.VP, + scores, + lambda s: s.victory_points, + ) ties: List[TieContext] = [] - for score_value, participants in buckets.items(): - if len(participants) <= 1: + for _, group, _ in ranking: + if len(group) <= 1: continue + score_value = scores[group[0]].victory_points context: TieContext = TieContext( ContextType.CAMPAIGN, campaign_id, @@ -110,13 +125,13 @@ class TieBreaker: if TieBreaker.is_tie_resolved(war, context): continue tie_id = TieBreaker.find_active_tie_id(war, context) - if not TieBreaker.can_tie_be_resolved(war, context, participants): + if not TieBreaker.can_tie_be_resolved(war, context, group): war.events.append( TieResolved( participant_id=None, context_type=ContextType.CAMPAIGN, context_id=campaign_id, - participants=participants, + participants=group, tie_id=tie_id, score_value=score_value, ) @@ -126,7 +141,7 @@ class TieBreaker: TieContext( context_type=ContextType.CAMPAIGN, context_id=campaign_id, - participants=participants, + participants=group, score_value=score_value, score_kind=ScoreKind.VP, ) @@ -137,20 +152,32 @@ class TieBreaker: def find_campaign_objective_ties( war: War, campaign_id: str, objective_id: str ) -> List[TieContext]: + from warchron.model.checking import ResultChecker + scores = ScoreComputer.compute_scores( war, ContextType.CAMPAIGN, campaign_id, ) - buckets: DefaultDict[int, List[str]] = defaultdict(list) - for pid, score in scores.items(): - np_value = score.narrative_points.get(objective_id, 0) - buckets[np_value].append(pid) + + def value_getter(score: ParticipantScore) -> int: + return score.narrative_points.get(objective_id, 0) + + ranking = ResultChecker.get_effective_ranking( + war, + ContextType.CAMPAIGN, + campaign_id, + ScoreKind.NP, + scores, + value_getter, + objective_id, + ) ties: List[TieContext] = [] context_id = campaign_id - for np_value, participants in buckets.items(): - if len(participants) <= 1: + for _, group, _ in ranking: + if len(group) <= 1: continue + np_value = value_getter(scores[group[0]]) context: TieContext = TieContext( ContextType.CAMPAIGN, campaign_id, @@ -165,14 +192,14 @@ class TieBreaker: if not TieBreaker.can_tie_be_resolved( war, context, - participants, + group, ): war.events.append( TieResolved( participant_id=None, context_type=ContextType.CAMPAIGN, context_id=context_id, - participants=participants, + participants=group, tie_id=tie_id, score_value=np_value, objective_id=objective_id, @@ -183,7 +210,7 @@ class TieBreaker: TieContext( context_type=ContextType.CAMPAIGN, context_id=context_id, - participants=participants, + participants=group, score_value=np_value, score_kind=ScoreKind.NP, objective_id=objective_id, @@ -307,6 +334,51 @@ class TieBreaker: ) return ties + @staticmethod + def resolve_group( + war: War, + context: TieContext, + resolve_ties_callback: ResolveTiesCallback, + ) -> None: + tie_id = TieBreaker.find_active_tie_id(war, context) or str(uuid4()) + while not TieBreaker.is_tie_resolved(war, context): + active = TieBreaker.get_active_participants( + war, + context, + context.participants, + ) + 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, + objective_id=context.objective_id, + sector_id=context.sector_id, + ) + if not TieBreaker.can_tie_be_resolved( + war, + context, + current_context.participants, + ): + 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, + sector_id=context.sector_id, + ) + ) + return + bids_map = resolve_ties_callback(war, [current_context]) + bids = bids_map[current_context.key()] + TieBreaker.apply_bids(war, context, tie_id, bids) + TieBreaker.resolve_tie_state(war, context, tie_id, bids) + @staticmethod def apply_bids( war: War, @@ -447,22 +519,6 @@ class TieBreaker: active = TieBreaker.get_active_participants(war, context, participants) return any(war.get_influence_tokens(pid) > 0 for pid in active) - @staticmethod - def was_tie_broken_by_tokens( - war: War, - context: TieContext, - ) -> bool: - for ev in reversed(war.events): - if ( - 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 - ): - return ev.participant_id is not None - return False - @staticmethod def is_tie_resolved(war: War, context: TieContext) -> bool: for ev in war.events: