allow closing round with incomplete battles

This commit is contained in:
Maxime Réaux 2026-03-26 11:21:17 +01:00
parent ae6c033bbe
commit 69942a3cff
10 changed files with 67 additions and 29 deletions

View file

@ -235,7 +235,8 @@ class AppController:
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) )
if reply == QMessageBox.StandardButton.Yes: if reply == QMessageBox.StandardButton.Yes:
e.action() if e.action:
e.action()
else: else:
return return
self.is_dirty = True self.is_dirty = True
@ -290,7 +291,8 @@ class AppController:
) )
if reply == QMessageBox.StandardButton.Yes: if reply == QMessageBox.StandardButton.Yes:
try: try:
e.action() if e.action:
e.action()
except DomainError as inner: except DomainError as inner:
QMessageBox.warning( QMessageBox.warning(
self.view, self.view,
@ -361,7 +363,8 @@ class AppController:
) )
if reply == QMessageBox.StandardButton.Yes: if reply == QMessageBox.StandardButton.Yes:
try: try:
e.action() if e.action:
e.action()
except DomainError as inner: except DomainError as inner:
QMessageBox.warning( QMessageBox.warning(
self.view, self.view,

View file

@ -186,14 +186,36 @@ class RoundController:
camp = self.app.model.get_campaign_by_round(round_id) camp = self.app.model.get_campaign_by_round(round_id)
war = self.app.model.get_war_by_round(round_id) war = self.app.model.get_war_by_round(round_id)
workflow = RoundClosureWorkflow(self.app) workflow = RoundClosureWorkflow(self.app)
try: confirmed = False
workflow.start(war, camp, rnd) stop = False
except DomainError as e: while True:
QMessageBox.warning( try:
self.app.view, workflow.start(war, camp, rnd, confirmed)
"Closure forbidden", break
str(e), except RequiresConfirmation as e:
) reply = QMessageBox.question(
self.app.view,
"Confirm closing",
str(e),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
if e.action:
e.action()
confirmed = True
continue
else:
stop = True
break
except DomainError as e:
QMessageBox.warning(
self.app.view,
"Closure forbidden",
str(e),
)
stop = True
break
if stop:
return return
self.app.is_dirty = True self.app.is_dirty = True
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS) self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
@ -234,7 +256,8 @@ class RoundController:
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) )
if reply == QMessageBox.StandardButton.Yes: if reply == QMessageBox.StandardButton.Yes:
e.action() if e.action:
e.action()
else: else:
return return
self.app.is_dirty = True self.app.is_dirty = True

View file

@ -221,7 +221,8 @@ class WarController:
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
) )
if reply == QMessageBox.StandardButton.Yes: if reply == QMessageBox.StandardButton.Yes:
e.action() if e.action:
e.action()
else: else:
return return
self.is_dirty = True self.is_dirty = True

View file

@ -18,8 +18,10 @@ class Workflow:
class RoundClosureWorkflow(Workflow): class RoundClosureWorkflow(Workflow):
def start(self, war: War, campaign: Campaign, round: Round) -> None: def start(
Closer.check_round_closable(round) self, war: War, campaign: Campaign, round: Round, confirmed: bool = False
) -> None:
Closer.check_round_closable(round, confirmed)
ties = TieBreaker.find_battle_ties(war, round.id) ties = TieBreaker.find_battle_ties(war, round.id)
while ties: while ties:
for tie in ties: for tie in ties:

View file

@ -80,6 +80,12 @@ class Battle:
def is_finished(self) -> bool: def is_finished(self) -> bool:
return self.winner_id is not None or self.is_draw() return self.winner_id is not None or self.is_draw()
def has_player(self) -> bool:
return self.player_1_id is not None or self.player_2_id is not None
def is_complete(self) -> bool:
return self.player_1_id is not None and self.player_2_id is not None
def toDict(self) -> Dict[str, Any]: def toDict(self) -> Dict[str, Any]:
return { return {
"sector_id": self.sector_id, "sector_id": self.sector_id,

View file

@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from warchron.constants import ContextType from warchron.constants import ContextType
from warchron.model.exception import ForbiddenOperation from warchron.model.exception import ForbiddenOperation, RequiresConfirmation
from warchron.model.war_event import InfluenceGained from warchron.model.war_event import InfluenceGained
from warchron.model.war import War from warchron.model.war import War
from warchron.model.campaign import Campaign from warchron.model.campaign import Campaign
@ -14,13 +14,19 @@ class Closer:
# Round methods # Round methods
@staticmethod @staticmethod
def check_round_closable(round: Round) -> None: def check_round_closable(round: Round, confirmed: bool) -> None:
if round.is_over: if round.is_over:
raise ForbiddenOperation("Round already closed") raise ForbiddenOperation("Round already closed")
if not round.all_battles_finished(): if not confirmed:
raise ForbiddenOperation( if any(not bat.is_complete() for bat in round.battles.values()):
"All battles must be finished to close their round" raise RequiresConfirmation(
) "Battle(s) in this round miss player(s).\n"
"Do you want to continue?",
)
if not round.all_battles_finished():
raise ForbiddenOperation(
"All battles must be finished to close their round"
)
@staticmethod @staticmethod
def apply_battle_outcomes(war: War, campaign: Campaign, battle: Battle) -> None: def apply_battle_outcomes(war: War, campaign: Campaign, battle: Battle) -> None:

View file

@ -26,6 +26,6 @@ class DomainDecision(Exception):
class RequiresConfirmation(DomainDecision): class RequiresConfirmation(DomainDecision):
def __init__(self, message: str, action: Callable[[], None]): def __init__(self, message: str, action: Callable[[], None] | None = None):
super().__init__(message) super().__init__(message)
self.action = action self.action = action

View file

@ -63,10 +63,7 @@ class Pairing:
bat.set_winner(None) bat.set_winner(None)
war.revert_choice_ties(round.id) war.revert_choice_ties(round.id)
if any( if any(bat.has_player() for bat in round.battles.values()):
bat.player_1_id is not None or bat.player_2_id is not None
for bat in round.battles.values()
):
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"

View file

@ -166,7 +166,6 @@ class Round:
return any(b.is_finished() for b in self.battles.values()) return any(b.is_finished() for b in self.battles.values())
def all_battles_finished(self) -> bool: def all_battles_finished(self) -> bool:
# TODO exception for participant alone
return all( return all(
b.winner_id is not None or b.is_draw() for b in self.battles.values() b.winner_id is not None or b.is_draw() for b in self.battles.values()
) )

View file

@ -63,8 +63,9 @@ class TieBreaker:
for battle in round.battles.values(): for battle in round.battles.values():
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 not battle.is_complete():
raise DomainError("Missing player(s) in this battle context.") continue
assert battle.player_1_id is not None and battle.player_2_id is not None
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(): if not battle.is_draw():