fix mismatch part_id in choice event + fix revert events on sector/participant removal

This commit is contained in:
Maxime Réaux 2026-03-18 09:26:43 +01:00
parent 42ad708e77
commit db78c6dacc
6 changed files with 145 additions and 53 deletions

View file

@ -23,6 +23,12 @@ class Campaign:
self.is_over = False
self._war: War | None = None # private link
@property
def war(self) -> "War":
if self._war is None:
raise RuntimeError("Campaign is not linked to a War")
return self._war
def set_id(self, new_id: str) -> None:
self.id = new_id

View file

@ -49,6 +49,7 @@ class Model:
self.players[player.id] = player
for w in data.get("wars", []):
war = War.fromDict(w)
war.relink()
self.wars[war.id] = war
except json.JSONDecodeError:
raise RuntimeError("Data file is corrupted")

View file

@ -186,12 +186,13 @@ class Pairing:
campaign = war.get_campaign_by_round(round.id)
if campaign is None:
raise DomainError("Campaign not found for round {round.id}")
war_participants = [
campaign.campaign_to_war_part_id(pid) for pid in participants
]
context = TieContext(
context_type=ContextType.CHOICE,
context_id=round.id,
participants=[
campaign.campaign_to_war_part_id(pid) for pid in participants
],
participants=war_participants,
score_value=score_value,
score_kind=ScoreKind.VP,
sector_id=sector_id,
@ -212,6 +213,7 @@ class Pairing:
score_kind=context.score_kind,
sector_id=context.sector_id,
)
# natural or unbreakable draw
if not TieResolver.can_tie_be_resolved(
war, context, current_context.participants
):
@ -220,7 +222,7 @@ class Pairing:
None,
context.context_type,
context.context_id,
participants,
participants=context.participants,
tie_id=tie_id,
score_value=score_value,
sector_id=sector_id,
@ -254,7 +256,6 @@ class Pairing:
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
@ -266,6 +267,7 @@ class Pairing:
round: Round,
remaining: List[str],
) -> None:
# TODO avoid rematch
for pid in list(remaining):
available = round.get_battles_with_places()
if not available:

View file

@ -4,6 +4,7 @@ from typing import Any, Dict, List, TYPE_CHECKING
if TYPE_CHECKING:
from warchron.model.campaign import Campaign
from warchron.model.war import War
from warchron.model.exception import ForbiddenOperation
from warchron.model.choice import Choice
from warchron.model.battle import Battle
@ -17,6 +18,16 @@ class Round:
self.is_over: bool = False
self._campaign: Campaign | None = None # private link
@property
def campaign(self) -> "Campaign":
if self._campaign is None:
raise RuntimeError("Round is not linked to a Campaign")
return self._campaign
@property
def war(self) -> "War":
return self.campaign.war
def set_id(self, new_id: str) -> None:
self.id = new_id
@ -91,7 +102,6 @@ class Round:
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
@ -102,18 +112,19 @@ class Round:
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)
self.war.revert_choice_ties(self.id, sector_id=sector_id)
def remove_choice(self, participant_id: str) -> None:
if participant_id not in self.choices:
return
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]
)
self.war.revert_choice_ties(
self.id,
participants=[self.campaign.campaign_to_war_part_id(participant_id)],
)
del self.choices[participant_id]
# Battle methods
@ -187,10 +198,16 @@ class Round:
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)
self.war.revert_battle_ties(
self.id,
participants=[
self.campaign.campaign_to_war_part_id(participant_id)
],
)
def remove_battle(self, sector_id: str) -> None:
if sector_id not in self.battles:
return
if self.is_over:
# TODO catch me if you can
raise ForbiddenOperation("Can't remove battle in a closed round.")
@ -198,6 +215,5 @@ class Round:
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)
self.war.revert_battle_ties(self.id, sector_id=sector_id)
del self.battles[sector_id]

View file

@ -161,6 +161,12 @@ class War:
war.set_state(data.get("is_over", False))
return war
def relink(self) -> None:
for campaign in self.campaigns:
campaign._war = self
for rnd in campaign.rounds:
rnd._campaign = campaign
# Objective methods
def add_objective(self, name: str, description: str | None) -> Objective:
@ -552,12 +558,60 @@ class War:
)
return gained - spent
def get_events_by_ties_session(self, tie_id: str) -> List[WarEvent]:
def get_events_by_tie_id(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:
def remove_events_by_tie_id(self, tie_id: str) -> None:
self.events = [ev for ev in self.events if ev.tie_id != tie_id]
def find_ties(
self,
*,
context_type: str,
context_id: str,
sector_id: str | None = None,
participants: List[str] | None = None,
) -> Set[str]:
ties = set()
for ev in self.events:
if not isinstance(ev, TieResolved):
continue
if ev.context_type != context_type:
continue
if ev.context_id != context_id:
continue
if sector_id and ev.sector_id != sector_id:
continue
if participants and not any(p in ev.participants for p in participants):
continue
if ev.tie_id:
ties.add(ev.tie_id)
return ties
def get_draws(
self,
*,
context_type: str,
context_id: str,
sector_id: str | None = None,
participants: List[str] | None = None,
) -> List[WarEvent]:
draws: List[WarEvent] = list()
for ev in self.events:
if not isinstance(ev, TieResolved):
continue
if ev.context_type != context_type:
continue
if ev.context_id != context_id:
continue
if sector_id and ev.sector_id != sector_id:
continue
if participants and not any(p in ev.participants for p in participants):
continue
if ev.tie_id is None:
draws.append(ev)
return draws
def revert_choice_ties(
self,
round_id: str,
@ -565,33 +619,45 @@ class War:
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
removed_ties = self.find_ties(
context_type=ContextType.CHOICE,
context_id=round_id,
sector_id=sector_id,
participants=participants,
)
draws = self.get_draws(
context_type=ContextType.CHOICE,
context_id=round_id,
sector_id=sector_id,
participants=participants,
)
self.events = [
ev
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]
if ev.tie_id not in removed_ties and ev not in draws
]
def revert_tie(self, tie_id: str) -> None:
self.events = [ev for ev in self.events if ev.tie_id != tie_id]
def revert_battle_ties(
self,
round_id: str,
*,
sector_id: str | None = None,
participants: List[str] | None = None,
) -> None:
removed_ties = self.find_ties(
context_type=ContextType.BATTLE,
context_id=round_id,
sector_id=sector_id,
participants=participants,
)
draws = self.get_draws(
context_type=ContextType.BATTLE,
context_id=round_id,
sector_id=sector_id,
participants=participants,
)
self.events = [
ev
for ev in self.events
if ev.tie_id not in removed_ties and ev not in draws
]

View file

@ -325,25 +325,26 @@
"events": [
{
"type": "InfluenceGained",
"id": "8f20b587-0ff3-42c9-9965-88112e2a4e74",
"id": "096802e2-5833-4c6d-941d-c91d98957fc1",
"participant_id": "accc25f2-43a0-4d41-804f-3c8aec853c97",
"context_type": "battle",
"context_id": "4548997e-50e5-493a-b751-483f5bcccc00",
"timestamp": "2026-02-26T16:10:54.125407",
"timestamp": "2026-03-18T09:25:03.746744",
"tie_id": null,
"amount": 1
},
{
"type": "TieResolved",
"id": "c83a9d4e-4620-47de-93c8-d29d6e119c59",
"id": "ec6b8932-b24f-4042-a8f4-566a42d0857d",
"participant_id": null,
"context_type": "battle",
"context_id": "79accf7c-2d93-4ac3-b747-e7092bfe3feb",
"timestamp": "2026-03-18T09:25:08.578539",
"tie_id": "07aa41e5-1696-4b9f-931b-2540e213c7ef",
"participants": [
"602e2eaf-297e-490b-b0e9-efec818e466a",
"1f6b4e7c-b1e4-4a2e-9aea-7bb75b20b4de"
"accc25f2-43a0-4d41-804f-3c8aec853c97",
"179bdab6-8630-4a92-8fb6-d2637562c66c"
],
"timestamp": "2026-02-26T16:11:44.346337",
"tie_id": null,
"score_value": null,
"objective_id": null,
"sector_id": null