fix ignored campaign NP tie-break when closing war
This commit is contained in:
parent
b1bde76319
commit
72f80563f1
16 changed files with 314 additions and 219 deletions
|
|
@ -1,8 +1,8 @@
|
|||
from typing import List, Dict, DefaultDict
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from collections import defaultdict
|
||||
|
||||
from warchron.constants import ContextType
|
||||
from warchron.constants import ContextType, ScoreKind
|
||||
from warchron.model.exception import ForbiddenOperation
|
||||
from warchron.model.war import War
|
||||
from warchron.model.war_event import InfluenceSpent, TieResolved
|
||||
|
|
@ -13,8 +13,13 @@ from warchron.model.score_service import ScoreService, ParticipantScore
|
|||
class TieContext:
|
||||
context_type: ContextType
|
||||
context_id: str
|
||||
participants: List[str] # war_participant_ids
|
||||
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
|
||||
|
||||
def key(self) -> tuple[str, str, int | None]:
|
||||
return (self.context_type, self.context_id, self.score_value)
|
||||
|
||||
|
||||
class TieResolver:
|
||||
|
|
@ -27,9 +32,11 @@ class TieResolver:
|
|||
for battle in round.battles.values():
|
||||
if not battle.is_draw():
|
||||
continue
|
||||
if TieResolver.is_tie_resolved(
|
||||
war, ContextType.BATTLE, battle.sector_id, None
|
||||
):
|
||||
context: TieContext = TieContext(
|
||||
ContextType.BATTLE,
|
||||
battle.sector_id,
|
||||
)
|
||||
if TieResolver.is_tie_resolved(war, context):
|
||||
continue
|
||||
if campaign is None:
|
||||
raise RuntimeError("No campaign for this battle tie")
|
||||
|
|
@ -37,9 +44,7 @@ class TieResolver:
|
|||
raise RuntimeError("Missing player(s) in this battle context.")
|
||||
p1 = campaign.participants[battle.player_1_id].war_participant_id
|
||||
p2 = campaign.participants[battle.player_2_id].war_participant_id
|
||||
if not TieResolver.can_tie_be_resolved(
|
||||
war, ContextType.BATTLE, battle.sector_id, [p1, p2]
|
||||
):
|
||||
if not TieResolver.can_tie_be_resolved(war, context, [p1, p2]):
|
||||
war.events.append(
|
||||
TieResolved(None, ContextType.BATTLE, battle.sector_id)
|
||||
)
|
||||
|
|
@ -50,6 +55,7 @@ class TieResolver:
|
|||
context_id=battle.sector_id,
|
||||
participants=[p1, p2],
|
||||
score_value=None,
|
||||
score_kind=None,
|
||||
)
|
||||
)
|
||||
return ties
|
||||
|
|
@ -64,15 +70,23 @@ class TieResolver:
|
|||
for score_value, participants in buckets.items():
|
||||
if len(participants) <= 1:
|
||||
continue
|
||||
if TieResolver.is_tie_resolved(
|
||||
war, ContextType.CAMPAIGN, campaign_id, score_value
|
||||
):
|
||||
context: TieContext = TieContext(
|
||||
ContextType.CAMPAIGN,
|
||||
campaign_id,
|
||||
[],
|
||||
score_value,
|
||||
ScoreKind.VP,
|
||||
)
|
||||
if TieResolver.is_tie_resolved(war, context):
|
||||
continue
|
||||
if not TieResolver.can_tie_be_resolved(
|
||||
war, ContextType.CAMPAIGN, campaign_id, participants
|
||||
):
|
||||
if not TieResolver.can_tie_be_resolved(war, context, participants):
|
||||
war.events.append(
|
||||
TieResolved(None, ContextType.CAMPAIGN, campaign_id, score_value)
|
||||
TieResolved(
|
||||
None,
|
||||
ContextType.CAMPAIGN,
|
||||
campaign_id,
|
||||
score_value,
|
||||
)
|
||||
)
|
||||
continue
|
||||
ties.append(
|
||||
|
|
@ -81,6 +95,7 @@ class TieResolver:
|
|||
context_id=campaign_id,
|
||||
participants=participants,
|
||||
score_value=score_value,
|
||||
score_kind=ScoreKind.VP,
|
||||
)
|
||||
)
|
||||
return ties
|
||||
|
|
@ -101,38 +116,39 @@ class TieResolver:
|
|||
np_value = score.narrative_points.get(objective_id, 0)
|
||||
buckets[np_value].append(pid)
|
||||
ties: List[TieContext] = []
|
||||
context_id = f"{campaign_id}:{objective_id}"
|
||||
context_id = campaign_id
|
||||
for np_value, participants in buckets.items():
|
||||
if len(participants) <= 1:
|
||||
continue
|
||||
if TieResolver.is_tie_resolved(
|
||||
war,
|
||||
ContextType.OBJECTIVE,
|
||||
context_id,
|
||||
context: TieContext = TieContext(
|
||||
ContextType.CAMPAIGN,
|
||||
campaign_id,
|
||||
[],
|
||||
np_value,
|
||||
):
|
||||
ScoreKind.NP,
|
||||
objective_id,
|
||||
)
|
||||
if TieResolver.is_tie_resolved(war, context):
|
||||
continue
|
||||
if not TieResolver.can_tie_be_resolved(
|
||||
war,
|
||||
ContextType.OBJECTIVE,
|
||||
context_id,
|
||||
context,
|
||||
participants,
|
||||
):
|
||||
war.events.append(
|
||||
TieResolved(
|
||||
None,
|
||||
ContextType.OBJECTIVE,
|
||||
context_id,
|
||||
np_value,
|
||||
None, ContextType.CAMPAIGN, context_id, np_value, objective_id
|
||||
)
|
||||
)
|
||||
continue
|
||||
ties.append(
|
||||
TieContext(
|
||||
context_type=ContextType.OBJECTIVE,
|
||||
context_type=ContextType.CAMPAIGN,
|
||||
context_id=context_id,
|
||||
participants=participants,
|
||||
score_value=np_value,
|
||||
score_kind=ScoreKind.NP,
|
||||
objective_id=objective_id,
|
||||
)
|
||||
)
|
||||
return ties
|
||||
|
|
@ -146,17 +162,25 @@ class TieResolver:
|
|||
war,
|
||||
ContextType.WAR,
|
||||
war.id,
|
||||
ScoreKind.VP,
|
||||
scores,
|
||||
value_getter=lambda s: s.victory_points,
|
||||
lambda s: s.victory_points,
|
||||
)
|
||||
ties: List[TieContext] = []
|
||||
for _, group, _ in ranking:
|
||||
if len(group) <= 1:
|
||||
continue
|
||||
score_value = scores[group[0]].victory_points
|
||||
if TieResolver.is_tie_resolved(war, ContextType.WAR, war.id, score_value):
|
||||
context: TieContext = TieContext(
|
||||
ContextType.WAR,
|
||||
war.id,
|
||||
[],
|
||||
score_value,
|
||||
ScoreKind.VP,
|
||||
)
|
||||
if TieResolver.is_tie_resolved(war, context):
|
||||
continue
|
||||
if not TieResolver.can_tie_be_resolved(war, ContextType.WAR, war.id, group):
|
||||
if not TieResolver.can_tie_be_resolved(war, context, group):
|
||||
war.events.append(
|
||||
TieResolved(None, ContextType.WAR, war.id, score_value)
|
||||
)
|
||||
|
|
@ -167,6 +191,7 @@ class TieResolver:
|
|||
context_id=war.id,
|
||||
participants=group,
|
||||
score_value=score_value,
|
||||
score_kind=ScoreKind.VP,
|
||||
)
|
||||
)
|
||||
return ties
|
||||
|
|
@ -189,45 +214,43 @@ class TieResolver:
|
|||
|
||||
ranking = ResultChecker.get_effective_ranking(
|
||||
war,
|
||||
ContextType.OBJECTIVE,
|
||||
f"{war.id}:{objective_id}",
|
||||
ContextType.WAR,
|
||||
war.id,
|
||||
ScoreKind.NP,
|
||||
scores,
|
||||
value_getter=value_getter,
|
||||
value_getter,
|
||||
objective_id,
|
||||
)
|
||||
ties: List[TieContext] = []
|
||||
for _, group, _ in ranking:
|
||||
if len(group) <= 1:
|
||||
continue
|
||||
np_value = value_getter(scores[group[0]])
|
||||
context_id = f"{war.id}:{objective_id}"
|
||||
context: TieContext = TieContext(
|
||||
ContextType.WAR, war.id, [], np_value, ScoreKind.NP, objective_id
|
||||
)
|
||||
if TieResolver.is_tie_resolved(
|
||||
war,
|
||||
ContextType.OBJECTIVE,
|
||||
context_id,
|
||||
np_value,
|
||||
context,
|
||||
):
|
||||
continue
|
||||
if not TieResolver.can_tie_be_resolved(
|
||||
war,
|
||||
ContextType.OBJECTIVE,
|
||||
context_id,
|
||||
context,
|
||||
group,
|
||||
):
|
||||
war.events.append(
|
||||
TieResolved(
|
||||
None,
|
||||
ContextType.OBJECTIVE,
|
||||
context_id,
|
||||
np_value,
|
||||
)
|
||||
TieResolved(None, ContextType.WAR, war.id, np_value, objective_id)
|
||||
)
|
||||
continue
|
||||
ties.append(
|
||||
TieContext(
|
||||
context_type=ContextType.OBJECTIVE,
|
||||
context_id=context_id,
|
||||
context_type=ContextType.WAR,
|
||||
context_id=war.id,
|
||||
participants=group,
|
||||
score_value=np_value,
|
||||
score_kind=ScoreKind.NP,
|
||||
objective_id=objective_id,
|
||||
)
|
||||
)
|
||||
return ties
|
||||
|
|
@ -235,8 +258,7 @@ class TieResolver:
|
|||
@staticmethod
|
||||
def apply_bids(
|
||||
war: War,
|
||||
context_type: ContextType,
|
||||
context_id: str,
|
||||
context: TieContext,
|
||||
bids: Dict[str, bool], # war_participant_id -> spend?
|
||||
) -> None:
|
||||
for war_part_id, spend in bids.items():
|
||||
|
|
@ -248,8 +270,9 @@ class TieResolver:
|
|||
InfluenceSpent(
|
||||
participant_id=war_part_id,
|
||||
amount=1,
|
||||
context_type=context_type,
|
||||
context_id=context_id,
|
||||
context_type=context.context_type,
|
||||
context_id=context.context_id,
|
||||
objective_id=context.objective_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -257,9 +280,7 @@ class TieResolver:
|
|||
@staticmethod
|
||||
def cancel_tie_break(
|
||||
war: War,
|
||||
context_type: ContextType,
|
||||
context_id: str,
|
||||
score_value: int | None = None,
|
||||
context: TieContext,
|
||||
) -> None:
|
||||
war.events = [
|
||||
ev
|
||||
|
|
@ -267,14 +288,15 @@ class TieResolver:
|
|||
if not (
|
||||
(
|
||||
isinstance(ev, InfluenceSpent)
|
||||
and ev.context_type == context_type
|
||||
and ev.context_id == context_id
|
||||
and ev.context_type == context.context_type
|
||||
and ev.context_id == context.context_id
|
||||
)
|
||||
or (
|
||||
isinstance(ev, TieResolved)
|
||||
and ev.context_type == context_type
|
||||
and ev.context_id == context_id
|
||||
and ev.score_value == score_value
|
||||
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
|
||||
)
|
||||
)
|
||||
]
|
||||
|
|
@ -282,16 +304,16 @@ class TieResolver:
|
|||
@staticmethod
|
||||
def rank_by_tokens(
|
||||
war: War,
|
||||
context_type: ContextType,
|
||||
context_id: str,
|
||||
context: TieContext,
|
||||
participants: List[str],
|
||||
) -> List[List[str]]:
|
||||
spent = {pid: 0 for pid in participants}
|
||||
for ev in war.events:
|
||||
if (
|
||||
isinstance(ev, InfluenceSpent)
|
||||
and ev.context_type == context_type
|
||||
and ev.context_id == context_id
|
||||
and ev.context_type == context.context_type
|
||||
and ev.context_id == context.context_id
|
||||
and ev.objective_id == context.objective_id
|
||||
and ev.participant_id in spent
|
||||
):
|
||||
spent[ev.participant_id] += ev.amount
|
||||
|
|
@ -308,16 +330,16 @@ class TieResolver:
|
|||
@staticmethod
|
||||
def tokens_spent_map(
|
||||
war: War,
|
||||
context_type: ContextType,
|
||||
context_id: str,
|
||||
context: TieContext,
|
||||
participants: List[str],
|
||||
) -> Dict[str, int]:
|
||||
spent = {pid: 0 for pid in participants}
|
||||
for ev in war.events:
|
||||
if (
|
||||
isinstance(ev, InfluenceSpent)
|
||||
and ev.context_type == context_type
|
||||
and ev.context_id == context_id
|
||||
and ev.context_type == context.context_type
|
||||
and ev.context_id == context.context_id
|
||||
and ev.objective_id == context.objective_id
|
||||
and ev.participant_id in spent
|
||||
):
|
||||
spent[ev.participant_id] += ev.amount
|
||||
|
|
@ -326,45 +348,57 @@ class TieResolver:
|
|||
@staticmethod
|
||||
def get_active_participants(
|
||||
war: War,
|
||||
context_type: ContextType,
|
||||
context_id: str,
|
||||
context: TieContext,
|
||||
participants: List[str],
|
||||
) -> List[str]:
|
||||
groups = TieResolver.rank_by_tokens(war, context_type, context_id, participants)
|
||||
groups = TieResolver.rank_by_tokens(war, context, participants)
|
||||
return groups[0]
|
||||
|
||||
@staticmethod
|
||||
def resolve_tie_state(
|
||||
war: War,
|
||||
ctx: TieContext,
|
||||
context: TieContext,
|
||||
bids: dict[str, bool] | None = None,
|
||||
) -> None:
|
||||
active = TieResolver.get_active_participants(
|
||||
war,
|
||||
ctx.context_type,
|
||||
ctx.context_id,
|
||||
ctx.participants,
|
||||
context,
|
||||
context.participants,
|
||||
)
|
||||
# confirmed draw if non had bid
|
||||
if not active:
|
||||
war.events.append(
|
||||
TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value)
|
||||
TieResolved(
|
||||
None,
|
||||
context.context_type,
|
||||
context.context_id,
|
||||
context.score_value,
|
||||
context.objective_id,
|
||||
)
|
||||
)
|
||||
return
|
||||
# confirmed draw if current bids are 0
|
||||
if bids is not None and not any(bids.values()):
|
||||
war.events.append(
|
||||
TieResolved(None, ctx.context_type, ctx.context_id, ctx.score_value)
|
||||
TieResolved(
|
||||
None,
|
||||
context.context_type,
|
||||
context.context_id,
|
||||
context.score_value,
|
||||
context.objective_id,
|
||||
)
|
||||
)
|
||||
return
|
||||
# else rank_by_tokens
|
||||
groups = TieResolver.rank_by_tokens(
|
||||
war, ctx.context_type, ctx.context_id, ctx.participants
|
||||
)
|
||||
groups = TieResolver.rank_by_tokens(war, context, context.participants)
|
||||
if len(groups[0]) == 1:
|
||||
war.events.append(
|
||||
TieResolved(
|
||||
groups[0][0], ctx.context_type, ctx.context_id, ctx.score_value
|
||||
groups[0][0],
|
||||
context.context_type,
|
||||
context.context_id,
|
||||
context.score_value,
|
||||
context.objective_id,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
|
@ -372,41 +406,34 @@ class TieResolver:
|
|||
|
||||
@staticmethod
|
||||
def can_tie_be_resolved(
|
||||
war: War, context_type: ContextType, context_id: str, participants: List[str]
|
||||
war: War, context: TieContext, participants: List[str]
|
||||
) -> bool:
|
||||
active = TieResolver.get_active_participants(
|
||||
war, context_type, context_id, participants
|
||||
)
|
||||
active = TieResolver.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_type: ContextType,
|
||||
context_id: str,
|
||||
score_value: int | None = None,
|
||||
context: TieContext,
|
||||
) -> 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
|
||||
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_type: ContextType,
|
||||
context_id: str,
|
||||
score_value: int | None = None,
|
||||
) -> bool:
|
||||
def is_tie_resolved(war: War, context: TieContext) -> bool:
|
||||
return any(
|
||||
isinstance(ev, TieResolved)
|
||||
and ev.context_type == context_type
|
||||
and ev.context_id == context_id
|
||||
and ev.score_value == score_value
|
||||
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
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue