diff --git a/src/warchron/controller/app_controller.py b/src/warchron/controller/app_controller.py index fd47b91..7522e08 100644 --- a/src/warchron/controller/app_controller.py +++ b/src/warchron/controller/app_controller.py @@ -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, diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index fb11b7b..e0612fe 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -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 diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 4b7c3ef..eb6848e 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -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 diff --git a/src/warchron/controller/workflows.py b/src/warchron/controller/workflows.py index ea0a4b3..ce0cc19 100644 --- a/src/warchron/controller/workflows.py +++ b/src/warchron/controller/workflows.py @@ -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: diff --git a/src/warchron/model/battle.py b/src/warchron/model/battle.py index 02a9e90..3a105ce 100644 --- a/src/warchron/model/battle.py +++ b/src/warchron/model/battle.py @@ -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, diff --git a/src/warchron/model/closing.py b/src/warchron/model/closing.py index 0b70b39..7140ac8 100644 --- a/src/warchron/model/closing.py +++ b/src/warchron/model/closing.py @@ -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: diff --git a/src/warchron/model/exception.py b/src/warchron/model/exception.py index 26194e3..5c27295 100644 --- a/src/warchron/model/exception.py +++ b/src/warchron/model/exception.py @@ -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 diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py index fc3cf71..09d9d58 100644 --- a/src/warchron/model/pairing.py +++ b/src/warchron/model/pairing.py @@ -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" diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py index 4b1a630..ce4c73b 100644 --- a/src/warchron/model/round.py +++ b/src/warchron/model/round.py @@ -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() ) diff --git a/src/warchron/model/tiebreaking.py b/src/warchron/model/tiebreaking.py index ed11ed0..a71767b 100644 --- a/src/warchron/model/tiebreaking.py +++ b/src/warchron/model/tiebreaking.py @@ -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():