From f55106c260874a1243cf0548fd0b4c9b65b7daa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Tue, 3 Mar 2026 15:39:30 +0100 Subject: [PATCH] display objective awards in participant tables --- src/warchron/constants.py | 65 ++++++++++++++++++- .../controller/campaign_controller.py | 22 ++++++- src/warchron/controller/dtos.py | 4 +- src/warchron/controller/ranking_icon.py | 34 ++++++++-- src/warchron/controller/war_controller.py | 25 ++++++- src/warchron/model/result_checker.py | 23 ++++--- src/warchron/model/tie_manager.py | 7 +- src/warchron/view/view.py | 4 ++ 8 files changed, 160 insertions(+), 24 deletions(-) diff --git a/src/warchron/constants.py b/src/warchron/constants.py index 37c4a01..241ff64 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -64,15 +64,30 @@ class IconName(str, Enum): VPNTHDRAW = auto() VPNTHBREAK = auto() VPNTHTIEDRAW = auto() + NP1STDRAW = auto() + NP1STBREAK = auto() + NP1STTIEDRAW = auto() + NP2NDDRAW = auto() + NP2NDBREAK = auto() + NP2NDTIEDRAW = auto() + NP3RDDRAW = auto() + NP3RDBREAK = auto() + NP3RDTIEDRAW = auto() -RANK_TO_ICON = { +VP_RANK_TO_ICON = { 1: IconName.VP1ST, 2: IconName.VP2ND, 3: IconName.VP3RD, 4: IconName.VPNTH, } +NP_RANK_TO_ICON = { + 1: IconName.NP1ST, + 2: IconName.NP2ND, + 3: IconName.NP3RD, +} + class Icons: _icon_cache: Dict[IconName, QIcon] = {} @@ -194,6 +209,54 @@ class Icons: cls.get_pixmap(IconName.DRAW), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.NP1STDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.NP1ST), + cls.get_pixmap(IconName.DRAW), + ) + elif name == IconName.NP1STBREAK: + pix = cls._compose( + cls.get_pixmap(IconName.NP1ST), + cls.get_pixmap(IconName.TOKEN), + ) + elif name == IconName.NP1STTIEDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.NP1ST), + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) + elif name == IconName.NP2NDDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.NP2ND), + cls.get_pixmap(IconName.DRAW), + ) + elif name == IconName.NP2NDBREAK: + pix = cls._compose( + cls.get_pixmap(IconName.NP2ND), + cls.get_pixmap(IconName.TOKEN), + ) + elif name == IconName.NP2NDTIEDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.NP2ND), + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) + elif name == IconName.NP3RDDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.NP3RD), + cls.get_pixmap(IconName.DRAW), + ) + elif name == IconName.NP3RDBREAK: + pix = cls._compose( + cls.get_pixmap(IconName.NP3RD), + cls.get_pixmap(IconName.TOKEN), + ) + elif name == IconName.NP3RDTIEDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.NP3RD), + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) else: path = RESOURCES_DIR / cls._paths[name] pix = QPixmap(path.as_posix()) diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index e0ff8b2..00ffa3d 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -1,6 +1,7 @@ from typing import List, Dict, Tuple, TYPE_CHECKING from PyQt6.QtWidgets import QMessageBox, QDialog +from PyQt6.QtGui import QIcon from warchron.constants import ( RefreshScope, @@ -57,16 +58,30 @@ class CampaignController: self.app.view.display_campaign_sectors(sectors_for_display) scores = ScoreService.compute_scores(war, ContextType.CAMPAIGN, campaign_id) rows: List[CampaignParticipantScoreDTO] = [] - icon_map = {} + vp_icon_map: Dict[str, QIcon] = {} + objective_icon_maps: Dict[str, Dict[str, QIcon]] = {} if camp.is_over: - icon_map = RankingIcon.compute_icons( + vp_icon_map = RankingIcon.compute_icons( war, ContextType.CAMPAIGN, campaign_id, scores ) + for obj in war.get_all_objectives(): + objective_icon_maps[obj.id] = RankingIcon.compute_icons( + war, + ContextType.CAMPAIGN, + f"{camp.id}:{obj.id}", + scores, + objective_id=obj.id, + ) for camp_part in camp.get_all_campaign_participants(): war_part_id = camp_part.war_participant_id war_part = war.get_war_participant(war_part_id) player_name = self.app.model.get_player_name(war_part.player_id) score = scores[war_part_id] + objective_icons = { + obj_id: icon_map[war_part_id] + for obj_id, icon_map in objective_icon_maps.items() + if war_part_id in icon_map + } rows.append( CampaignParticipantScoreDTO( campaign_participant_id=camp_part.id, @@ -77,7 +92,8 @@ class CampaignController: victory_points=score.victory_points, narrative_points=dict(score.narrative_points), tokens=war.get_influence_tokens(war_part.id), - rank_icon=icon_map.get(war_part_id), + rank_icon=vp_icon_map.get(war_part_id), + objective_icons=objective_icons, ) ) objectives = [ diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py index 84bc0c4..9451bc6 100644 --- a/src/warchron/controller/dtos.py +++ b/src/warchron/controller/dtos.py @@ -1,5 +1,5 @@ from typing import List, Dict -from dataclasses import dataclass +from dataclasses import dataclass, field from PyQt6.QtGui import QIcon @@ -118,6 +118,7 @@ class CampaignParticipantScoreDTO: narrative_points: Dict[str, int] tokens: int rank_icon: QIcon | None = None + objective_icons: Dict[str, QIcon] = field(default_factory=dict) @dataclass(frozen=True, slots=True) @@ -130,3 +131,4 @@ class WarParticipantScoreDTO: narrative_points: Dict[str, int] tokens: int rank_icon: QIcon | None = None + objective_icons: Dict[str, QIcon] = field(default_factory=dict) diff --git a/src/warchron/controller/ranking_icon.py b/src/warchron/controller/ranking_icon.py index 06f1b3f..27024d5 100644 --- a/src/warchron/controller/ranking_icon.py +++ b/src/warchron/controller/ranking_icon.py @@ -6,7 +6,8 @@ from warchron.constants import ( ContextType, Icons, IconName, - RANK_TO_ICON, + VP_RANK_TO_ICON, + NP_RANK_TO_ICON, ) from warchron.model.war import War from warchron.model.score_service import ParticipantScore @@ -20,16 +21,37 @@ class RankingIcon: 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 + else: + + def value_getter(score: ParticipantScore) -> int: + return score.narrative_points.get(objective_id, 0) + + icon_ranking = NP_RANK_TO_ICON ranking = ResultChecker.get_effective_ranking( - war, context_type, context_id, scores + war, context_type, context_id, scores, value_getter=value_getter ) - icon_map = {} + icon_map: Dict[str, QIcon] = {} for rank, group, token_map in ranking: - base_icon = RANK_TO_ICON.get(rank, IconName.VPNTH) - vp = scores[group[0]].victory_points + 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 s.victory_points == vp + 1 for s in scores.values() if value_getter(s) == value ) for pid in group: spent = token_map.get(pid, 0) diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py index dd516ae..ddec9ff 100644 --- a/src/warchron/controller/war_controller.py +++ b/src/warchron/controller/war_controller.py @@ -1,6 +1,7 @@ from typing import List, Tuple, TYPE_CHECKING, Dict from PyQt6.QtWidgets import QMessageBox, QDialog +from PyQt6.QtGui import QIcon from warchron.constants import ( RefreshScope, @@ -53,12 +54,28 @@ class WarController: self.app.view.display_war_objectives(objectives_for_display) scores = ScoreService.compute_scores(war, ContextType.WAR, war.id) rows: List[WarParticipantScoreDTO] = [] - icon_map = {} + vp_icon_map: dict[str, QIcon] = {} + objective_icon_maps: Dict[str, Dict[str, QIcon]] = {} if war.is_over: - icon_map = RankingIcon.compute_icons(war, ContextType.WAR, war_id, scores) + vp_icon_map = RankingIcon.compute_icons( + war, ContextType.WAR, war_id, scores + ) + for obj in war.get_all_objectives(): + objective_icon_maps[obj.id] = RankingIcon.compute_icons( + war, + ContextType.WAR, + f"{war.id}:{obj.id}", + scores, + objective_id=obj.id, + ) for war_part in war.get_all_war_participants(): player_name = self.app.model.get_player_name(war_part.player_id) score = scores[war_part.id] + objective_icons = { + obj_id: icon_map[war_part.id] + for obj_id, icon_map in objective_icon_maps.items() + if war_part.id in icon_map + } rows.append( WarParticipantScoreDTO( war_participant_id=war_part.id, @@ -68,7 +85,8 @@ class WarController: victory_points=score.victory_points, narrative_points=dict(score.narrative_points), tokens=war.get_influence_tokens(war_part.id), - rank_icon=icon_map.get(war_part.id), + rank_icon=vp_icon_map.get(war_part.id), + objective_icons=objective_icons, ) ) self.app.view.display_war_participants(rows, objectives_for_display) @@ -133,6 +151,7 @@ class WarController: RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id ) + # FIXME tie dialog with all participant even without tie def resolve_ties( self, war: War, contexts: List[TieContext] ) -> Dict[Tuple[ContextType, str, int | None], Dict[str, bool]]: diff --git a/src/warchron/model/result_checker.py b/src/warchron/model/result_checker.py index 6430ef6..b06ecb8 100644 --- a/src/warchron/model/result_checker.py +++ b/src/warchron/model/result_checker.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import List, Tuple, Dict, TYPE_CHECKING +from typing import List, Tuple, Dict, TYPE_CHECKING, Callable from collections import defaultdict from warchron.constants import ContextType @@ -36,15 +36,16 @@ class ResultChecker: context_type: ContextType, context_id: str, scores: Dict[str, ParticipantScore], + value_getter: Callable[[ParticipantScore], int], ) -> List[Tuple[int, List[str], Dict[str, int]]]: - vp_buckets: Dict[int, List[str]] = defaultdict(list) + buckets: Dict[int, List[str]] = defaultdict(list) for pid, score in scores.items(): - vp_buckets[score.victory_points].append(pid) - sorted_vps = sorted(vp_buckets.keys(), reverse=True) + buckets[value_getter(score)].append(pid) + sorted_vps = sorted(buckets.keys(), reverse=True) ranking: List[Tuple[int, List[str], Dict[str, int]]] = [] current_rank = 1 - for vp in sorted_vps: - participants = vp_buckets[vp] + for value in sorted_vps: + participants = buckets[value] if context_type == ContextType.WAR and len(participants) > 1: subgroups = ResultChecker._secondary_sorting_war(war, participants) for subgroup in subgroups: @@ -57,7 +58,7 @@ class ResultChecker: continue # normal tie-break if tie persists if not TieResolver.is_tie_resolved( - war, context_type, context_id, vp + war, context_type, context_id, value ): ranking.append( (current_rank, subgroup, {pid: 0 for pid in subgroup}) @@ -80,7 +81,7 @@ class ResultChecker: continue # no tie if len(participants) == 1 or not TieResolver.is_tie_resolved( - war, context_type, context_id, vp + war, context_type, context_id, value ): ranking.append( (current_rank, participants, {pid: 0 for pid in participants}) @@ -118,7 +119,11 @@ class ResultChecker: war, ContextType.CAMPAIGN, campaign.id ) ranking = ResultChecker.get_effective_ranking( - war, ContextType.CAMPAIGN, campaign.id, scores + war, + ContextType.CAMPAIGN, + campaign.id, + scores, + lambda s: s.victory_points, ) for rank, group, _ in ranking: if pid in group: diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index 028001e..13a220b 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -146,7 +146,11 @@ class TieResolver: scores = ScoreService.compute_scores(war, ContextType.WAR, war.id) ranking = ResultChecker.get_effective_ranking( - war, ContextType.WAR, war.id, scores + war, + ContextType.WAR, + war.id, + scores, + value_getter=lambda s: s.victory_points, ) ties: List[TieContext] = [] for _, group, _ in ranking: @@ -191,6 +195,7 @@ class TieResolver: ContextType.OBJECTIVE, f"{war.id}:{objective_id}", scores, + value_getter=lambda s: s.narrative_points.get(objective_id, 0), ) ties: List[TieContext] = [] for _, group, _ in ranking: diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 012c2bf..160719a 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -413,6 +413,8 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): for obj in objectives: value = part.narrative_points.get(obj.id, 0) NP_item = QtWidgets.QTableWidgetItem(str(value)) + if part.objective_icons.get(obj.id): + NP_item.setIcon(part.objective_icons[obj.id]) table.setItem(row, col, NP_item) col += 1 table.setItem(row, col, token_item) @@ -545,6 +547,8 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): for obj in objectives: value = part.narrative_points.get(obj.id, 0) NP_item = QtWidgets.QTableWidgetItem(str(value)) + if part.objective_icons.get(obj.id): + NP_item.setIcon(part.objective_icons[obj.id]) table.setItem(row, col, NP_item) col += 1 table.setItem(row, col, token_item)