fix choice tiebreak loop and cancel tiebreak lost tokens

This commit is contained in:
Maxime Réaux 2026-03-17 11:16:47 +01:00
parent a3b9f5a943
commit 42ad708e77
13 changed files with 333 additions and 129 deletions

View file

@ -1,5 +1,5 @@
from typing import List, Dict, DefaultDict
from dataclasses import dataclass, field
from typing import List, Dict, DefaultDict, Tuple
from dataclasses import dataclass
from collections import defaultdict
from warchron.constants import ContextType, ScoreKind
@ -13,41 +13,70 @@ from warchron.model.score_service import ScoreService, ParticipantScore
class TieContext:
context_type: ContextType
context_id: str
participants: List[str] = field(default_factory=list) # war_participant_ids
participants: List[str]
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]:
return (self.context_type, self.context_id, self.score_value)
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,
)
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(None, ContextType.BATTLE, battle.sector_id)
TieResolved(
participant_id=None,
context_type=ContextType.BATTLE,
context_id=battle.sector_id,
participants=[p1_id, p2_id],
tie_id=tie_id,
)
)
continue
ties.append(
@ -80,13 +109,16 @@ 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(
None,
ContextType.CAMPAIGN,
campaign_id,
score_value,
participant_id=None,
context_type=ContextType.CAMPAIGN,
context_id=campaign_id,
participants=participants,
tie_id=tie_id,
score_value=score_value,
)
)
continue
@ -103,9 +135,7 @@ 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,
@ -131,6 +161,7 @@ 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,
@ -138,7 +169,13 @@ class TieResolver:
):
war.events.append(
TieResolved(
None, ContextType.CAMPAIGN, context_id, np_value, objective_id
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,
)
)
continue
@ -181,9 +218,17 @@ 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(None, ContextType.WAR, war.id, score_value)
TieResolved(
participant_id=None,
context_type=ContextType.WAR,
context_id=war.id,
participants=group,
tie_id=tie_id,
score_value=score_value,
)
)
continue
ties.append(
@ -198,10 +243,7 @@ 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(
@ -235,13 +277,22 @@ 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(None, ContextType.WAR, war.id, np_value, objective_id)
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,
)
)
continue
ties.append(
@ -260,6 +311,7 @@ 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():
@ -273,6 +325,7 @@ class TieResolver:
amount=1,
context_type=context.context_type,
context_id=context.context_id,
tie_id=tie_id,
objective_id=context.objective_id,
)
)
@ -287,12 +340,7 @@ class TieResolver:
for ev in war.events
if not (
(
isinstance(ev, InfluenceSpent)
and ev.context_type == context.context_type
and ev.context_id == context.context_id
)
or (
isinstance(ev, TieResolved)
(isinstance(ev, InfluenceSpent) or isinstance(ev, TieResolved))
and ev.context_type == context.context_type
and ev.context_id == context.context_id
)
@ -356,25 +404,9 @@ class TieResolver:
def resolve_tie_state(
war: War,
context: TieContext,
bids: dict[str, bool] | None = None,
tie_id: str,
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(
@ -382,12 +414,13 @@ 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(
@ -395,8 +428,10 @@ class TieResolver:
groups[0][0],
context.context_type,
context.context_id,
context.score_value,
context.objective_id,
participants=context.participants,
tie_id=tie_id,
score_value=context.score_value,
objective_id=context.objective_id,
)
)
return
@ -427,11 +462,24 @@ class TieResolver:
@staticmethod
def is_tie_resolved(war: War, context: TieContext) -> bool:
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
)
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