From 23110383c273f9750b0c6eb8f20460c99f2b38d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Wed, 18 Feb 2026 11:15:53 +0100 Subject: [PATCH] display battle tie-break token --- src/warchron/constants.py | 86 +++++++++++++----- src/warchron/controller/dtos.py | 2 + src/warchron/controller/round_controller.py | 28 +++++- src/warchron/model/tie_manager.py | 15 +++ .../view/resources/balance-unbalance.png | Bin 0 -> 806 bytes src/warchron/view/resources/point.png | Bin 0 -> 832 bytes src/warchron/view/resources/points.png | Bin 0 -> 754 bytes src/warchron/view/tie_dialog.py | 22 ++++- src/warchron/view/view.py | 3 +- 9 files changed, 127 insertions(+), 29 deletions(-) create mode 100644 src/warchron/view/resources/balance-unbalance.png create mode 100644 src/warchron/view/resources/point.png create mode 100644 src/warchron/view/resources/points.png diff --git a/src/warchron/constants.py b/src/warchron/constants.py index d4f69e9..7c28c79 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -4,7 +4,7 @@ from pathlib import Path from typing import Dict from PyQt6.QtCore import Qt -from PyQt6.QtGui import QIcon +from PyQt6.QtGui import QIcon, QPixmap, QPainter # Paths @@ -17,37 +17,44 @@ ROLE_ID = Qt.ItemDataRole.UserRole + 1 class IconName(str, Enum): - UNDO = ("undo",) - REDO = ("redo",) - PAIRING = ("pairing",) - DRAW = ("draw",) - DELETE = ("delete",) - SAVE_AS = ("save_as",) - SAVE = ("save",) - NEW = ("new",) - EXIT = ("exit",) - END = ("end",) - OPEN = ("load",) - ONGOING = ("ongoing",) - EXPORT = ("export",) - EDIT = ("edit",) - ADD = ("add",) - ABOUT = ("about",) - WARS = ("wars",) - DONE = ("done",) - WIN = ("win",) - PLAYERS = ("players",) + UNDO = "undo" + REDO = "redo" + PAIRING = "pairing" + DRAW = "draw" + TIEBREAK = "tie-break" + DELETE = "delete" + SAVE_AS = "save_as" + SAVE = "save" + NEW = "new" + EXIT = "exit" + END = "end" + OPEN = "load" + ONGOING = "ongoing" + EXPORT = "export" + EDIT = "edit" + ADD = "add" + ABOUT = "about" + WARS = "wars" + DONE = "done" + WIN = "win" + PLAYERS = "players" WARCHRON = "warchron" + TOKEN = "token" + TOKENS = "tokens" + TIEBREAK_TOKEN = auto() class Icons: - _cache: Dict[str, QIcon] = {} + _icon_cache: Dict[IconName, QIcon] = {} + + _pixmap_cache: Dict[IconName, QPixmap] = {} _paths = { IconName.UNDO: "arrow-curve-180-left", IconName.REDO: "arrow-curve", IconName.PAIRING: "arrow-switch", IconName.DRAW: "balance.png", + IconName.TIEBREAK: "balance-unbalance.png", IconName.DELETE: "cross.png", IconName.SAVE_AS: "disk--pencil.png", IconName.SAVE: "disk.png", @@ -65,14 +72,43 @@ class Icons: IconName.WIN: "trophy.png", IconName.PLAYERS: "users.png", IconName.WARCHRON: "warchron_logo.png", + IconName.TOKEN: "point.png", + IconName.TOKENS: "points.png", } @classmethod def get(cls, name: IconName) -> QIcon: - if name not in cls._cache: + if name not in cls._icon_cache: path = RESOURCES_DIR / cls._paths[name] - cls._cache[name] = QIcon(str(path)) - return cls._cache[name] + cls._icon_cache[name] = QIcon(str(path)) + return cls._icon_cache[name] + + @classmethod + def get_pixmap(cls, name: IconName) -> QPixmap: + if name in cls._pixmap_cache: + return cls._pixmap_cache[name] + if name == IconName.TIEBREAK_TOKEN: + pix = cls._compose( + cls.get_pixmap(IconName.TIEBREAK), + cls.get_pixmap(IconName.TOKEN), + ) + else: + path = RESOURCES_DIR / cls._paths[name] + pix = QPixmap(path.as_posix()) + cls._pixmap_cache[name] = pix + return pix + + @staticmethod + def _compose(left: QPixmap, right: QPixmap) -> QPixmap: + w = left.width() + right.width() + h = max(left.height(), right.height()) + 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) + painter.end() + return result class ItemType(StrEnum): diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py index 454aa61..5222b03 100644 --- a/src/warchron/controller/dtos.py +++ b/src/warchron/controller/dtos.py @@ -105,6 +105,8 @@ class BattleDTO: state_icon: QIcon | None player1_icon: QIcon | None player2_icon: QIcon | None + player1_tooltip: str | None = None + player2_tooltip: str | None = None @dataclass diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 429d5ec..d0c8601 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -2,9 +2,11 @@ from typing import List, Dict, TYPE_CHECKING from PyQt6.QtWidgets import QDialog from PyQt6.QtWidgets import QMessageBox +from PyQt6.QtGui import QIcon -from warchron.constants import ItemType, RefreshScope, Icons, IconName +from warchron.constants import ItemType, RefreshScope, Icons, IconName, ContextType from warchron.model.exception import ForbiddenOperation, DomainError +from warchron.model.tie_manager import TieResolver from warchron.model.round import Round from warchron.model.war import War @@ -31,6 +33,7 @@ class RoundController: def _fill_round_details(self, round_id: str) -> None: 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) self.app.view.show_round_details(index=camp.get_round_index(round_id)) participants = self.app.model.get_round_participants(round_id) sectors = camp.get_sectors_in_round(round_id) @@ -97,9 +100,29 @@ class RoundController: winner_name = "" p1_icon = None p2_icon = None + p1_tooltip = None + p2_tooltip = None if battle.is_draw(): p1_icon = Icons.get(IconName.DRAW) p2_icon = Icons.get(IconName.DRAW) + if TieResolver.was_tie_broken_by_tokens( + war, ContextType.BATTLE, battle.sector_id + ): + effective_winner = TieResolver.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.participants[ + battle.player_1_id + ].war_participant_id + pixmap = Icons.get_pixmap(IconName.TIEBREAK_TOKEN) + if effective_winner == p1_war: + p1_icon = QIcon(pixmap) + p1_tooltip = "Won by tie-break" + else: + p2_icon = QIcon(pixmap) + p2_tooltip = "Won by tie-break" elif battle.winner_id: if battle.winner_id == battle.player_1_id: p1_icon = Icons.get(IconName.WIN) @@ -118,6 +141,8 @@ class RoundController: state_icon=state_icon, player1_icon=p1_icon, player2_icon=p2_icon, + player1_tooltip=p1_tooltip, + player2_tooltip=p2_tooltip, ) ) self.app.view.display_round_battles(battles_for_display) @@ -168,6 +193,7 @@ class RoundController: parent=self.app.view, players=players, counters=counters, + context_type=ContextType.BATTLE, context_id=ctx.context_id, ) if not dialog.exec(): diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index da031f9..54cef74 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -106,3 +106,18 @@ class TieResolver: return ev.participant_id # None if confirmed draw return None + + @staticmethod + def was_tie_broken_by_tokens( + war: War, + context_type: ContextType, + context_id: str, + ) -> bool: + for ev in reversed(war.events): + if ( + isinstance(ev, TieResolved) + and ev.context_type == context_type + and ev.context_id == context_id + ): + return ev.participant_id is not None + return False diff --git a/src/warchron/view/resources/balance-unbalance.png b/src/warchron/view/resources/balance-unbalance.png new file mode 100644 index 0000000000000000000000000000000000000000..9e52d6229737e0248881954de5140357562da98d GIT binary patch literal 806 zcmV+>1KIqEP)gn?YHjgTAQ2M zsu2DVDMVV3_*#UJ1PMhDgb_qh6d{xqL_|d|J_v#dJ%pANM7=~0g&_2j;ZqT)Nrq;w zDQd3ku6}n3fK-H6?U>jwe(-$^%HF~ZpNT- zhZ!ysexiqte(npdD^16?kh#~{kL38p$I7Syy=yzIAi0_@NHfnvL^HFs(S~hVzQu)U zI4=C#Lg~&_Q&q*PgL>}ULjmzKO+fZ29~_{()6D*oinRU5wrz7IF{c%n@sxt!M9uko zJ;!E$jUXr|@v6J`U7EmCsxh2v^i`D#j7+J%cuF=x;i>1jrYRh_Qu#d=FlAw)?RxV- zw&F}UktT@biz-U0q_q736D73Av_nc|1al-GHj#xLjw>p$ibdF^LK@3lwWhdBe#Lp1Yy1g1(LoUahd z73ge#-rv%r&M-DbEOSylU19yltwiC#u$q}~x%n-!BN_-7MA#J9?>R}CN-o*)#klo# kRLwB1n3&6uc>e?#0L=?oW$M{M^8f$<07*qoM6N<$f{y`kVE_OC literal 0 HcmV?d00001 diff --git a/src/warchron/view/resources/point.png b/src/warchron/view/resources/point.png new file mode 100644 index 0000000000000000000000000000000000000000..1776e06eed097f0f7353573d03b4f0d00bbba157 GIT binary patch literal 832 zcmV-G1Hb%k7RCwB?lgm#NQ543%ndys8TiU^q zSA_Dg3L-BfLL|lo5|MLe?WIG+_QFvx=?q%9|RY;FdYgBd^5R|`#bZUbI)Zg%fkPJjXvJrv4LZ8BoZfxhKWuQxy$=hqIIH= zL|Y{jpt^F`_?h63xax5GM;p6Gd&SUDhs$%UhJGx_sr3HFLS*Ob+<8rv=E$enzZ{g~ zn#SlW_Wc2n3r@)6xRB5I{N}$NtWqc=FPXNygzFr%c6xZdfC&T_e2=$BXh_ z9AYqt&2ShcO@mo70ER_FYYHP#ayA7#$44I4sT`S`0 z%rvTaH!@Nhc~ydC>R>t6Pnk-Bf)1CaWSLy<5K2x$Iu(biD#$BYj9#BYbbSS-QjyfE zfaE8T!BD2sgQ9L}QX(pItXaLk8zW8C0E?Wl-GSGS9)Mwh_GTZPoCe;-qNFP_WhxFd z9m|`MrDToZft1{am#>1&#$fizLpaF$V!w#STAt+8AXGcB9g8L@Q*oe`@O*W7X)M^@ z*5Kngz9=7Hpsx{bhXp%lky!w8iUF^ig~?hIRSOzA@cP!qiZOHu{uA_(sg;vvk3{| zg$>LuN3lZI86t(SrC$k1pWz^KmSz9P?KSA3(UYeGOjz@W00RK`6HuqDQZDcS0000< KMNUMnLSTY?fQZNd literal 0 HcmV?d00001 diff --git a/src/warchron/view/resources/points.png b/src/warchron/view/resources/points.png new file mode 100644 index 0000000000000000000000000000000000000000..d51781112332bd4c3707bb3b759091824915a427 GIT binary patch literal 754 zcmVGaTv$H=bY{2W+s+1 zr->8M=tWHeQ`kZ&k-P|_ZmgT=Ch8yPG9kL{GVrFC3z0CQ$g&rls1=S9aV45sI#*lM zsb|e?XXosk({oN6*aak*;wpZtXHL7q^06%l)$+L?RoI zBoR;U-GRkwL6qhy7<(d#gZf2NE?!klnZ?K|x8QjJH5P^#`;CtCm$9-qL)0=@Ey=_s zSPTP6*^w8OjhzrS`XNR_7=7agOL^8f_8=MG5aLNKXc%Pkwsv(vQBn}XVR$Fr<3LRV zJ`DDOx0Io!(FTU+e8=(tXwc5Io1g zb-V<2d%pZ>Vse?<`cX#%6G*r6Dk+*VU{2M6X*+`JC8f5juC}I%(*1=63}q5p-HeWn zO!`B?aPP None: super().__init__(parent) @@ -22,7 +24,13 @@ class TieDialog(QDialog): self._p2_id = players[1].id self.ui: Ui_tieDialog = Ui_tieDialog() self.ui.setupUi(self) # type: ignore - self.ui.tieContext.setText("Battle tie") # Change with context + self.ui.tieContext.setText(self._get_context_title(context_type)) + icon_path = (RESOURCES_DIR / Icons._paths[IconName.TOKENS]).as_posix() + html = f' Remaining token(s)' + self.ui.label_2.setText(html) + self.ui.label_2.setTextFormat(Qt.TextFormat.RichText) + self.ui.label_3.setText(html) + self.ui.label_3.setTextFormat(Qt.TextFormat.RichText) self.ui.groupBox_1.setTitle(players[0].name) self.ui.groupBox_2.setTitle(players[1].name) self.ui.tokenCount_1.setText(str(counters[0])) @@ -38,3 +46,13 @@ class TieDialog(QDialog): self._p1_id: self.ui.tokenSpend_1.isChecked(), self._p2_id: self.ui.tokenSpend_2.isChecked(), } + + @staticmethod + def _get_context_title(context_type: ContextType) -> str: + titles = { + ContextType.BATTLE: "Battle tie", + ContextType.CAMPAIGN: "Campaign tie", + ContextType.WAR: "War tie", + ContextType.CHOICE: "Choice tie", + } + return titles.get(context_type, "Tie") diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 15e1e29..5082df5 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -4,7 +4,7 @@ from pathlib import Path import calendar from PyQt6 import QtWidgets -from PyQt6.QtCore import Qt, QPoint +from PyQt6.QtCore import Qt, QPoint, QSize from PyQt6.QtWidgets import QWidget, QFileDialog, QTreeWidgetItem, QMenu from PyQt6.QtGui import QCloseEvent @@ -540,6 +540,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): table = self.battlesTable table.clearContents() table.setRowCount(len(sectors)) + self.battlesTable.setIconSize(QSize(32, 16)) for row, battle in enumerate(sectors): sector_item = QtWidgets.QTableWidgetItem(battle.sector_name) if battle.state_icon: