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.is_over = False
self._war: War | None = None # private link 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: def set_id(self, new_id: str) -> None:
self.id = new_id self.id = new_id

View file

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

View file

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

View file

@ -4,6 +4,7 @@ from typing import Any, Dict, List, TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from warchron.model.campaign import Campaign from warchron.model.campaign import Campaign
from warchron.model.war import War
from warchron.model.exception import ForbiddenOperation from warchron.model.exception import ForbiddenOperation
from warchron.model.choice import Choice from warchron.model.choice import Choice
from warchron.model.battle import Battle from warchron.model.battle import Battle
@ -17,6 +18,16 @@ class Round:
self.is_over: bool = False self.is_over: bool = False
self._campaign: Campaign | None = None # private link 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: def set_id(self, new_id: str) -> None:
self.id = new_id self.id = new_id
@ -91,7 +102,6 @@ class Round:
choice.set_secondary(secondary_sector_id) choice.set_secondary(secondary_sector_id)
choice.set_comment(comment) choice.set_comment(comment)
# FIXME remove corresponding InfluenceSpent and TieResolved
def clear_sector_references(self, sector_id: str) -> None: def clear_sector_references(self, sector_id: str) -> None:
for choice in self.choices.values(): for choice in self.choices.values():
trigger_revert_ties = False trigger_revert_ties = False
@ -102,18 +112,19 @@ class Round:
choice.secondary_sector_id = None choice.secondary_sector_id = None
trigger_revert_ties = True trigger_revert_ties = True
if trigger_revert_ties: if trigger_revert_ties:
if self._campaign and self._campaign._war: self.war.revert_choice_ties(self.id, sector_id=sector_id)
self._campaign._war.revert_choice_ties(self.id, sector_id=sector_id)
def remove_choice(self, participant_id: str) -> None: def remove_choice(self, participant_id: str) -> None:
if participant_id not in self.choices:
return
if self.is_over: if self.is_over:
# TODO catch me if you can (inner) # TODO catch me if you can (inner)
raise ForbiddenOperation("Can't remove choice in a closed round.") raise ForbiddenOperation("Can't remove choice in a closed round.")
# TODO prevent if battles already assigned # TODO prevent if battles already assigned
if self._campaign and self._campaign._war: self.war.revert_choice_ties(
self._campaign._war.revert_choice_ties( self.id,
self.id, participants=[participant_id] participants=[self.campaign.campaign_to_war_part_id(participant_id)],
) )
del self.choices[participant_id] del self.choices[participant_id]
# Battle methods # Battle methods
@ -187,10 +198,16 @@ class Round:
if battle.winner_id == participant_id: if battle.winner_id == participant_id:
battle.winner_id = None battle.winner_id = None
if trigger_revert_ties: if trigger_revert_ties:
if self._campaign and self._campaign._war: self.war.revert_battle_ties(
self._campaign._war.revert_battle_ties(battle.sector_id) self.id,
participants=[
self.campaign.campaign_to_war_part_id(participant_id)
],
)
def remove_battle(self, sector_id: str) -> None: def remove_battle(self, sector_id: str) -> None:
if sector_id not in self.battles:
return
if self.is_over: if self.is_over:
# TODO catch me if you can # TODO catch me if you can
raise ForbiddenOperation("Can't remove battle in a closed round.") raise ForbiddenOperation("Can't remove battle in a closed round.")
@ -198,6 +215,5 @@ class Round:
if bat and bat.is_finished(): if bat and bat.is_finished():
# TODO catch me if you can # TODO catch me if you can
raise ForbiddenOperation("Can't remove finished battle.") raise ForbiddenOperation("Can't remove finished battle.")
if self._campaign and self._campaign._war: self.war.revert_battle_ties(self.id, sector_id=sector_id)
self._campaign._war.revert_battle_ties(sector_id)
del self.battles[sector_id] del self.battles[sector_id]

View file

@ -161,6 +161,12 @@ class War:
war.set_state(data.get("is_over", False)) war.set_state(data.get("is_over", False))
return war return war
def relink(self) -> None:
for campaign in self.campaigns:
campaign._war = self
for rnd in campaign.rounds:
rnd._campaign = campaign
# Objective methods # Objective methods
def add_objective(self, name: str, description: str | None) -> Objective: def add_objective(self, name: str, description: str | None) -> Objective:
@ -552,12 +558,60 @@ class War:
) )
return gained - spent 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] 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] 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( def revert_choice_ties(
self, self,
round_id: str, round_id: str,
@ -565,33 +619,45 @@ class War:
sector_id: str | None = None, sector_id: str | None = None,
participants: List[str] | None = None, participants: List[str] | None = None,
) -> None: ) -> None:
removed_ties: Set[str] = set() removed_ties = self.find_ties(
for ev in self.events: context_type=ContextType.CHOICE,
if ( context_id=round_id,
isinstance(ev, TieResolved) sector_id=sector_id,
and ev.context_type == ContextType.CHOICE participants=participants,
and ev.context_id == round_id )
): draws = self.get_draws(
if ( context_type=ContextType.CHOICE,
sector_id is None context_id=round_id,
or ev.sector_id == sector_id sector_id=sector_id,
or participants is None participants=participants,
or any(p in ev.participants for p in participants) )
): self.events = [
if ev.tie_id: ev
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 for ev in self.events
if isinstance(ev, TieResolved) if ev.tie_id not in removed_ties and ev not in draws
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: def revert_battle_ties(
self.events = [ev for ev in self.events if ev.tie_id != tie_id] 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": [ "events": [
{ {
"type": "InfluenceGained", "type": "InfluenceGained",
"id": "8f20b587-0ff3-42c9-9965-88112e2a4e74", "id": "096802e2-5833-4c6d-941d-c91d98957fc1",
"participant_id": "accc25f2-43a0-4d41-804f-3c8aec853c97", "participant_id": "accc25f2-43a0-4d41-804f-3c8aec853c97",
"context_type": "battle", "context_type": "battle",
"context_id": "4548997e-50e5-493a-b751-483f5bcccc00", "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 "amount": 1
}, },
{ {
"type": "TieResolved", "type": "TieResolved",
"id": "c83a9d4e-4620-47de-93c8-d29d6e119c59", "id": "ec6b8932-b24f-4042-a8f4-566a42d0857d",
"participant_id": null, "participant_id": null,
"context_type": "battle", "context_type": "battle",
"context_id": "79accf7c-2d93-4ac3-b747-e7092bfe3feb", "context_id": "79accf7c-2d93-4ac3-b747-e7092bfe3feb",
"timestamp": "2026-03-18T09:25:08.578539",
"tie_id": "07aa41e5-1696-4b9f-931b-2540e213c7ef",
"participants": [ "participants": [
"602e2eaf-297e-490b-b0e9-efec818e466a", "accc25f2-43a0-4d41-804f-3c8aec853c97",
"1f6b4e7c-b1e4-4a2e-9aea-7bb75b20b4de" "179bdab6-8630-4a92-8fb6-d2637562c66c"
], ],
"timestamp": "2026-02-26T16:11:44.346337",
"tie_id": null,
"score_value": null, "score_value": null,
"objective_id": null, "objective_id": null,
"sector_id": null "sector_id": null