From d766befd312b82783ce4f0b5ff700ef3920c21c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Mon, 23 Feb 2026 19:28:13 +0100 Subject: [PATCH] fix icon mapping in campaign ranking --- src/warchron/constants.py | 68 +++++++++++++++---- .../controller/campaign_controller.py | 58 +++++++--------- src/warchron/model/result_checker.py | 25 ++++--- src/warchron/model/tie_manager.py | 18 +++++ src/warchron/view/view.py | 2 +- 5 files changed, 113 insertions(+), 58 deletions(-) diff --git a/src/warchron/constants.py b/src/warchron/constants.py index 948a97b..ead7600 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -50,13 +50,25 @@ class IconName(str, Enum): NP3RD = "np3rd" TIEBREAK_TOKEN = auto() VP1STDRAW = auto() - VP1STTIEBREAK = auto() + VP1STBREAK = auto() + VP1STTIEDRAW = auto() VP2NDDRAW = auto() - VP2NDTIEBREAK = auto() + VP2NDBREAK = auto() + VP2NDTIEDRAW = auto() VP3RDDRAW = auto() - VP3RDTIEBREAK = auto() + VP3RDBREAK = auto() + VP3RDTIEDRAW = auto() VPNTHDRAW = auto() - VPNTHTIEBREAK = auto() + VPNTHBREAK = auto() + VPNTHTIEDRAW = auto() + + +RANK_TO_ICON = { + 1: IconName.VP1ST, + 2: IconName.VP2ND, + 3: IconName.VP3RD, + 4: IconName.VPNTH, +} class Icons: @@ -119,41 +131,65 @@ class Icons: cls.get_pixmap(IconName.VP1ST), cls.get_pixmap(IconName.DRAW), ) - elif name == IconName.VP1STTIEBREAK: + elif name == IconName.VP1STBREAK: pix = cls._compose( cls.get_pixmap(IconName.VP1ST), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.VP1STTIEDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.VP1ST), + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) elif name == IconName.VP2NDDRAW: pix = cls._compose( cls.get_pixmap(IconName.VP2ND), cls.get_pixmap(IconName.DRAW), ) - elif name == IconName.VP2NDTIEBREAK: + elif name == IconName.VP2NDBREAK: pix = cls._compose( cls.get_pixmap(IconName.VP2ND), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.VP2NDTIEDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.VP2ND), + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) elif name == IconName.VP3RDDRAW: pix = cls._compose( cls.get_pixmap(IconName.VP3RD), cls.get_pixmap(IconName.DRAW), ) - elif name == IconName.VP3RDTIEBREAK: + elif name == IconName.VP3RDBREAK: pix = cls._compose( cls.get_pixmap(IconName.VP3RD), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.VP3RDTIEDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.VP3RD), + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) elif name == IconName.VPNTHDRAW: pix = cls._compose( cls.get_pixmap(IconName.VPNTH), cls.get_pixmap(IconName.DRAW), ) - elif name == IconName.VPNTHTIEBREAK: + elif name == IconName.VPNTHBREAK: pix = cls._compose( cls.get_pixmap(IconName.VPNTH), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.VPNTHTIEDRAW: + pix = cls._compose( + cls.get_pixmap(IconName.VPNTH), + cls.get_pixmap(IconName.DRAW), + cls.get_pixmap(IconName.TOKEN), + ) else: path = RESOURCES_DIR / cls._paths[name] pix = QPixmap(path.as_posix()) @@ -161,14 +197,20 @@ class Icons: return pix @staticmethod - def _compose(left: QPixmap, right: QPixmap) -> QPixmap: - w = left.width() + right.width() - h = max(left.height(), right.height()) + def _compose(*pixmaps: QPixmap) -> QPixmap: + if not pixmaps: + return QPixmap() + if len(pixmaps) == 1: + return pixmaps[0] + w = sum(p.width() for p in pixmaps) + h = max(p.height() for p in pixmaps) result = QPixmap(w, h) result.fill(Qt.GlobalColor.transparent) painter = QPainter(result) - painter.drawPixmap(0, (h - left.height()) // 2, left) - painter.drawPixmap(left.width(), (h - right.height()) // 2, right) + x = 0 + for p in pixmaps: + painter.drawPixmap(x, (h - p.height()) // 2, p) + x += p.width() painter.end() return result diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py index dcf4d34..89805db 100644 --- a/src/warchron/controller/campaign_controller.py +++ b/src/warchron/controller/campaign_controller.py @@ -3,7 +3,14 @@ from typing import List, Dict, TYPE_CHECKING from PyQt6.QtWidgets import QMessageBox, QDialog from PyQt6.QtGui import QIcon -from warchron.constants import RefreshScope, ContextType, ItemType, Icons, IconName +from warchron.constants import ( + RefreshScope, + ContextType, + ItemType, + Icons, + IconName, + RANK_TO_ICON, +) if TYPE_CHECKING: from warchron.controller.app_controller import AppController @@ -45,40 +52,25 @@ class CampaignController: war, ContextType.CAMPAIGN, campaign.id, scores ) icon_map = {} - for rank, group in ranking: - vp = scores[group[0]].victory_points - tie_id = f"{campaign.id}:score:{vp}" - is_tie = len(group) > 1 - broken = TieResolver.was_tie_broken_by_tokens( - war, - ContextType.CAMPAIGN, - tie_id, + for rank, group, token_map in ranking: + base_icon = RANK_TO_ICON.get(rank, IconName.VPNTH) + tie_id = f"{campaign.id}:score:{scores[group[0]].victory_points}" + tie_resolved = TieResolver.is_tie_resolved( + war, ContextType.CAMPAIGN, tie_id ) - # choose icon name - if rank == 1: - base = IconName.VP1ST - draw = IconName.VP1STDRAW - tb = IconName.VP1STTIEBREAK - elif rank == 2: - base = IconName.VP2ND - draw = IconName.VP2NDDRAW - tb = IconName.VP2NDTIEBREAK - elif rank == 3: - base = IconName.VP3RD - draw = IconName.VP3RDDRAW - tb = IconName.VP3RDTIEBREAK - else: - base = IconName.VPNTH - draw = IconName.VPNTHDRAW - tb = IconName.VPNTHTIEBREAK - if not is_tie: - icon = Icons.get(base) - elif not broken: - icon = QIcon(Icons.get_pixmap(draw)) - else: - icon = QIcon(Icons.get_pixmap(tb)) for pid in group: - icon_map[pid] = icon + spent = token_map.get(pid, 0) + if not tie_resolved and spent == 0: + icon_name = getattr(IconName, f"{base_icon.name}DRAW") + elif tie_resolved and spent == 0 and len(group) > 1: + icon_name = getattr(IconName, f"{base_icon.name}DRAW") + elif tie_resolved and spent > 0 and len(group) == 1: + icon_name = getattr(IconName, f"{base_icon.name}BREAK") + elif tie_resolved and spent > 0 and len(group) > 1: + icon_name = getattr(IconName, f"{base_icon.name}TIEDRAW") + else: + icon_name = base_icon + icon_map[pid] = QIcon(Icons.get_pixmap(icon_name)) return icon_map def _fill_campaign_details(self, campaign_id: str) -> None: diff --git a/src/warchron/model/result_checker.py b/src/warchron/model/result_checker.py index 5556dc7..978b0ce 100644 --- a/src/warchron/model/result_checker.py +++ b/src/warchron/model/result_checker.py @@ -37,24 +37,23 @@ class ResultChecker: context_type: ContextType, context_id: str, scores: Dict[str, ParticipantScore], - ) -> List[Tuple[int, List[str]]]: + ) -> List[Tuple[int, List[str], Dict[str, int]]]: vp_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) - ranking: List[Tuple[int, List[str]]] = [] + ranking: List[Tuple[int, List[str], Dict[str, int]]] = [] current_rank = 1 for vp in sorted_vps: participants = vp_buckets[vp] - # no tie - if len(participants) == 1: - ranking.append((current_rank, participants)) - current_rank += 1 - continue tie_id = f"{context_id}:score:{vp}" - # tie unresolved → shared rank (theoretically impossible) - if not TieResolver.is_tie_resolved(war, context_type, tie_id): - ranking.append((current_rank, participants)) + # no tie + if len(participants) == 1 or not TieResolver.is_tie_resolved( + war, context_type, tie_id + ): + ranking.append( + (current_rank, participants, {pid: 0 for pid in participants}) + ) current_rank += 1 continue # apply token ranking @@ -64,7 +63,11 @@ class ResultChecker: tie_id, participants, ) + tokens_spent = TieResolver.tokens_spent_map( + war, context_type, tie_id, participants + ) for group in groups: - ranking.append((current_rank, group)) + group_tokens = {pid: tokens_spent[pid] for pid in group} + ranking.append((current_rank, group, group_tokens)) current_rank += 1 return ranking diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index fd678d7..12b291f 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -131,6 +131,24 @@ class TieResolver: groups[-1].append(pid) return groups + @staticmethod + def tokens_spent_map( + war: War, + context_type: ContextType, + context_id: str, + participants: List[str], + ) -> Dict[str, int]: + spent = {pid: 0 for pid in participants} + for ev in war.events: + if ( + isinstance(ev, InfluenceSpent) + and ev.context_type == context_type + and ev.context_id == context_id + and ev.participant_id in spent + ): + spent[ev.participant_id] += ev.amount + return spent + @staticmethod def get_active_participants( war: War, diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index a2c5cf5..fd65300 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -475,7 +475,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): table.setColumnCount(len(headers)) table.setHorizontalHeaderLabels(headers) table.setRowCount(len(participants)) - table.setIconSize(QSize(32, 16)) + table.setIconSize(QSize(48, 16)) for row, part in enumerate(participants): name_item = QtWidgets.QTableWidgetItem(part.player_name) if part.rank_icon: