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

View file

@ -186,14 +186,36 @@ class RoundController:
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:
workflow.start(war, camp, rnd)
except DomainError as e:
QMessageBox.warning(
self.app.view,
"Closure forbidden",
str(e),
)
confirmed = False
stop = False
while True:
try:
workflow.start(war, camp, rnd, confirmed)
break
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
self.app.is_dirty = True
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
@ -234,7 +256,8 @@ class RoundController:
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
e.action()
if e.action:
e.action()
else:
return
self.app.is_dirty = True

View file

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

View file

@ -18,8 +18,10 @@ class Workflow:
class RoundClosureWorkflow(Workflow):
def start(self, war: War, campaign: Campaign, round: Round) -> None:
Closer.check_round_closable(round)
def start(
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)
while ties:
for tie in ties:

View file

@ -80,6 +80,12 @@ class Battle:
def is_finished(self) -> bool:
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]:
return {
"sector_id": self.sector_id,

View file

@ -1,7 +1,7 @@
from __future__ import annotations
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 import War
from warchron.model.campaign import Campaign
@ -14,13 +14,19 @@ class Closer:
# Round methods
@staticmethod
def check_round_closable(round: Round) -> None:
def check_round_closable(round: Round, confirmed: bool) -> None:
if round.is_over:
raise ForbiddenOperation("Round already closed")
if not round.all_battles_finished():
raise ForbiddenOperation(
"All battles must be finished to close their round"
)
if not confirmed:
if any(not bat.is_complete() for bat in round.battles.values()):
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
def apply_battle_outcomes(war: War, campaign: Campaign, battle: Battle) -> None:

View file

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

View file

@ -63,10 +63,7 @@ class Pairing:
bat.set_winner(None)
war.revert_choice_ties(round.id)
if any(
bat.player_1_id is not None or bat.player_2_id is not None
for bat in round.battles.values()
):
if any(bat.has_player() for bat in round.battles.values()):
raise RequiresConfirmation(
"Battle(s) already have player(s) assigned for this round.\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())
def all_battles_finished(self) -> bool:
# TODO exception for participant alone
return all(
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():
if campaign is None:
raise DomainError("No campaign for this battle tie")
if battle.player_1_id is None or battle.player_2_id is None:
raise DomainError("Missing player(s) in this battle context.")
if not battle.is_complete():
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)
p2_id = campaign.campaign_to_war_part_id(battle.player_2_id)
if not battle.is_draw():