Compare commits
2 commits
241e76c937
...
42ad708e77
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42ad708e77 | ||
|
|
a3b9f5a943 |
14 changed files with 340 additions and 130 deletions
|
|
@ -167,7 +167,7 @@ class CampaignController:
|
||||||
|
|
||||||
def resolve_ties(
|
def resolve_ties(
|
||||||
self, war: War, contexts: List[TieContext]
|
self, war: War, contexts: List[TieContext]
|
||||||
) -> Dict[tuple[str, str, int | None], Dict[str, bool]]:
|
) -> Dict[tuple[str, str, int | None, str | None, str | None], Dict[str, bool]]:
|
||||||
bids_map = {}
|
bids_map = {}
|
||||||
for ctx in contexts:
|
for ctx in contexts:
|
||||||
active = TieResolver.get_active_participants(war, ctx, ctx.participants)
|
active = TieResolver.get_active_participants(war, ctx, ctx.participants)
|
||||||
|
|
@ -180,7 +180,7 @@ class CampaignController:
|
||||||
parent=self.app.view,
|
parent=self.app.view,
|
||||||
players=players,
|
players=players,
|
||||||
counters=counters,
|
counters=counters,
|
||||||
context_type=ContextType.CAMPAIGN,
|
context_type=ctx.context_type,
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
context_name=None,
|
context_name=None,
|
||||||
)
|
)
|
||||||
|
|
@ -192,7 +192,7 @@ class CampaignController:
|
||||||
counters=counters,
|
counters=counters,
|
||||||
context_type=ctx.context_type,
|
context_type=ctx.context_type,
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
context_name=objective.name,
|
context_name=f"Objective tie: {objective.name}",
|
||||||
)
|
)
|
||||||
if not dialog.exec():
|
if not dialog.exec():
|
||||||
TieResolver.cancel_tie_break(war, ctx)
|
TieResolver.cancel_tie_break(war, ctx)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from warchron.controller.app_controller import AppController
|
from warchron.controller.app_controller import AppController
|
||||||
|
|
@ -25,8 +26,9 @@ class RoundClosureWorkflow(ClosureWorkflow):
|
||||||
bids_map = self.app.rounds.resolve_ties(war, ties)
|
bids_map = self.app.rounds.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[tie.key()]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie, bids)
|
tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4())
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.apply_bids(war, tie, tie_id, bids)
|
||||||
|
TieResolver.resolve_tie_state(war, tie, tie_id, bids)
|
||||||
ties = TieResolver.find_battle_ties(war, round.id)
|
ties = TieResolver.find_battle_ties(war, round.id)
|
||||||
for battle in round.battles.values():
|
for battle in round.battles.values():
|
||||||
ClosureService.apply_battle_outcomes(war, campaign, battle)
|
ClosureService.apply_battle_outcomes(war, campaign, battle)
|
||||||
|
|
@ -42,8 +44,9 @@ class CampaignClosureWorkflow(ClosureWorkflow):
|
||||||
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[tie.key()]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie, bids)
|
tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4())
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.apply_bids(war, tie, tie_id, bids)
|
||||||
|
TieResolver.resolve_tie_state(war, tie, tie_id, bids)
|
||||||
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
ties = TieResolver.find_campaign_ties(war, campaign.id)
|
||||||
for obj in war.get_objectives_used_as_maj_or_min():
|
for obj in war.get_objectives_used_as_maj_or_min():
|
||||||
objective_id = obj.id
|
objective_id = obj.id
|
||||||
|
|
@ -56,8 +59,9 @@ class CampaignClosureWorkflow(ClosureWorkflow):
|
||||||
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
bids_map = self.app.campaigns.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[tie.key()]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie, bids)
|
tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4())
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.apply_bids(war, tie, tie_id, bids)
|
||||||
|
TieResolver.resolve_tie_state(war, tie, tie_id, bids)
|
||||||
ties = TieResolver.find_campaign_objective_ties(
|
ties = TieResolver.find_campaign_objective_ties(
|
||||||
war,
|
war,
|
||||||
campaign.id,
|
campaign.id,
|
||||||
|
|
@ -75,8 +79,9 @@ class WarClosureWorkflow(ClosureWorkflow):
|
||||||
bids_map = self.app.wars.resolve_ties(war, ties)
|
bids_map = self.app.wars.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[tie.key()]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie, bids)
|
tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4())
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.apply_bids(war, tie, tie_id, bids)
|
||||||
|
TieResolver.resolve_tie_state(war, tie, tie_id, bids)
|
||||||
ties = TieResolver.find_war_ties(war)
|
ties = TieResolver.find_war_ties(war)
|
||||||
for obj in war.get_objectives_used_as_maj_or_min():
|
for obj in war.get_objectives_used_as_maj_or_min():
|
||||||
objective_id = obj.id
|
objective_id = obj.id
|
||||||
|
|
@ -88,8 +93,9 @@ class WarClosureWorkflow(ClosureWorkflow):
|
||||||
bids_map = self.app.wars.resolve_ties(war, ties)
|
bids_map = self.app.wars.resolve_ties(war, ties)
|
||||||
for tie in ties:
|
for tie in ties:
|
||||||
bids = bids_map[tie.key()]
|
bids = bids_map[tie.key()]
|
||||||
TieResolver.apply_bids(war, tie, bids)
|
tie_id = TieResolver.find_active_tie_id(war, tie) or str(uuid4())
|
||||||
TieResolver.resolve_tie_state(war, tie, bids)
|
TieResolver.apply_bids(war, tie, tie_id, bids)
|
||||||
|
TieResolver.resolve_tie_state(war, tie, tie_id, bids)
|
||||||
ties = TieResolver.find_war_objective_ties(
|
ties = TieResolver.find_war_objective_ties(
|
||||||
war,
|
war,
|
||||||
objective_id,
|
objective_id,
|
||||||
|
|
@ -103,7 +109,7 @@ class RoundPairingWorkflow:
|
||||||
self.app = controller
|
self.app = controller
|
||||||
|
|
||||||
def start(self, war: War, round: Round) -> None:
|
def start(self, war: War, round: Round) -> None:
|
||||||
Pairing.check_round_pairable(round)
|
Pairing.check_round_pairable(war, round)
|
||||||
Pairing.assign_battles_to_participants(
|
Pairing.assign_battles_to_participants(
|
||||||
war,
|
war,
|
||||||
round,
|
round,
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ class RoundController:
|
||||||
comment=choice.comment,
|
comment=choice.comment,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# TODO display allocated sectors and used token
|
||||||
self.app.view.display_round_choices(choices_for_display)
|
self.app.view.display_round_choices(choices_for_display)
|
||||||
battles_for_display: List[BattleDTO] = []
|
battles_for_display: List[BattleDTO] = []
|
||||||
for sect in sectors:
|
for sect in sectors:
|
||||||
|
|
@ -89,6 +90,7 @@ class RoundController:
|
||||||
player_1_name = self.app.model.get_participant_name(
|
player_1_name = self.app.model.get_participant_name(
|
||||||
camp_part.war_participant_id
|
camp_part.war_participant_id
|
||||||
)
|
)
|
||||||
|
p1_id = battle.player_1_id
|
||||||
else:
|
else:
|
||||||
player_1_name = ""
|
player_1_name = ""
|
||||||
if battle.player_2_id:
|
if battle.player_2_id:
|
||||||
|
|
@ -96,6 +98,7 @@ class RoundController:
|
||||||
player_2_name = self.app.model.get_participant_name(
|
player_2_name = self.app.model.get_participant_name(
|
||||||
camp_part.war_participant_id
|
camp_part.war_participant_id
|
||||||
)
|
)
|
||||||
|
p2_id = battle.player_2_id
|
||||||
else:
|
else:
|
||||||
player_2_name = ""
|
player_2_name = ""
|
||||||
if battle.winner_id:
|
if battle.winner_id:
|
||||||
|
|
@ -112,7 +115,11 @@ class RoundController:
|
||||||
if battle.is_draw():
|
if battle.is_draw():
|
||||||
p1_icon = Icons.get(IconName.DRAW)
|
p1_icon = Icons.get(IconName.DRAW)
|
||||||
p2_icon = Icons.get(IconName.DRAW)
|
p2_icon = Icons.get(IconName.DRAW)
|
||||||
context = TieContext(ContextType.BATTLE, battle.sector_id)
|
context = TieContext(
|
||||||
|
ContextType.BATTLE,
|
||||||
|
battle.sector_id,
|
||||||
|
[p1_id, p2_id],
|
||||||
|
)
|
||||||
if TieResolver.was_tie_broken_by_tokens(war, context):
|
if TieResolver.was_tie_broken_by_tokens(war, context):
|
||||||
effective_winner = ResultChecker.get_effective_winner_id(
|
effective_winner = ResultChecker.get_effective_winner_id(
|
||||||
war, ContextType.BATTLE, battle.sector_id, None
|
war, ContextType.BATTLE, battle.sector_id, None
|
||||||
|
|
@ -197,7 +204,7 @@ class RoundController:
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
for bat in rnd.battles.values():
|
for bat in rnd.battles.values():
|
||||||
bat.cleanup_battle_players()
|
bat.clear_battle_players()
|
||||||
return
|
return
|
||||||
except DomainError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
|
|
@ -225,7 +232,7 @@ class RoundController:
|
||||||
|
|
||||||
def resolve_ties(
|
def resolve_ties(
|
||||||
self, war: War, contexts: List[TieContext]
|
self, war: War, contexts: List[TieContext]
|
||||||
) -> Dict[tuple[str, str, int | None], Dict[str, bool]]:
|
) -> Dict[tuple[str, str, int | None, str | None, str | None], Dict[str, bool]]:
|
||||||
bids_map = {}
|
bids_map = {}
|
||||||
for ctx in contexts:
|
for ctx in contexts:
|
||||||
players = [
|
players = [
|
||||||
|
|
@ -236,11 +243,12 @@ class RoundController:
|
||||||
for pid in ctx.participants
|
for pid in ctx.participants
|
||||||
]
|
]
|
||||||
counters = [war.get_influence_tokens(pid) for pid in ctx.participants]
|
counters = [war.get_influence_tokens(pid) for pid in ctx.participants]
|
||||||
|
# TODO display sector name for BATTLE or CHOICE
|
||||||
dialog = TieDialog(
|
dialog = TieDialog(
|
||||||
parent=self.app.view,
|
parent=self.app.view,
|
||||||
players=players,
|
players=players,
|
||||||
counters=counters,
|
counters=counters,
|
||||||
context_type=ContextType.BATTLE,
|
context_type=ctx.context_type,
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
)
|
)
|
||||||
if not dialog.exec():
|
if not dialog.exec():
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class WarController:
|
||||||
]
|
]
|
||||||
scores = ScoreService.compute_scores(war, ContextType.WAR, war.id)
|
scores = ScoreService.compute_scores(war, ContextType.WAR, war.id)
|
||||||
rows: List[WarParticipantScoreDTO] = []
|
rows: List[WarParticipantScoreDTO] = []
|
||||||
vp_icon_map: dict[str, QIcon] = {}
|
vp_icon_map: Dict[str, QIcon] = {}
|
||||||
objective_icon_maps: Dict[str, Dict[str, QIcon]] = {}
|
objective_icon_maps: Dict[str, Dict[str, QIcon]] = {}
|
||||||
if war.is_over:
|
if war.is_over:
|
||||||
vp_icon_map = RankingIcon.compute_icons(
|
vp_icon_map = RankingIcon.compute_icons(
|
||||||
|
|
@ -157,7 +157,7 @@ class WarController:
|
||||||
|
|
||||||
def resolve_ties(
|
def resolve_ties(
|
||||||
self, war: War, contexts: List[TieContext]
|
self, war: War, contexts: List[TieContext]
|
||||||
) -> Dict[tuple[str, str, int | None], Dict[str, bool]]:
|
) -> Dict[tuple[str, str, int | None, str | None, str | None], Dict[str, bool]]:
|
||||||
bids_map = {}
|
bids_map = {}
|
||||||
for ctx in contexts:
|
for ctx in contexts:
|
||||||
active = TieResolver.get_active_participants(
|
active = TieResolver.get_active_participants(
|
||||||
|
|
@ -174,7 +174,7 @@ class WarController:
|
||||||
parent=self.app.view,
|
parent=self.app.view,
|
||||||
players=players,
|
players=players,
|
||||||
counters=counters,
|
counters=counters,
|
||||||
context_type=ContextType.WAR,
|
context_type=ctx.context_type,
|
||||||
context_id=ctx.context_id,
|
context_id=ctx.context_id,
|
||||||
context_name=None,
|
context_name=None,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class Battle:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_available_places(self) -> List[str]:
|
def get_available_places(self) -> List[str]:
|
||||||
places: list[str] = []
|
places: List[str] = []
|
||||||
if self.player_1_id is None:
|
if self.player_1_id is None:
|
||||||
places.append("player_1")
|
places.append("player_1")
|
||||||
if self.player_2_id is None:
|
if self.player_2_id is None:
|
||||||
|
|
@ -73,7 +73,7 @@ class Battle:
|
||||||
return
|
return
|
||||||
raise DomainError("Battle has no available places")
|
raise DomainError("Battle has no available places")
|
||||||
|
|
||||||
def cleanup_battle_players(self) -> None:
|
def clear_battle_players(self) -> None:
|
||||||
self.player_1_id = None
|
self.player_1_id = None
|
||||||
self.player_2_id = None
|
self.player_2_id = None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from typing import Any, Dict, List, Set
|
from typing import Any, Dict, List, Set, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from warchron.model.war import War
|
||||||
from warchron.model.exception import ForbiddenOperation, RequiresConfirmation
|
from warchron.model.exception import ForbiddenOperation, RequiresConfirmation
|
||||||
from warchron.model.campaign_participant import CampaignParticipant
|
from warchron.model.campaign_participant import CampaignParticipant
|
||||||
from warchron.model.sector import Sector
|
from warchron.model.sector import Sector
|
||||||
|
|
@ -19,6 +21,7 @@ class Campaign:
|
||||||
self.sectors: Dict[str, Sector] = {}
|
self.sectors: Dict[str, Sector] = {}
|
||||||
self.rounds: List[Round] = []
|
self.rounds: List[Round] = []
|
||||||
self.is_over = False
|
self.is_over = False
|
||||||
|
self._war: War | None = None # private link
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_id(self, new_id: str) -> None:
|
||||||
self.id = new_id
|
self.id = new_id
|
||||||
|
|
@ -60,8 +63,8 @@ class Campaign:
|
||||||
|
|
||||||
# Campaign participant methods
|
# Campaign participant methods
|
||||||
|
|
||||||
def get_all_campaign_participants_ids(self) -> set[str]:
|
def get_all_campaign_participants_ids(self) -> List[str]:
|
||||||
return set(self.participants.keys())
|
return list(self.participants.keys())
|
||||||
|
|
||||||
def has_participant(self, participant_id: str) -> bool:
|
def has_participant(self, participant_id: str) -> bool:
|
||||||
return participant_id in self.participants
|
return participant_id in self.participants
|
||||||
|
|
@ -351,6 +354,7 @@ class Campaign:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
raise ForbiddenOperation("Can't add round in a closed campaign.")
|
raise ForbiddenOperation("Can't add round in a closed campaign.")
|
||||||
round = Round()
|
round = Round()
|
||||||
|
round._campaign = self
|
||||||
self.rounds.append(round)
|
self.rounds.append(round)
|
||||||
return round
|
return round
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -360,8 +360,8 @@ class Model:
|
||||||
)
|
)
|
||||||
|
|
||||||
def remove_sector(self, sector_id: str) -> None:
|
def remove_sector(self, sector_id: str) -> None:
|
||||||
camp = self.get_campaign_by_sector(sector_id)
|
war = self.get_war_by_sector(sector_id)
|
||||||
camp.remove_sector(sector_id)
|
war.remove_sector(sector_id)
|
||||||
|
|
||||||
# Campaign participant methods
|
# Campaign participant methods
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Dict, List, Callable, Tuple
|
from typing import Dict, List, Callable, Tuple
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from warchron.constants import ContextType, ScoreKind
|
from warchron.constants import ContextType, ScoreKind
|
||||||
|
|
@ -18,7 +20,7 @@ from warchron.model.score_service import ParticipantScore
|
||||||
|
|
||||||
ResolveTiesCallback = Callable[
|
ResolveTiesCallback = Callable[
|
||||||
["War", List["TieContext"]],
|
["War", List["TieContext"]],
|
||||||
Dict[Tuple[str, str, int | None], Dict[str, bool]],
|
Dict[Tuple[str, str, int | None, str | None, str | None], Dict[str, bool]],
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -26,6 +28,7 @@ class Pairing:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_round_pairable(
|
def check_round_pairable(
|
||||||
|
war: War,
|
||||||
round: Round,
|
round: Round,
|
||||||
) -> None:
|
) -> None:
|
||||||
if round.is_over:
|
if round.is_over:
|
||||||
|
|
@ -48,8 +51,9 @@ class Pairing:
|
||||||
|
|
||||||
def cleanup() -> None:
|
def cleanup() -> None:
|
||||||
for bat in round.battles.values():
|
for bat in round.battles.values():
|
||||||
bat.cleanup_battle_players()
|
bat.clear_battle_players()
|
||||||
# FIXME cancel TieResolved + TokenSpent
|
bat.set_winner(None)
|
||||||
|
war.revert_choice_ties(round.id)
|
||||||
|
|
||||||
if any(
|
if any(
|
||||||
bat.player_1_id is not None or bat.player_2_id is not None
|
bat.player_1_id is not None or bat.player_2_id is not None
|
||||||
|
|
@ -58,6 +62,7 @@ class Pairing:
|
||||||
raise RequiresConfirmation(
|
raise RequiresConfirmation(
|
||||||
"Battle(s) already have player(s) assigned for this round.\n"
|
"Battle(s) already have player(s) assigned for this round.\n"
|
||||||
"Battle players will be cleared.\n"
|
"Battle players will be cleared.\n"
|
||||||
|
"Choice tokens and tie-breaks will be deleted.\n"
|
||||||
"Do you want to continue?",
|
"Do you want to continue?",
|
||||||
action=cleanup,
|
action=cleanup,
|
||||||
)
|
)
|
||||||
|
|
@ -90,20 +95,20 @@ class Pairing:
|
||||||
campaign.war_to_campaign_part_id(pid) for pid in group
|
campaign.war_to_campaign_part_id(pid) for pid in group
|
||||||
]
|
]
|
||||||
Pairing._run_phase(
|
Pairing._run_phase(
|
||||||
war,
|
war=war,
|
||||||
round,
|
round=round,
|
||||||
remaining,
|
remaining=remaining,
|
||||||
sector_to_battle,
|
sector_to_battle=sector_to_battle,
|
||||||
resolve_ties_callback,
|
resolve_ties_callback=resolve_ties_callback,
|
||||||
use_priority=True,
|
use_priority=True,
|
||||||
score_value=score_value,
|
score_value=score_value,
|
||||||
)
|
)
|
||||||
Pairing._run_phase(
|
Pairing._run_phase(
|
||||||
war,
|
war=war,
|
||||||
round,
|
round=round,
|
||||||
remaining,
|
remaining=remaining,
|
||||||
sector_to_battle,
|
sector_to_battle=sector_to_battle,
|
||||||
resolve_ties_callback,
|
resolve_ties_callback=resolve_ties_callback,
|
||||||
use_priority=False,
|
use_priority=False,
|
||||||
score_value=score_value,
|
score_value=score_value,
|
||||||
)
|
)
|
||||||
|
|
@ -133,13 +138,13 @@ class Pairing:
|
||||||
if places <= 0:
|
if places <= 0:
|
||||||
continue
|
continue
|
||||||
winners = Pairing._resolve_sector_allocation(
|
winners = Pairing._resolve_sector_allocation(
|
||||||
war,
|
war=war,
|
||||||
round,
|
round=round,
|
||||||
sector_id,
|
sector_id=sector_id,
|
||||||
participants,
|
participants=participants,
|
||||||
places,
|
places=places,
|
||||||
resolve_ties_callback,
|
resolve_ties_callback=resolve_ties_callback,
|
||||||
score_value,
|
score_value=score_value,
|
||||||
)
|
)
|
||||||
for pid in winners:
|
for pid in winners:
|
||||||
battle.assign_participant(pid)
|
battle.assign_participant(pid)
|
||||||
|
|
@ -187,26 +192,59 @@ class Pairing:
|
||||||
participants=[
|
participants=[
|
||||||
campaign.campaign_to_war_part_id(pid) for pid in participants
|
campaign.campaign_to_war_part_id(pid) for pid in participants
|
||||||
],
|
],
|
||||||
|
score_value=score_value,
|
||||||
score_kind=ScoreKind.VP,
|
score_kind=ScoreKind.VP,
|
||||||
sector_id=sector_id,
|
sector_id=sector_id,
|
||||||
)
|
)
|
||||||
# ---- resolve tie loop ----
|
# ---- resolve tie loop ----
|
||||||
|
tie_id = TieResolver.find_active_tie_id(war, context) or str(uuid4())
|
||||||
while not TieResolver.is_tie_resolved(war, context):
|
while not TieResolver.is_tie_resolved(war, context):
|
||||||
if not TieResolver.can_tie_be_resolved(war, context, context.participants):
|
active = TieResolver.get_active_participants(
|
||||||
|
war, context, context.participants
|
||||||
|
)
|
||||||
|
if len(active) <= 1:
|
||||||
|
break
|
||||||
|
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,
|
||||||
|
sector_id=context.sector_id,
|
||||||
|
)
|
||||||
|
if not TieResolver.can_tie_be_resolved(
|
||||||
|
war, context, current_context.participants
|
||||||
|
):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(
|
TieResolved(
|
||||||
None,
|
None,
|
||||||
context.context_type,
|
context.context_type,
|
||||||
context.context_id,
|
context.context_id,
|
||||||
|
participants,
|
||||||
|
tie_id=tie_id,
|
||||||
score_value=score_value,
|
score_value=score_value,
|
||||||
sector_id=sector_id,
|
sector_id=sector_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
bids_map = resolve_ties_callback(war, [context])
|
bids_map = resolve_ties_callback(war, [current_context])
|
||||||
bids = bids_map[context.key()]
|
bids = bids_map[current_context.key()]
|
||||||
TieResolver.apply_bids(war, context, bids)
|
# confirmed draw if current bids are 0
|
||||||
TieResolver.resolve_tie_state(war, context, bids)
|
if bids is not None and not any(bids.values()):
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
break
|
||||||
|
TieResolver.apply_bids(war, context, tie_id, bids)
|
||||||
|
TieResolver.resolve_tie_state(war, context, tie_id, bids)
|
||||||
ranked_groups = TieResolver.rank_by_tokens(
|
ranked_groups = TieResolver.rank_by_tokens(
|
||||||
war,
|
war,
|
||||||
context,
|
context,
|
||||||
|
|
@ -215,6 +253,8 @@ class Pairing:
|
||||||
ordered: List[str] = []
|
ordered: List[str] = []
|
||||||
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 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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from warchron.model.campaign import Campaign
|
||||||
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
|
||||||
|
|
@ -13,6 +15,7 @@ class Round:
|
||||||
self.choices: Dict[str, Choice] = {}
|
self.choices: Dict[str, Choice] = {}
|
||||||
self.battles: Dict[str, Battle] = {}
|
self.battles: Dict[str, Battle] = {}
|
||||||
self.is_over: bool = False
|
self.is_over: bool = False
|
||||||
|
self._campaign: Campaign | None = None # private link
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_id(self, new_id: str) -> None:
|
||||||
self.id = new_id
|
self.id = new_id
|
||||||
|
|
@ -60,6 +63,7 @@ class Round:
|
||||||
|
|
||||||
def create_choice(self, participant_id: str) -> Choice:
|
def create_choice(self, participant_id: str) -> Choice:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
|
# TODO catch me if you can
|
||||||
raise ForbiddenOperation("Can't create choice in a closed round.")
|
raise ForbiddenOperation("Can't create choice in a closed round.")
|
||||||
if participant_id not in self.choices:
|
if participant_id not in self.choices:
|
||||||
choice = Choice(
|
choice = Choice(
|
||||||
|
|
@ -78,23 +82,38 @@ class Round:
|
||||||
comment: str | None,
|
comment: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
|
# TODO catch me if you can
|
||||||
raise ForbiddenOperation("Can't update choice in a closed round.")
|
raise ForbiddenOperation("Can't update choice in a closed round.")
|
||||||
|
# TODO prevent if battles already assigned
|
||||||
choice = self.get_choice(participant_id)
|
choice = self.get_choice(participant_id)
|
||||||
if choice:
|
if choice:
|
||||||
choice.set_priority(priority_sector_id)
|
choice.set_priority(priority_sector_id)
|
||||||
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
|
||||||
if choice.priority_sector_id == sector_id:
|
if choice.priority_sector_id == sector_id:
|
||||||
choice.priority_sector_id = None
|
choice.priority_sector_id = None
|
||||||
|
trigger_revert_ties = True
|
||||||
if choice.secondary_sector_id == sector_id:
|
if choice.secondary_sector_id == sector_id:
|
||||||
choice.secondary_sector_id = None
|
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)
|
||||||
|
|
||||||
def remove_choice(self, participant_id: str) -> None:
|
def remove_choice(self, participant_id: str) -> None:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
|
# 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
|
||||||
|
if self._campaign and self._campaign._war:
|
||||||
|
self._campaign._war.revert_choice_ties(
|
||||||
|
self.id, participants=[participant_id]
|
||||||
|
)
|
||||||
del self.choices[participant_id]
|
del self.choices[participant_id]
|
||||||
|
|
||||||
# Battle methods
|
# Battle methods
|
||||||
|
|
@ -126,6 +145,7 @@ class Round:
|
||||||
|
|
||||||
def create_battle(self, sector_id: str) -> Battle:
|
def create_battle(self, sector_id: str) -> Battle:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
|
# TODO catch me if you can
|
||||||
raise ForbiddenOperation("Can't create battle in a closed round.")
|
raise ForbiddenOperation("Can't create battle in a closed round.")
|
||||||
if sector_id not in self.battles:
|
if sector_id not in self.battles:
|
||||||
battle = Battle(sector_id=sector_id, player_1_id=None, player_2_id=None)
|
battle = Battle(sector_id=sector_id, player_1_id=None, player_2_id=None)
|
||||||
|
|
@ -143,8 +163,10 @@ class Round:
|
||||||
comment: str | None,
|
comment: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
|
# TODO catch me if you can
|
||||||
raise ForbiddenOperation("Can't update battle in a closed round.")
|
raise ForbiddenOperation("Can't update battle in a closed round.")
|
||||||
bat = self.get_battle(sector_id)
|
bat = self.get_battle(sector_id)
|
||||||
|
# TODO require confirmation if there was choice tie to clear it
|
||||||
if bat:
|
if bat:
|
||||||
bat.set_player_1(player_1_id)
|
bat.set_player_1(player_1_id)
|
||||||
bat.set_player_2(player_2_id)
|
bat.set_player_2(player_2_id)
|
||||||
|
|
@ -155,17 +177,27 @@ class Round:
|
||||||
|
|
||||||
def clear_participant_references(self, participant_id: str) -> None:
|
def clear_participant_references(self, participant_id: str) -> None:
|
||||||
for battle in self.battles.values():
|
for battle in self.battles.values():
|
||||||
|
trigger_revert_ties = False
|
||||||
if battle.player_1_id == participant_id:
|
if battle.player_1_id == participant_id:
|
||||||
battle.player_1_id = None
|
battle.player_1_id = None
|
||||||
|
trigger_revert_ties = True
|
||||||
if battle.player_2_id == participant_id:
|
if battle.player_2_id == participant_id:
|
||||||
battle.player_2_id = None
|
battle.player_2_id = None
|
||||||
|
trigger_revert_ties = True
|
||||||
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 self._campaign and self._campaign._war:
|
||||||
|
self._campaign._war.revert_battle_ties(battle.sector_id)
|
||||||
|
|
||||||
def remove_battle(self, sector_id: str) -> None:
|
def remove_battle(self, sector_id: str) -> None:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
|
# 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.")
|
||||||
bat = self.battles[sector_id]
|
bat = self.battles[sector_id]
|
||||||
if bat and bat.is_finished():
|
if bat and bat.is_finished():
|
||||||
|
# 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._campaign._war.revert_battle_ties(sector_id)
|
||||||
del self.battles[sector_id]
|
del self.battles[sector_id]
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,17 @@ class ScoreService:
|
||||||
) -> Dict[str, ParticipantScore]:
|
) -> Dict[str, ParticipantScore]:
|
||||||
from warchron.model.result_checker import ResultChecker
|
from warchron.model.result_checker import ResultChecker
|
||||||
|
|
||||||
|
if context_type == ContextType.CAMPAIGN:
|
||||||
|
camp = war.get_campaign(context_id)
|
||||||
|
camp_pids = camp.get_all_campaign_participants_ids()
|
||||||
|
participant_ids = [(camp.campaign_to_war_part_id(pid)) for pid in camp_pids]
|
||||||
|
elif context_type == ContextType.WAR:
|
||||||
|
participant_ids = war.get_all_war_participants_ids()
|
||||||
scores = {
|
scores = {
|
||||||
pid: ParticipantScore(
|
pid: ParticipantScore(
|
||||||
narrative_points={obj_id: 0 for obj_id in war.objectives}
|
narrative_points={obj_id: 0 for obj_id in war.objectives}
|
||||||
)
|
)
|
||||||
for pid in war.participants
|
for pid in participant_ids
|
||||||
}
|
}
|
||||||
battles = ScoreService._get_battles_for_context(war, context_type, context_id)
|
battles = ScoreService._get_battles_for_context(war, context_type, context_id)
|
||||||
for battle in battles:
|
for battle in battles:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from typing import List, Dict, DefaultDict
|
from typing import List, Dict, DefaultDict, Tuple
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
from warchron.constants import ContextType, ScoreKind
|
from warchron.constants import ContextType, ScoreKind
|
||||||
|
|
@ -13,41 +13,70 @@ from warchron.model.score_service import ScoreService, ParticipantScore
|
||||||
class TieContext:
|
class TieContext:
|
||||||
context_type: ContextType
|
context_type: ContextType
|
||||||
context_id: str
|
context_id: str
|
||||||
participants: List[str] = field(default_factory=list) # war_participant_ids
|
participants: List[str]
|
||||||
score_value: int | None = None
|
score_value: int | None = None
|
||||||
score_kind: ScoreKind | None = None
|
score_kind: ScoreKind | None = None
|
||||||
objective_id: str | None = None
|
objective_id: str | None = None
|
||||||
sector_id: str | None = None
|
sector_id: str | None = None
|
||||||
|
|
||||||
def key(self) -> tuple[str, str, int | None]:
|
def key(self) -> Tuple[str, str, int | None, str | None, str | None]:
|
||||||
return (self.context_type, self.context_id, self.score_value)
|
return (
|
||||||
|
self.context_type,
|
||||||
|
self.context_id,
|
||||||
|
self.score_value,
|
||||||
|
self.objective_id,
|
||||||
|
self.sector_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TieResolver:
|
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
|
@staticmethod
|
||||||
def find_battle_ties(war: War, round_id: str) -> List[TieContext]:
|
def find_battle_ties(war: War, round_id: str) -> List[TieContext]:
|
||||||
round = war.get_round(round_id)
|
round = war.get_round(round_id)
|
||||||
campaign = war.get_campaign_by_round(round_id)
|
campaign = war.get_campaign_by_round(round_id)
|
||||||
ties = []
|
ties = []
|
||||||
for battle in round.battles.values():
|
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:
|
if campaign is None:
|
||||||
raise DomainError("No campaign for this battle tie")
|
raise DomainError("No campaign for this battle tie")
|
||||||
if battle.player_1_id is None or battle.player_2_id is None:
|
if battle.player_1_id is None or battle.player_2_id is None:
|
||||||
raise DomainError("Missing player(s) in this battle context.")
|
raise DomainError("Missing player(s) in this battle context.")
|
||||||
p1_id = campaign.campaign_to_war_part_id(battle.player_1_id)
|
p1_id = campaign.campaign_to_war_part_id(battle.player_1_id)
|
||||||
p2_id = campaign.campaign_to_war_part_id(battle.player_2_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]):
|
if not TieResolver.can_tie_be_resolved(war, context, [p1_id, p2_id]):
|
||||||
war.events.append(
|
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
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
|
|
@ -80,13 +109,16 @@ class TieResolver:
|
||||||
)
|
)
|
||||||
if TieResolver.is_tie_resolved(war, context):
|
if TieResolver.is_tie_resolved(war, context):
|
||||||
continue
|
continue
|
||||||
|
tie_id = TieResolver.find_active_tie_id(war, context)
|
||||||
if not TieResolver.can_tie_be_resolved(war, context, participants):
|
if not TieResolver.can_tie_be_resolved(war, context, participants):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(
|
TieResolved(
|
||||||
None,
|
participant_id=None,
|
||||||
ContextType.CAMPAIGN,
|
context_type=ContextType.CAMPAIGN,
|
||||||
campaign_id,
|
context_id=campaign_id,
|
||||||
score_value,
|
participants=participants,
|
||||||
|
tie_id=tie_id,
|
||||||
|
score_value=score_value,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
@ -103,9 +135,7 @@ class TieResolver:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_campaign_objective_ties(
|
def find_campaign_objective_ties(
|
||||||
war: War,
|
war: War, campaign_id: str, objective_id: str
|
||||||
campaign_id: str,
|
|
||||||
objective_id: str,
|
|
||||||
) -> List[TieContext]:
|
) -> List[TieContext]:
|
||||||
scores = ScoreService.compute_scores(
|
scores = ScoreService.compute_scores(
|
||||||
war,
|
war,
|
||||||
|
|
@ -131,6 +161,7 @@ class TieResolver:
|
||||||
)
|
)
|
||||||
if TieResolver.is_tie_resolved(war, context):
|
if TieResolver.is_tie_resolved(war, context):
|
||||||
continue
|
continue
|
||||||
|
tie_id = TieResolver.find_active_tie_id(war, context)
|
||||||
if not TieResolver.can_tie_be_resolved(
|
if not TieResolver.can_tie_be_resolved(
|
||||||
war,
|
war,
|
||||||
context,
|
context,
|
||||||
|
|
@ -138,7 +169,13 @@ class TieResolver:
|
||||||
):
|
):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
TieResolved(
|
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
|
continue
|
||||||
|
|
@ -181,9 +218,17 @@ class TieResolver:
|
||||||
)
|
)
|
||||||
if TieResolver.is_tie_resolved(war, context):
|
if TieResolver.is_tie_resolved(war, context):
|
||||||
continue
|
continue
|
||||||
|
tie_id = TieResolver.find_active_tie_id(war, context)
|
||||||
if not TieResolver.can_tie_be_resolved(war, context, group):
|
if not TieResolver.can_tie_be_resolved(war, context, group):
|
||||||
war.events.append(
|
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
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
|
|
@ -198,10 +243,7 @@ class TieResolver:
|
||||||
return ties
|
return ties
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def find_war_objective_ties(
|
def find_war_objective_ties(war: War, objective_id: str) -> List[TieContext]:
|
||||||
war: War,
|
|
||||||
objective_id: str,
|
|
||||||
) -> List[TieContext]:
|
|
||||||
from warchron.model.result_checker import ResultChecker
|
from warchron.model.result_checker import ResultChecker
|
||||||
|
|
||||||
scores = ScoreService.compute_scores(
|
scores = ScoreService.compute_scores(
|
||||||
|
|
@ -235,13 +277,22 @@ class TieResolver:
|
||||||
context,
|
context,
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
|
tie_id = TieResolver.find_active_tie_id(war, context)
|
||||||
if not TieResolver.can_tie_be_resolved(
|
if not TieResolver.can_tie_be_resolved(
|
||||||
war,
|
war,
|
||||||
context,
|
context,
|
||||||
group,
|
group,
|
||||||
):
|
):
|
||||||
war.events.append(
|
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
|
continue
|
||||||
ties.append(
|
ties.append(
|
||||||
|
|
@ -260,6 +311,7 @@ class TieResolver:
|
||||||
def apply_bids(
|
def apply_bids(
|
||||||
war: War,
|
war: War,
|
||||||
context: TieContext,
|
context: TieContext,
|
||||||
|
tie_id: str,
|
||||||
bids: Dict[str, bool], # war_participant_id -> spend?
|
bids: Dict[str, bool], # war_participant_id -> spend?
|
||||||
) -> None:
|
) -> None:
|
||||||
for war_part_id, spend in bids.items():
|
for war_part_id, spend in bids.items():
|
||||||
|
|
@ -273,6 +325,7 @@ class TieResolver:
|
||||||
amount=1,
|
amount=1,
|
||||||
context_type=context.context_type,
|
context_type=context.context_type,
|
||||||
context_id=context.context_id,
|
context_id=context.context_id,
|
||||||
|
tie_id=tie_id,
|
||||||
objective_id=context.objective_id,
|
objective_id=context.objective_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
@ -287,12 +340,7 @@ class TieResolver:
|
||||||
for ev in war.events
|
for ev in war.events
|
||||||
if not (
|
if not (
|
||||||
(
|
(
|
||||||
isinstance(ev, InfluenceSpent)
|
(isinstance(ev, InfluenceSpent) or isinstance(ev, TieResolved))
|
||||||
and ev.context_type == context.context_type
|
|
||||||
and ev.context_id == context.context_id
|
|
||||||
)
|
|
||||||
or (
|
|
||||||
isinstance(ev, TieResolved)
|
|
||||||
and ev.context_type == context.context_type
|
and ev.context_type == context.context_type
|
||||||
and ev.context_id == context.context_id
|
and ev.context_id == context.context_id
|
||||||
)
|
)
|
||||||
|
|
@ -356,25 +404,9 @@ class TieResolver:
|
||||||
def resolve_tie_state(
|
def resolve_tie_state(
|
||||||
war: War,
|
war: War,
|
||||||
context: TieContext,
|
context: TieContext,
|
||||||
bids: dict[str, bool] | None = None,
|
tie_id: str,
|
||||||
|
bids: Dict[str, bool] | None = 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
|
# confirmed draw if current bids are 0
|
||||||
if bids is not None and not any(bids.values()):
|
if bids is not None and not any(bids.values()):
|
||||||
war.events.append(
|
war.events.append(
|
||||||
|
|
@ -382,12 +414,13 @@ class TieResolver:
|
||||||
None,
|
None,
|
||||||
context.context_type,
|
context.context_type,
|
||||||
context.context_id,
|
context.context_id,
|
||||||
|
participants=context.participants,
|
||||||
|
tie_id=tie_id,
|
||||||
score_value=context.score_value,
|
score_value=context.score_value,
|
||||||
objective_id=context.objective_id,
|
objective_id=context.objective_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
# else rank_by_tokens
|
|
||||||
groups = TieResolver.rank_by_tokens(war, context, context.participants)
|
groups = TieResolver.rank_by_tokens(war, context, context.participants)
|
||||||
if len(groups[0]) == 1:
|
if len(groups[0]) == 1:
|
||||||
war.events.append(
|
war.events.append(
|
||||||
|
|
@ -395,8 +428,10 @@ class TieResolver:
|
||||||
groups[0][0],
|
groups[0][0],
|
||||||
context.context_type,
|
context.context_type,
|
||||||
context.context_id,
|
context.context_id,
|
||||||
context.score_value,
|
participants=context.participants,
|
||||||
context.objective_id,
|
tie_id=tie_id,
|
||||||
|
score_value=context.score_value,
|
||||||
|
objective_id=context.objective_id,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -427,11 +462,24 @@ class TieResolver:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_tie_resolved(war: War, context: TieContext) -> bool:
|
def is_tie_resolved(war: War, context: TieContext) -> bool:
|
||||||
return any(
|
for ev in war.events:
|
||||||
isinstance(ev, TieResolved)
|
if not isinstance(ev, TieResolved):
|
||||||
and ev.context_type == context.context_type
|
continue
|
||||||
and ev.context_id == context.context_id
|
if ev.context_type != context.context_type:
|
||||||
and ev.score_value == context.score_value
|
continue
|
||||||
and ev.objective_id == context.objective_id
|
if ev.context_id != context.context_id:
|
||||||
for ev in war.events
|
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
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List, Set
|
||||||
|
|
||||||
from warchron.constants import ContextType
|
from warchron.constants import ContextType
|
||||||
from warchron.model.war_event import (
|
from warchron.model.war_event import (
|
||||||
|
|
@ -62,7 +62,7 @@ class War:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
raise ForbiddenOperation("Can't set influence token of a closed war.")
|
raise ForbiddenOperation("Can't set influence token of a closed war.")
|
||||||
|
|
||||||
def cleanup_token_and_tie() -> None:
|
def remove_token_and_draw_tie() -> None:
|
||||||
new_events: List[WarEvent] = []
|
new_events: List[WarEvent] = []
|
||||||
for ev in self.events:
|
for ev in self.events:
|
||||||
if isinstance(ev, (InfluenceSpent, InfluenceGained)):
|
if isinstance(ev, (InfluenceSpent, InfluenceGained)):
|
||||||
|
|
@ -73,11 +73,10 @@ class War:
|
||||||
self.events = new_events
|
self.events = new_events
|
||||||
self.influence_token = new_state
|
self.influence_token = new_state
|
||||||
|
|
||||||
def reset_tie_break() -> None:
|
def remove_tiebreak_and_not_over() -> None:
|
||||||
new_events: List[WarEvent] = []
|
new_events: List[WarEvent] = []
|
||||||
for ev in self.events:
|
for ev in self.events:
|
||||||
if isinstance(ev, (TieResolved)):
|
if isinstance(ev, (TieResolved)):
|
||||||
# FIXME cancel TieResolved + TokenSpent
|
|
||||||
if ev.context_type == ContextType.BATTLE:
|
if ev.context_type == ContextType.BATTLE:
|
||||||
battle = self.get_battle(ev.context_id)
|
battle = self.get_battle(ev.context_id)
|
||||||
campaign = self.get_campaign_by_sector(battle.sector_id)
|
campaign = self.get_campaign_by_sector(battle.sector_id)
|
||||||
|
|
@ -86,6 +85,7 @@ class War:
|
||||||
elif ev.context_type == ContextType.CAMPAIGN:
|
elif ev.context_type == ContextType.CAMPAIGN:
|
||||||
campaign = self.get_campaign(ev.context_id)
|
campaign = self.get_campaign(ev.context_id)
|
||||||
campaign.is_over = False
|
campaign.is_over = False
|
||||||
|
# nothing specific to do with CHOICE (can retrigger pairing)
|
||||||
else:
|
else:
|
||||||
new_events.append(ev)
|
new_events.append(ev)
|
||||||
self.events = new_events
|
self.events = new_events
|
||||||
|
|
@ -104,7 +104,7 @@ class War:
|
||||||
"Some influence tokens already exist in this war.\n"
|
"Some influence tokens already exist in this war.\n"
|
||||||
"All tokens will be deleted and tie-breaks will become draw.\n"
|
"All tokens will be deleted and tie-breaks will become draw.\n"
|
||||||
"Do you want to continue?",
|
"Do you want to continue?",
|
||||||
action=cleanup_token_and_tie,
|
action=remove_token_and_draw_tie,
|
||||||
)
|
)
|
||||||
if new_state is True:
|
if new_state is True:
|
||||||
has_tie_resolved = any(isinstance(ev, TieResolved) for ev in self.events)
|
has_tie_resolved = any(isinstance(ev, TieResolved) for ev in self.events)
|
||||||
|
|
@ -120,7 +120,7 @@ class War:
|
||||||
"Some influence tokens and draws exist in this war.\n"
|
"Some influence tokens and draws exist in this war.\n"
|
||||||
"All influence outcomes and tie-breaks will be reset.\n"
|
"All influence outcomes and tie-breaks will be reset.\n"
|
||||||
"Do you want to continue?",
|
"Do you want to continue?",
|
||||||
action=reset_tie_break,
|
action=remove_tiebreak_and_not_over,
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_state(self, new_state: bool) -> None:
|
def set_state(self, new_state: bool) -> None:
|
||||||
|
|
@ -216,8 +216,8 @@ class War:
|
||||||
|
|
||||||
# War participant methods
|
# War participant methods
|
||||||
|
|
||||||
def get_all_war_participants_ids(self) -> set[str]:
|
def get_all_war_participants_ids(self) -> List[str]:
|
||||||
return set(self.participants.keys())
|
return list(self.participants.keys())
|
||||||
|
|
||||||
def has_participant(self, participant_id: str) -> bool:
|
def has_participant(self, participant_id: str) -> bool:
|
||||||
return participant_id in self.participants
|
return participant_id in self.participants
|
||||||
|
|
@ -288,6 +288,7 @@ class War:
|
||||||
if month is None:
|
if month is None:
|
||||||
month = self.get_default_campaign_values()["month"]
|
month = self.get_default_campaign_values()["month"]
|
||||||
campaign = Campaign(name, month)
|
campaign = Campaign(name, month)
|
||||||
|
campaign._war = self
|
||||||
self.campaigns.append(campaign)
|
self.campaigns.append(campaign)
|
||||||
return campaign
|
return campaign
|
||||||
|
|
||||||
|
|
@ -550,3 +551,47 @@ class War:
|
||||||
if isinstance(e, InfluenceSpent) and e.participant_id == participant_id
|
if isinstance(e, InfluenceSpent) and e.participant_id == participant_id
|
||||||
)
|
)
|
||||||
return gained - spent
|
return gained - spent
|
||||||
|
|
||||||
|
def get_events_by_ties_session(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:
|
||||||
|
self.events = [ev for ev in self.events if ev.tie_id != tie_id]
|
||||||
|
|
||||||
|
def revert_choice_ties(
|
||||||
|
self,
|
||||||
|
round_id: str,
|
||||||
|
*,
|
||||||
|
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
|
||||||
|
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]
|
||||||
|
|
||||||
|
def revert_tie(self, tie_id: str) -> None:
|
||||||
|
self.events = [ev for ev in self.events if ev.tie_id != tie_id]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import Dict, Any, TypeVar, Type, cast
|
from typing import Dict, Any, TypeVar, Type, cast, List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
|
|
@ -22,12 +22,14 @@ class WarEvent:
|
||||||
participant_id: str | None,
|
participant_id: str | None,
|
||||||
context_type: str,
|
context_type: str,
|
||||||
context_id: str,
|
context_id: str,
|
||||||
|
tie_id: str | None = None,
|
||||||
):
|
):
|
||||||
self.id: str = str(uuid4())
|
self.id: str = str(uuid4())
|
||||||
self.participant_id: str | None = participant_id
|
self.participant_id: str | None = participant_id
|
||||||
self.context_type = context_type # battle, round, campaign, war
|
self.context_type = context_type # battle, round, campaign, war
|
||||||
self.context_id = context_id
|
self.context_id = context_id
|
||||||
self.timestamp: datetime = datetime.now()
|
self.timestamp: datetime = datetime.now()
|
||||||
|
self.tie_id = tie_id
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_id(self, new_id: str) -> None:
|
||||||
self.id = new_id
|
self.id = new_id
|
||||||
|
|
@ -46,6 +48,7 @@ class WarEvent:
|
||||||
"context_type": self.context_type,
|
"context_type": self.context_type,
|
||||||
"context_id": self.context_id,
|
"context_id": self.context_id,
|
||||||
"timestamp": self.timestamp.isoformat(),
|
"timestamp": self.timestamp.isoformat(),
|
||||||
|
"tie_id": self.tie_id,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
@ -55,6 +58,7 @@ class WarEvent:
|
||||||
ev.context_type = data["context_type"]
|
ev.context_type = data["context_type"]
|
||||||
ev.context_id = data["context_id"]
|
ev.context_id = data["context_id"]
|
||||||
ev.timestamp = datetime.fromisoformat(data["timestamp"])
|
ev.timestamp = datetime.fromisoformat(data["timestamp"])
|
||||||
|
ev.tie_id = data.get("tie_id")
|
||||||
return ev
|
return ev
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -72,14 +76,17 @@ class TieResolved(WarEvent):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
participant_id: str | None,
|
participant_id: str | None, # winner_id or None if confirmed draw
|
||||||
context_type: str,
|
context_type: str,
|
||||||
context_id: str,
|
context_id: str,
|
||||||
|
participants: List[str],
|
||||||
|
tie_id: str | None = None, # None if draw without tie-break
|
||||||
score_value: int | None = None,
|
score_value: int | None = None,
|
||||||
objective_id: str | None = None,
|
objective_id: str | None = None,
|
||||||
sector_id: str | None = None,
|
sector_id: str | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(participant_id, context_type, context_id)
|
super().__init__(participant_id, context_type, context_id, tie_id)
|
||||||
|
self.participants = participants
|
||||||
self.score_value = score_value
|
self.score_value = score_value
|
||||||
self.objective_id = objective_id
|
self.objective_id = objective_id
|
||||||
self.sector_id = sector_id
|
self.sector_id = sector_id
|
||||||
|
|
@ -88,6 +95,7 @@ class TieResolved(WarEvent):
|
||||||
d = super().toDict()
|
d = super().toDict()
|
||||||
d.update(
|
d.update(
|
||||||
{
|
{
|
||||||
|
"participants": self.participants,
|
||||||
"score_value": self.score_value or None,
|
"score_value": self.score_value or None,
|
||||||
"objective_id": self.objective_id or None,
|
"objective_id": self.objective_id or None,
|
||||||
"sector_id": self.sector_id or None,
|
"sector_id": self.sector_id or None,
|
||||||
|
|
@ -101,6 +109,8 @@ class TieResolved(WarEvent):
|
||||||
JsonHelper.none_if_empty(data["participant_id"]),
|
JsonHelper.none_if_empty(data["participant_id"]),
|
||||||
data["context_type"],
|
data["context_type"],
|
||||||
data["context_id"],
|
data["context_id"],
|
||||||
|
data["participants"],
|
||||||
|
data["tie_id"],
|
||||||
JsonHelper.none_if_empty(data["score_value"]),
|
JsonHelper.none_if_empty(data["score_value"]),
|
||||||
JsonHelper.none_if_empty(data["objective_id"]),
|
JsonHelper.none_if_empty(data["objective_id"]),
|
||||||
JsonHelper.none_if_empty(data["sector_id"]),
|
JsonHelper.none_if_empty(data["sector_id"]),
|
||||||
|
|
@ -156,11 +166,14 @@ class InfluenceSpent(WarEvent):
|
||||||
amount: int,
|
amount: int,
|
||||||
context_type: str,
|
context_type: str,
|
||||||
context_id: str,
|
context_id: str,
|
||||||
|
tie_id: str,
|
||||||
objective_id: str | None = None,
|
objective_id: str | None = None,
|
||||||
|
sector_id: str | None = None,
|
||||||
):
|
):
|
||||||
super().__init__(participant_id, context_type, context_id)
|
super().__init__(participant_id, context_type, context_id, tie_id)
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.objective_id = objective_id
|
self.objective_id = objective_id
|
||||||
|
self.sector_id = sector_id
|
||||||
|
|
||||||
def toDict(self) -> Dict[str, Any]:
|
def toDict(self) -> Dict[str, Any]:
|
||||||
d = super().toDict()
|
d = super().toDict()
|
||||||
|
|
@ -168,6 +181,7 @@ class InfluenceSpent(WarEvent):
|
||||||
{
|
{
|
||||||
"amount": self.amount,
|
"amount": self.amount,
|
||||||
"objective_id": self.objective_id,
|
"objective_id": self.objective_id,
|
||||||
|
"sector_id": self.sector_id or None,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return d
|
return d
|
||||||
|
|
@ -179,6 +193,8 @@ class InfluenceSpent(WarEvent):
|
||||||
int(data["amount"]),
|
int(data["amount"]),
|
||||||
data["context_type"],
|
data["context_type"],
|
||||||
data["context_id"],
|
data["context_id"],
|
||||||
|
data["tie_id"],
|
||||||
JsonHelper.none_if_empty(data["objective_id"]),
|
JsonHelper.none_if_empty(data["objective_id"]),
|
||||||
|
JsonHelper.none_if_empty(data["sector_id"]),
|
||||||
)
|
)
|
||||||
return cls._base_fromDict(ev, data)
|
return cls._base_fromDict(ev, data)
|
||||||
|
|
|
||||||
|
|
@ -338,7 +338,12 @@
|
||||||
"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",
|
||||||
|
"participants": [
|
||||||
|
"602e2eaf-297e-490b-b0e9-efec818e466a",
|
||||||
|
"1f6b4e7c-b1e4-4a2e-9aea-7bb75b20b4de"
|
||||||
|
],
|
||||||
"timestamp": "2026-02-26T16:11:44.346337",
|
"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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue