diff --git a/src/warchron/constants.py b/src/warchron/constants.py index 951f91d..495565d 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -49,7 +49,8 @@ class IconName(StrEnum): NP1ST = auto() NP2ND = auto() NP3RD = auto() - TIEBREAK_TOKEN = auto() + WINTOKEN = auto() + TIEBREAKTOKEN = auto() VP1STDRAW = auto() VP1STBREAK = auto() VP1STTIEDRAW = auto() @@ -143,11 +144,16 @@ class Icons: def get_pixmap(cls, name: IconName) -> QPixmap: if name in cls._pixmap_cache: return cls._pixmap_cache[name] - if name == IconName.TIEBREAK_TOKEN: + if name == IconName.TIEBREAKTOKEN: pix = cls._compose( cls.get_pixmap(IconName.TIEBREAK), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.WINTOKEN: + pix = cls._compose( + cls.get_pixmap(IconName.WIN), + cls.get_pixmap(IconName.TOKEN), + ) elif name == IconName.VP1STDRAW: pix = cls._compose( cls.get_pixmap(IconName.VP1ST), diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index 42855c1..b688bfd 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -22,8 +22,8 @@ from warchron.model.sector import Sector from warchron.model.tie_manager import TieContext, TieResolver from warchron.model.score_service import ScoreService from warchron.controller.closure_workflow import CampaignClosureWorkflow -from warchron.controller.ranking_icon import RankingIcon -from warchron.controller.presenter import TiePresenter + +from warchron.controller.presenter import Presenter from warchron.view.campaign_dialog import CampaignDialog from warchron.view.campaign_participant_dialog import CampaignParticipantDialog from warchron.view.sector_dialog import SectorDialog @@ -58,11 +58,11 @@ class CampaignController: vp_icon_map: Dict[str, QIcon] = {} objective_icon_maps: Dict[str, Dict[str, QIcon]] = {} if camp.is_over: - vp_icon_map = RankingIcon.compute_icons( + vp_icon_map = Presenter.compute_ranking_icons( war, ContextType.CAMPAIGN, campaign_id, scores ) for obj in war.get_objectives_used_as_maj_or_min(): - objective_icon_maps[obj.id] = RankingIcon.compute_icons( + objective_icon_maps[obj.id] = Presenter.compute_ranking_icons( war, ContextType.CAMPAIGN, camp.id, @@ -177,7 +177,7 @@ class CampaignController: for pid in active ] counters = [war.get_influence_tokens(pid) for pid in active] - data = TiePresenter.build_dialog_data( + data = Presenter.build_dialog_data( war, ctx, campaign=war.get_campaign(ctx.context_id) ) dialog = TieDialog( diff --git a/src/warchron/controller/presenter.py b/src/warchron/controller/presenter.py index 665028a..a90be32 100644 --- a/src/warchron/controller/presenter.py +++ b/src/warchron/controller/presenter.py @@ -1,12 +1,27 @@ -from warchron.constants import ContextType +from typing import Dict + +from PyQt6.QtGui import QIcon + +from warchron.constants import ( + ContextType, + Icons, + IconName, + VP_RANK_TO_ICON, + NP_RANK_TO_ICON, + ScoreKind, +) + from warchron.controller.dtos import TieDialogData -from warchron.model.tie_manager import TieContext +from warchron.model.tie_manager import TieContext, TieResolver from warchron.model.war import War from warchron.model.campaign import Campaign from warchron.model.round import Round +from warchron.model.score_service import ParticipantScore +from warchron.model.result_checker import ResultChecker +from warchron.model.exception import DomainError -class TiePresenter: +class Presenter: @staticmethod def build_dialog_data( @@ -33,7 +48,7 @@ class TiePresenter: if ctx.context_type == ContextType.CHOICE: if ctx.sector_id and campaign and round: sector = campaign.sectors[ctx.sector_id] - kind = TiePresenter._choice_kind(round, ctx) + kind = Presenter._choice_kind(round, ctx) return TieDialogData(f"Choice tie — {sector.name} ({kind})") return TieDialogData("Choice tie") return TieDialogData("Tie") @@ -50,3 +65,114 @@ class TiePresenter: if choice.secondary_sector_id == ctx.sector_id: return "secondary" return "choice" + + @staticmethod + def compute_ranking_icons( + war: War, + context_type: ContextType, + context_id: str, + scores: Dict[str, ParticipantScore], + *, + objective_id: str | None = None, + ) -> Dict[str, QIcon]: + if objective_id is None: + + def value_getter(score: ParticipantScore) -> int: + return score.victory_points + + icon_ranking = VP_RANK_TO_ICON + score_kind = ScoreKind.VP + else: + + def value_getter(score: ParticipantScore) -> int: + return score.narrative_points.get(objective_id, 0) + + icon_ranking = NP_RANK_TO_ICON + score_kind = ScoreKind.NP + ranking = ResultChecker.get_effective_ranking( + war, + context_type, + context_id, + score_kind, + scores, + value_getter, + objective_id, + ) + icon_map: Dict[str, QIcon] = {} + for rank, group, token_map in ranking: + if objective_id and rank not in icon_ranking: + continue + base_icon = icon_ranking.get( + rank, IconName.VPNTH if objective_id is None else None + ) + if base_icon is None: + continue + value = value_getter(scores[group[0]]) + original_group_size = sum( + 1 for s in scores.values() if value_getter(s) == value + ) + for pid in group: + spent = token_map.get(pid, 0) + if original_group_size == 1: + icon_name = base_icon + elif len(group) == 1: + if spent > 0: + icon_name = getattr(IconName, f"{base_icon.name}BREAK") + else: + icon_name = base_icon + else: + if spent > 0: + icon_name = getattr(IconName, f"{base_icon.name}TIEDRAW") + else: + icon_name = getattr(IconName, f"{base_icon.name}DRAW") + icon_map[pid] = QIcon(Icons.get_pixmap(icon_name)) + return icon_map + + @staticmethod + def compute_battle_icons( + war: War, + round_id: str, + battle_id: str, + ) -> tuple[QIcon | None, QIcon | None]: + battle = war.get_battle(battle_id) + if not (battle.player_1_id and battle.player_2_id and battle.is_finished()): + return None, None + campaign = war.get_campaign_by_sector(battle.sector_id) + if not campaign: + raise DomainError("No campaign found for this battle") + base_winner = None + if battle.winner_id is not None: + base_winner = campaign.campaign_to_war_part_id(battle.winner_id) + winner_id = ResultChecker.get_effective_winner_id( + war, + ContextType.BATTLE, + battle.sector_id, + base_winner, + ) + + def compute_icon(player: str) -> QIcon | None: + base_icon: IconName | None = None + if winner_id is None: + base_icon = IconName.DRAW + elif campaign.war_to_campaign_part_id(winner_id) == player: + base_icon = IconName.WIN + elif battle.is_draw(): + base_icon = IconName.TIEBREAK + if base_icon is None: + return None + spent = TieResolver.participant_spent_token( + war, + ContextType.BATTLE, + battle.sector_id, + None, + campaign.campaign_to_war_part_id(player), + ) + icon_name = ( + getattr(IconName, f"{base_icon.name}TOKEN") if spent else base_icon + ) + return QIcon(Icons.get_pixmap(icon_name)) + + return ( + compute_icon(battle.player_1_id), + compute_icon(battle.player_2_id), + ) diff --git a/src/warchron/controller/ranking_icon.py b/src/warchron/controller/ranking_icon.py deleted file mode 100644 index 1d9e131..0000000 --- a/src/warchron/controller/ranking_icon.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Dict - -from PyQt6.QtGui import QIcon - -from warchron.constants import ( - ContextType, - Icons, - IconName, - VP_RANK_TO_ICON, - NP_RANK_TO_ICON, - ScoreKind, -) -from warchron.model.war import War -from warchron.model.score_service import ParticipantScore -from warchron.model.result_checker import ResultChecker - - -class RankingIcon: - @staticmethod - def compute_icons( - war: War, - context_type: ContextType, - context_id: str, - scores: Dict[str, ParticipantScore], - *, - objective_id: str | None = None, - ) -> Dict[str, QIcon]: - - if objective_id is None: - - def value_getter(score: ParticipantScore) -> int: - return score.victory_points - - icon_ranking = VP_RANK_TO_ICON - score_kind = ScoreKind.VP - else: - - def value_getter(score: ParticipantScore) -> int: - return score.narrative_points.get(objective_id, 0) - - icon_ranking = NP_RANK_TO_ICON - score_kind = ScoreKind.NP - ranking = ResultChecker.get_effective_ranking( - war, - context_type, - context_id, - score_kind, - scores, - value_getter, - objective_id, - ) - icon_map: Dict[str, QIcon] = {} - for rank, group, token_map in ranking: - if objective_id and rank not in icon_ranking: - continue - base_icon = icon_ranking.get( - rank, IconName.VPNTH if objective_id is None else None - ) - if base_icon is None: - continue - value = value_getter(scores[group[0]]) - original_group_size = sum( - 1 for s in scores.values() if value_getter(s) == value - ) - for pid in group: - spent = token_map.get(pid, 0) - if original_group_size == 1: - icon_name = base_icon - elif len(group) == 1: - if spent > 0: - icon_name = getattr(IconName, f"{base_icon.name}BREAK") - else: - icon_name = base_icon - else: - if spent > 0: - icon_name = getattr(IconName, f"{base_icon.name}TIEDRAW") - else: - icon_name = getattr(IconName, f"{base_icon.name}DRAW") - icon_map[pid] = QIcon(Icons.get_pixmap(icon_name)) - return icon_map diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 3aa4b36..cecba13 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -18,7 +18,6 @@ from warchron.model.exception import ( RequiresConfirmation, ) from warchron.model.tie_manager import TieResolver, TieContext -from warchron.model.result_checker import ResultChecker from warchron.model.pairing import Pairing from warchron.model.round import Round from warchron.model.war import War @@ -36,7 +35,7 @@ from warchron.controller.closure_workflow import ( RoundClosureWorkflow, RoundPairingWorkflow, ) -from warchron.controller.presenter import TiePresenter +from warchron.controller.presenter import Presenter from warchron.view.choice_dialog import ChoiceDialog from warchron.view.battle_dialog import BattleDialog from warchron.view.tie_dialog import TieDialog @@ -47,7 +46,6 @@ class RoundController: self.app = app def _fill_round_details(self, round_id: str) -> None: - # self.app.view.clear_round_page() rnd = self.app.model.get_round(round_id) camp = self.app.model.get_campaign_by_round(round_id) war = self.app.model.get_war_by_round(round_id) @@ -135,7 +133,6 @@ class RoundController: player_1_name = self.app.model.get_participant_name( camp_part.war_participant_id ) - p1_id = battle.player_1_id else: player_1_name = "" if battle.player_2_id: @@ -143,7 +140,6 @@ class RoundController: player_2_name = self.app.model.get_participant_name( camp_part.war_participant_id ) - p2_id = battle.player_2_id else: player_2_name = "" if battle.winner_id: @@ -153,34 +149,9 @@ class RoundController: ) else: winner_name = "" - p1_icon = None - p2_icon = None - # TODO use uniform draw/tie icon logic with choice, war, campaign... - if battle.is_draw(): - p1_icon = Icons.get(IconName.DRAW) - p2_icon = Icons.get(IconName.DRAW) - context = TieContext( - ContextType.BATTLE, - battle.sector_id, - [p1_id, p2_id], - ) - if TieResolver.was_tie_broken_by_tokens(war, context): - effective_winner = ResultChecker.get_effective_winner_id( - war, ContextType.BATTLE, battle.sector_id, None - ) - p1_war = None - if battle.player_1_id is not None: - p1_war = camp.campaign_to_war_part_id(battle.player_1_id) - pixmap = Icons.get_pixmap(IconName.TIEBREAK_TOKEN) - if effective_winner == p1_war: - p1_icon = QIcon(pixmap) - else: - p2_icon = QIcon(pixmap) - elif battle.winner_id: - if battle.winner_id == battle.player_1_id: - p1_icon = Icons.get(IconName.WIN) - elif battle.winner_id == battle.player_2_id: - p2_icon = Icons.get(IconName.WIN) + p1_icon, p2_icon = Presenter.compute_battle_icons( + war, round_id, battle.sector_id + ) battles_for_display.append( BattleDTO( id=battle.sector_id, @@ -293,9 +264,7 @@ class RoundController: campaign = war.get_campaign_by_round(ctx.context_id) if campaign: round = war.get_round(ctx.context_id) - data = TiePresenter.build_dialog_data( - war, ctx, round=round, campaign=campaign - ) + data = Presenter.build_dialog_data(war, ctx, round=round, campaign=campaign) dialog = TieDialog( parent=self.app.view, players=players, diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index 8ebd690..48e68e6 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -27,8 +27,8 @@ from warchron.model.objective import Objective from warchron.model.tie_manager import TieContext, TieResolver from warchron.model.score_service import ScoreService from warchron.controller.closure_workflow import WarClosureWorkflow -from warchron.controller.ranking_icon import RankingIcon -from warchron.controller.presenter import TiePresenter + +from warchron.controller.presenter import Presenter from warchron.view.war_dialog import WarDialog from warchron.view.objective_dialog import ObjectiveDialog from warchron.view.war_participant_dialog import WarParticipantDialog @@ -62,11 +62,11 @@ class WarController: vp_icon_map: Dict[str, QIcon] = {} objective_icon_maps: Dict[str, Dict[str, QIcon]] = {} if war.is_over: - vp_icon_map = RankingIcon.compute_icons( + vp_icon_map = Presenter.compute_ranking_icons( war, ContextType.WAR, war_id, scores ) for obj in war.get_objectives_used_as_maj_or_min(): - objective_icon_maps[obj.id] = RankingIcon.compute_icons( + objective_icon_maps[obj.id] = Presenter.compute_ranking_icons( war, ContextType.WAR, war.id, @@ -171,7 +171,7 @@ class WarController: for pid in active ] counters = [war.get_influence_tokens(pid) for pid in active] - data = TiePresenter.build_dialog_data( + data = Presenter.build_dialog_data( war, ctx, ) diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py index fed5234..6371118 100644 --- a/src/warchron/model/pairing.py +++ b/src/warchron/model/pairing.py @@ -16,7 +16,7 @@ from warchron.model.round import Round from warchron.model.battle import Battle from warchron.model.score_service import ScoreService from warchron.model.tie_manager import TieResolver, TieContext -from warchron.model.war_event import TieResolved, InfluenceSpent +from warchron.model.war_event import TieResolved from warchron.model.score_service import ParticipantScore ResolveTiesCallback = Callable[ @@ -304,28 +304,6 @@ class Pairing: return AllocationType.SECONDARY return AllocationType.FALLBACK - @staticmethod - def participant_spent_token( - war: War, - round_id: str, - sector_id: str | None, - war_participant_id: str, - ) -> bool: - if sector_id is None: - return False - for ev in war.events: - if not isinstance(ev, InfluenceSpent): - continue - if ev.context_type != ContextType.CHOICE: - continue - if ev.context_id != round_id: - continue - if ev.sector_id != sector_id: - continue - if ev.participant_id == war_participant_id: - return True - return False - @staticmethod def get_round_allocation( war: War, @@ -338,14 +316,16 @@ class Pairing: raise DomainError(f"No campaign found for round {round.id}") war_pid = campaign.campaign_to_war_part_id(campaign_participant_id) - token_priority = Pairing.participant_spent_token( + token_priority = TieResolver.participant_spent_token( war, + ContextType.CHOICE, round.id, choice.priority_sector_id, war_pid, ) - token_secondary = Pairing.participant_spent_token( + token_secondary = TieResolver.participant_spent_token( war, + ContextType.CHOICE, round.id, choice.secondary_sector_id, war_pid, diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py index 86b279d..522e005 100644 --- a/src/warchron/model/round.py +++ b/src/warchron/model/round.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, TYPE_CHECKING if TYPE_CHECKING: from warchron.model.campaign import Campaign from warchron.model.war import War + from warchron.constants import ContextType from warchron.model.exception import ( ForbiddenOperation, DomainError, @@ -192,7 +193,7 @@ class Round: victory_condition: str | None, comment: str | None, ) -> None: - from warchron.model.pairing import Pairing + from warchron.model.tie_manager import TieResolver if self.is_over: raise ForbiddenOperation("Can't update battle in a closed round.") @@ -226,8 +227,9 @@ class Round: if ( player and self.has_choice_with_participant(player) - and Pairing.participant_spent_token( + and TieResolver.participant_spent_token( self.war, + ContextType.CHOICE, self.id, sector_id, self.campaign.campaign_to_war_part_id(player), diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index 0d416eb..9605887 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -486,3 +486,26 @@ class TieResolver: continue return True return False + + @staticmethod + def participant_spent_token( + war: War, + context_type: ContextType, + context_id: str, + sector_id: str | None, + war_participant_id: str, + ) -> bool: + if context_type == ContextType.CHOICE and sector_id is None: + return False + for ev in war.events: + if not isinstance(ev, InfluenceSpent): + continue + if ev.context_type != context_type: + continue + if ev.context_id != context_id: + continue + if ev.sector_id != sector_id: + continue + if ev.participant_id == war_participant_id: + return True + return False diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 53fab4b..80ac522 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -599,12 +599,6 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): def show_round_details(self, *, index: int | None) -> None: self.roundNb.setText(f"Round {index}") - def clear_round_page(self) -> None: - choices_table = self.choicesTable - choices_table.clearContents() - battles_table = self.battlesTable - battles_table.clearContents() - def display_round_choices(self, participants: List[ChoiceDTO]) -> None: table = self.choicesTable table.setSortingEnabled(False)