detect and resolve battle tie with influence_token

This commit is contained in:
Maxime Réaux 2026-02-17 16:37:36 +01:00
parent 115ddf8d50
commit 818d2886f4
23 changed files with 808 additions and 172 deletions

View file

@ -0,0 +1,77 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from warchron.controller.app_controller import AppController
from warchron.constants import ContextType
from warchron.model.exception import ForbiddenOperation
from warchron.model.war_event import TieResolved
from warchron.model.war import War
from warchron.model.campaign import Campaign
from warchron.model.battle import Battle
from warchron.model.round import Round
from warchron.model.closure_service import ClosureService
from warchron.model.tie_manager import TieResolver
from warchron.controller.dtos import TieContext
class ClosureWorkflow:
def __init__(self, controller: "AppController"):
self.app = controller
class RoundClosureWorkflow(ClosureWorkflow):
def start(self, war: War, campaign: Campaign, round: Round) -> None:
ClosureService.check_round_closable(round)
ties = TieResolver.find_round_ties(round, war)
while ties:
contexts = [
RoundClosureWorkflow.build_battle_context(campaign, b) for b in ties
]
resolvable = []
for ctx in contexts:
if TieResolver.can_tie_be_resolved(war, ctx.participants):
resolvable.append(ctx)
else:
war.events.append(
TieResolved(
participant_id=None,
context_type=ctx.context_type,
context_id=ctx.context_id,
)
)
if not resolvable:
break
bids_map = self.app.rounds.resolve_ties(war, contexts)
for ctx in contexts:
bids = bids_map[ctx.context_id]
TieResolver.apply_bids(
war,
ctx.context_type,
ctx.context_id,
bids,
)
TieResolver.try_tie_break(
war,
ctx.context_type,
ctx.context_id,
ctx.participants,
)
ties = TieResolver.find_round_ties(round, war)
for battle in round.battles.values():
ClosureService.apply_battle_outcomes(war, campaign, battle)
ClosureService.finalize_round(round)
@staticmethod
def build_battle_context(campaign: Campaign, battle: Battle) -> TieContext:
if battle.player_1_id is None or battle.player_2_id is None:
raise ForbiddenOperation("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
return TieContext(
context_type=ContextType.BATTLE,
context_id=battle.sector_id,
participants=[p1, p2],
)

View file

@ -3,6 +3,8 @@ from dataclasses import dataclass
from PyQt6.QtGui import QIcon
from warchron.constants import ContextType
@dataclass(frozen=True)
class ParticipantOption:
@ -103,3 +105,10 @@ class BattleDTO:
state_icon: QIcon | None
player1_icon: QIcon | None
player2_icon: QIcon | None
@dataclass
class TieContext:
context_type: ContextType
context_id: str
participants: List[str] # war_participant_ids

View file

@ -1,16 +1,27 @@
from typing import List, TYPE_CHECKING
from typing import List, Dict, TYPE_CHECKING
from PyQt6.QtWidgets import QDialog, QMessageBox
from PyQt6.QtWidgets import QDialog
from PyQt6.QtWidgets import QMessageBox
from warchron.constants import ItemType, RefreshScope, Icons, IconName
from warchron.model.exception import ForbiddenOperation, DomainError
from warchron.model.round import Round
from warchron.model.war import War
if TYPE_CHECKING:
from warchron.controller.app_controller import AppController
from warchron.controller.dtos import ParticipantOption, SectorDTO, ChoiceDTO, BattleDTO
from warchron.model.closure_service import ClosureService
from warchron.controller.dtos import (
ParticipantOption,
SectorDTO,
ChoiceDTO,
BattleDTO,
TieContext,
)
from warchron.controller.closure_workflow import RoundClosureWorkflow
from warchron.view.choice_dialog import ChoiceDialog
from warchron.view.battle_dialog import BattleDialog
from warchron.view.tie_dialog import TieDialog
class RoundController:
@ -123,26 +134,47 @@ class RoundController:
if not round_id:
return
rnd = self.app.model.get_round(round_id)
if rnd.is_over:
return
camp = self.app.model.get_campaign_by_round(round_id)
war = self.app.model.get_war_by_round(round_id)
workflow = RoundClosureWorkflow(self.app)
try:
ties = ClosureService.close_round(rnd)
except RuntimeError as e:
QMessageBox.warning(self.app.view, "Cannot close round", str(e))
return
if ties:
QMessageBox.information(
workflow.start(war, camp, rnd)
except DomainError as e:
QMessageBox.warning(
self.app.view,
"Tie detected",
"Round has unresolved ties. Resolution system not implemented yet.",
"Deletion forbidden",
str(e),
)
return
self.app.is_dirty = True
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
)
def resolve_ties(
self, war: War, contexts: List[TieContext]
) -> Dict[str, Dict[str, bool]]:
bids_map = {}
for ctx in contexts:
players = [
ParticipantOption(
id=pid,
name=self.app.model.get_participant_name(pid),
)
for pid in ctx.participants
]
counters = [war.get_influence_tokens(pid) for pid in ctx.participants]
dialog = TieDialog(
parent=self.app.view,
players=players,
counters=counters,
context_id=ctx.context_id,
)
if not dialog.exec():
raise ForbiddenOperation("Tie resolution cancelled")
bids_map[ctx.context_id] = dialog.get_bids()
return bids_map
# Choice methods
def edit_round_choice(self, choice_id: str) -> None: