diff --git a/src/warchron/constants.py b/src/warchron/constants.py index 8da62f9..37c4a01 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -15,6 +15,8 @@ RESOURCES_DIR = VIEW_ROOT / "resources" ROLE_TYPE = Qt.ItemDataRole.UserRole ROLE_ID = Qt.ItemDataRole.UserRole + 1 +# TODO use StrEnum and auto() instead of str,Enum and "name" + class IconName(str, Enum): UNDO = "undo" @@ -245,3 +247,4 @@ class ContextType(StrEnum): CAMPAIGN = "campaign" CHOICE = "choice" BATTLE = "battle" + OBJECTIVE = auto() diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 88199eb..e0ff8b2 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -171,7 +171,19 @@ class CampaignController: counters=counters, context_type=ContextType.CAMPAIGN, context_id=ctx.context_id, + context_name=None, ) + if ctx.context_type == ContextType.OBJECTIVE: + campaign_id, objective_id = ctx.context_id.split(":") + objective = war.objectives[objective_id] + dialog = TieDialog( + parent=self.app.view, + players=players, + counters=counters, + context_type=ctx.context_type, + context_id=ctx.context_id, + context_name=objective.name, + ) if not dialog.exec(): TieResolver.cancel_tie_break( war, ContextType.CAMPAIGN, ctx.context_id, ctx.score_value diff --git a/src/warchron/controller/closure_workflow.py b/src/warchron/controller/closure_workflow.py index fe368ac..e54f921 100644 --- a/src/warchron/controller/closure_workflow.py +++ b/src/warchron/controller/closure_workflow.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from warchron.controller.app_controller import AppController - from warchron.model.war import War from warchron.model.campaign import Campaign from warchron.model.round import Round @@ -45,6 +44,23 @@ class CampaignClosureWorkflow(ClosureWorkflow): TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids) TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_campaign_ties(war, campaign.id) + for objective_id in war.objectives: + ties = TieResolver.find_campaign_objective_ties( + war, + campaign.id, + objective_id, + ) + while ties: + bids_map = self.app.campaigns.resolve_ties(war, ties) + for tie in ties: + bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)] + TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids) + TieResolver.resolve_tie_state(war, tie, bids) + ties = TieResolver.find_campaign_objective_ties( + war, + campaign.id, + objective_id, + ) ClosureService.finalize_campaign(campaign) @@ -60,4 +76,19 @@ class WarClosureWorkflow(ClosureWorkflow): TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids) TieResolver.resolve_tie_state(war, tie, bids) ties = TieResolver.find_war_ties(war) + for objective_id in war.objectives: + ties = TieResolver.find_war_objective_ties( + war, + objective_id, + ) + while ties: + bids_map = self.app.wars.resolve_ties(war, ties) + for tie in ties: + bids = bids_map[(tie.context_type, tie.context_id, tie.score_value)] + TieResolver.apply_bids(war, tie.context_type, tie.context_id, bids) + TieResolver.resolve_tie_state(war, tie, bids) + ties = TieResolver.find_war_objective_ties( + war, + objective_id, + ) ClosureService.finalize_war(war) diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py index dcbf05e..84bc0c4 100644 --- a/src/warchron/controller/dtos.py +++ b/src/warchron/controller/dtos.py @@ -107,14 +107,6 @@ class BattleDTO: player2_tooltip: str | None = None -@dataclass(frozen=True, slots=True) -class ParticipantScoreDTO: - participant_id: str - player_name: str - victory_points: int - narrative_points: Dict[str, int] - - @dataclass(frozen=True, slots=True) class CampaignParticipantScoreDTO: campaign_participant_id: str diff --git a/src/warchron/controller/ranking_icon.py b/src/warchron/controller/ranking_icon.py index bb7d914..06f1b3f 100644 --- a/src/warchron/controller/ranking_icon.py +++ b/src/warchron/controller/ranking_icon.py @@ -21,11 +21,6 @@ class RankingIcon: context_id: str, scores: Dict[str, ParticipantScore], ) -> Dict[str, QIcon]: - # scores = ScoreService.compute_scores( - # war, - # context_type, - # context_id, - # ) ranking = ResultChecker.get_effective_ranking( war, context_type, context_id, scores ) diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 84977e9..dd516ae 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -155,7 +155,19 @@ class WarController: counters=counters, context_type=ContextType.WAR, context_id=ctx.context_id, + context_name=None, ) + if ctx.context_type == ContextType.OBJECTIVE: + _, objective_id = ctx.context_id.split(":") + objective = war.objectives[objective_id] + dialog = TieDialog( + parent=self.app.view, + players=players, + counters=counters, + context_type=ctx.context_type, + context_id=ctx.context_id, + context_name=objective.name, + ) if not dialog.exec(): TieResolver.cancel_tie_break( war, ContextType.WAR, ctx.context_id, ctx.score_value diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index e174e8e..028001e 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -6,7 +6,7 @@ from warchron.constants import ContextType from warchron.model.exception import ForbiddenOperation from warchron.model.war import War from warchron.model.war_event import InfluenceSpent, TieResolved -from warchron.model.score_service import ScoreService +from warchron.model.score_service import ScoreService, ParticipantScore @dataclass @@ -85,6 +85,61 @@ class TieResolver: ) return ties + @staticmethod + def find_campaign_objective_ties( + war: War, + campaign_id: str, + objective_id: str, + ) -> List[TieContext]: + base_scores = ScoreService.compute_scores( + war, + ContextType.CAMPAIGN, + campaign_id, + ) + scores = TieResolver._build_objective_scores( + base_scores, + objective_id, + ) + buckets: DefaultDict[int, List[str]] = defaultdict(list) + for pid, score in scores.items(): + buckets[score.victory_points].append(pid) + ties: List[TieContext] = [] + context_id = f"{campaign_id}:{objective_id}" + for score_value, participants in buckets.items(): + if len(participants) <= 1: + continue + if TieResolver.is_tie_resolved( + war, + ContextType.OBJECTIVE, + context_id, + score_value, + ): + continue + if not TieResolver.can_tie_be_resolved( + war, + ContextType.OBJECTIVE, + context_id, + participants, + ): + war.events.append( + TieResolved( + None, + ContextType.OBJECTIVE, + context_id, + score_value, + ) + ) + continue + ties.append( + TieContext( + context_type=ContextType.OBJECTIVE, + context_id=context_id, + participants=participants, + score_value=score_value, + ) + ) + return ties + @staticmethod def find_war_ties(war: War) -> List[TieContext]: from warchron.model.result_checker import ResultChecker @@ -115,6 +170,79 @@ class TieResolver: ) return ties + @staticmethod + def find_war_objective_ties( + war: War, + objective_id: str, + ) -> List[TieContext]: + from warchron.model.result_checker import ResultChecker + + base_scores = ScoreService.compute_scores( + war, + ContextType.WAR, + war.id, + ) + scores = TieResolver._build_objective_scores( + base_scores, + objective_id, + ) + ranking = ResultChecker.get_effective_ranking( + war, + ContextType.OBJECTIVE, + f"{war.id}:{objective_id}", + scores, + ) + ties: List[TieContext] = [] + for _, group, _ in ranking: + if len(group) <= 1: + continue + score_value = scores[group[0]].victory_points + context_id = f"{war.id}:{objective_id}" + if TieResolver.is_tie_resolved( + war, + ContextType.OBJECTIVE, + context_id, + score_value, + ): + continue + if not TieResolver.can_tie_be_resolved( + war, + ContextType.OBJECTIVE, + context_id, + group, + ): + war.events.append( + TieResolved( + None, + ContextType.OBJECTIVE, + context_id, + score_value, + ) + ) + continue + ties.append( + TieContext( + context_type=ContextType.OBJECTIVE, + context_id=context_id, + participants=group, + score_value=score_value, + ) + ) + return ties + + @staticmethod + def _build_objective_scores( + base_scores: Dict[str, ParticipantScore], + objective_id: str, + ) -> Dict[str, ParticipantScore]: + return { + pid: ParticipantScore( + victory_points=score.narrative_points.get(objective_id, 0), + narrative_points={}, + ) + for pid, score in base_scores.items() + } + @staticmethod def apply_bids( war: War, diff --git a/src/warchron/view/tie_dialog.py b/src/warchron/view/tie_dialog.py index 971a5ec..41a17ff 100644 --- a/src/warchron/view/tie_dialog.py +++ b/src/warchron/view/tie_dialog.py @@ -26,6 +26,7 @@ class TieDialog(QDialog): counters: List[int], context_type: ContextType, context_id: str, + context_name: str | None = None, ) -> None: super().__init__(parent) self._context_id = context_id @@ -33,7 +34,7 @@ class TieDialog(QDialog): self.ui: Ui_tieDialog = Ui_tieDialog() self.ui.setupUi(self) # type: ignore self.setWindowIcon(Icons.get(IconName.WARCHRONICO)) - self.ui.tieContext.setText(self._get_context_title(context_type)) + self.ui.tieContext.setText(self._get_context_title(context_type, context_name)) grid = self.ui.playersGridLayout icon_path = (RESOURCES_DIR / Icons._paths[IconName.TOKENS]).as_posix() token_html = ( @@ -72,11 +73,14 @@ class TieDialog(QDialog): return {pid: checkbox.isChecked() for pid, checkbox in self._checkboxes.items()} @staticmethod - def _get_context_title(context_type: ContextType) -> str: + def _get_context_title( + context_type: ContextType, context_name: str | None = None + ) -> str: titles = { ContextType.BATTLE: "Battle tie", ContextType.CAMPAIGN: "Campaign tie", ContextType.WAR: "War tie", ContextType.CHOICE: "Choice tie", + ContextType.OBJECTIVE: f"Objective tie: {context_name}", } return titles.get(context_type, "Tie")