display battle tie-break token

This commit is contained in:
Maxime Réaux 2026-02-18 11:15:53 +01:00
parent 818d2886f4
commit 23110383c2
9 changed files with 127 additions and 29 deletions

View file

@ -4,7 +4,7 @@ from pathlib import Path
from typing import Dict from typing import Dict
from PyQt6.QtCore import Qt from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon from PyQt6.QtGui import QIcon, QPixmap, QPainter
# Paths # Paths
@ -17,37 +17,44 @@ ROLE_ID = Qt.ItemDataRole.UserRole + 1
class IconName(str, Enum): class IconName(str, Enum):
UNDO = ("undo",) UNDO = "undo"
REDO = ("redo",) REDO = "redo"
PAIRING = ("pairing",) PAIRING = "pairing"
DRAW = ("draw",) DRAW = "draw"
DELETE = ("delete",) TIEBREAK = "tie-break"
SAVE_AS = ("save_as",) DELETE = "delete"
SAVE = ("save",) SAVE_AS = "save_as"
NEW = ("new",) SAVE = "save"
EXIT = ("exit",) NEW = "new"
END = ("end",) EXIT = "exit"
OPEN = ("load",) END = "end"
ONGOING = ("ongoing",) OPEN = "load"
EXPORT = ("export",) ONGOING = "ongoing"
EDIT = ("edit",) EXPORT = "export"
ADD = ("add",) EDIT = "edit"
ABOUT = ("about",) ADD = "add"
WARS = ("wars",) ABOUT = "about"
DONE = ("done",) WARS = "wars"
WIN = ("win",) DONE = "done"
PLAYERS = ("players",) WIN = "win"
PLAYERS = "players"
WARCHRON = "warchron" WARCHRON = "warchron"
TOKEN = "token"
TOKENS = "tokens"
TIEBREAK_TOKEN = auto()
class Icons: class Icons:
_cache: Dict[str, QIcon] = {} _icon_cache: Dict[IconName, QIcon] = {}
_pixmap_cache: Dict[IconName, QPixmap] = {}
_paths = { _paths = {
IconName.UNDO: "arrow-curve-180-left", IconName.UNDO: "arrow-curve-180-left",
IconName.REDO: "arrow-curve", IconName.REDO: "arrow-curve",
IconName.PAIRING: "arrow-switch", IconName.PAIRING: "arrow-switch",
IconName.DRAW: "balance.png", IconName.DRAW: "balance.png",
IconName.TIEBREAK: "balance-unbalance.png",
IconName.DELETE: "cross.png", IconName.DELETE: "cross.png",
IconName.SAVE_AS: "disk--pencil.png", IconName.SAVE_AS: "disk--pencil.png",
IconName.SAVE: "disk.png", IconName.SAVE: "disk.png",
@ -65,14 +72,43 @@ class Icons:
IconName.WIN: "trophy.png", IconName.WIN: "trophy.png",
IconName.PLAYERS: "users.png", IconName.PLAYERS: "users.png",
IconName.WARCHRON: "warchron_logo.png", IconName.WARCHRON: "warchron_logo.png",
IconName.TOKEN: "point.png",
IconName.TOKENS: "points.png",
} }
@classmethod @classmethod
def get(cls, name: IconName) -> QIcon: 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] path = RESOURCES_DIR / cls._paths[name]
cls._cache[name] = QIcon(str(path)) cls._icon_cache[name] = QIcon(str(path))
return cls._cache[name] 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): class ItemType(StrEnum):

View file

@ -105,6 +105,8 @@ class BattleDTO:
state_icon: QIcon | None state_icon: QIcon | None
player1_icon: QIcon | None player1_icon: QIcon | None
player2_icon: QIcon | None player2_icon: QIcon | None
player1_tooltip: str | None = None
player2_tooltip: str | None = None
@dataclass @dataclass

View file

@ -2,9 +2,11 @@ from typing import List, Dict, TYPE_CHECKING
from PyQt6.QtWidgets import QDialog from PyQt6.QtWidgets import QDialog
from PyQt6.QtWidgets import QMessageBox 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.exception import ForbiddenOperation, DomainError
from warchron.model.tie_manager import TieResolver
from warchron.model.round import Round from warchron.model.round import Round
from warchron.model.war import War from warchron.model.war import War
@ -31,6 +33,7 @@ class RoundController:
def _fill_round_details(self, round_id: str) -> None: def _fill_round_details(self, round_id: str) -> None:
rnd = self.app.model.get_round(round_id) rnd = self.app.model.get_round(round_id)
camp = self.app.model.get_campaign_by_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)) self.app.view.show_round_details(index=camp.get_round_index(round_id))
participants = self.app.model.get_round_participants(round_id) participants = self.app.model.get_round_participants(round_id)
sectors = camp.get_sectors_in_round(round_id) sectors = camp.get_sectors_in_round(round_id)
@ -97,9 +100,29 @@ class RoundController:
winner_name = "" winner_name = ""
p1_icon = None p1_icon = None
p2_icon = None p2_icon = None
p1_tooltip = None
p2_tooltip = None
if battle.is_draw(): if battle.is_draw():
p1_icon = Icons.get(IconName.DRAW) p1_icon = Icons.get(IconName.DRAW)
p2_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: elif battle.winner_id:
if battle.winner_id == battle.player_1_id: if battle.winner_id == battle.player_1_id:
p1_icon = Icons.get(IconName.WIN) p1_icon = Icons.get(IconName.WIN)
@ -118,6 +141,8 @@ class RoundController:
state_icon=state_icon, state_icon=state_icon,
player1_icon=p1_icon, player1_icon=p1_icon,
player2_icon=p2_icon, player2_icon=p2_icon,
player1_tooltip=p1_tooltip,
player2_tooltip=p2_tooltip,
) )
) )
self.app.view.display_round_battles(battles_for_display) self.app.view.display_round_battles(battles_for_display)
@ -168,6 +193,7 @@ class RoundController:
parent=self.app.view, parent=self.app.view,
players=players, players=players,
counters=counters, counters=counters,
context_type=ContextType.BATTLE,
context_id=ctx.context_id, context_id=ctx.context_id,
) )
if not dialog.exec(): if not dialog.exec():

View file

@ -106,3 +106,18 @@ class TieResolver:
return ev.participant_id # None if confirmed draw return ev.participant_id # None if confirmed draw
return None 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 754 B

View file

@ -1,8 +1,9 @@
from typing import List, Dict from typing import List, Dict
from PyQt6.QtWidgets import QWidget, QDialog from PyQt6.QtWidgets import QWidget, QDialog
from PyQt6.QtCore import Qt
from warchron.constants import Icons, IconName from warchron.constants import Icons, IconName, ContextType, RESOURCES_DIR
from warchron.controller.dtos import ParticipantOption from warchron.controller.dtos import ParticipantOption
from warchron.view.ui.ui_tie_dialog import Ui_tieDialog from warchron.view.ui.ui_tie_dialog import Ui_tieDialog
@ -14,6 +15,7 @@ class TieDialog(QDialog):
*, *,
players: List[ParticipantOption], players: List[ParticipantOption],
counters: List[int], counters: List[int],
context_type: ContextType,
context_id: str, context_id: str,
) -> None: ) -> None:
super().__init__(parent) super().__init__(parent)
@ -22,7 +24,13 @@ class TieDialog(QDialog):
self._p2_id = players[1].id self._p2_id = players[1].id
self.ui: Ui_tieDialog = Ui_tieDialog() self.ui: Ui_tieDialog = Ui_tieDialog()
self.ui.setupUi(self) # type: ignore 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'<img src="{icon_path}" width="16" height="16"> 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_1.setTitle(players[0].name)
self.ui.groupBox_2.setTitle(players[1].name) self.ui.groupBox_2.setTitle(players[1].name)
self.ui.tokenCount_1.setText(str(counters[0])) self.ui.tokenCount_1.setText(str(counters[0]))
@ -38,3 +46,13 @@ class TieDialog(QDialog):
self._p1_id: self.ui.tokenSpend_1.isChecked(), self._p1_id: self.ui.tokenSpend_1.isChecked(),
self._p2_id: self.ui.tokenSpend_2.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")

View file

@ -4,7 +4,7 @@ from pathlib import Path
import calendar import calendar
from PyQt6 import QtWidgets 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.QtWidgets import QWidget, QFileDialog, QTreeWidgetItem, QMenu
from PyQt6.QtGui import QCloseEvent from PyQt6.QtGui import QCloseEvent
@ -540,6 +540,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
table = self.battlesTable table = self.battlesTable
table.clearContents() table.clearContents()
table.setRowCount(len(sectors)) table.setRowCount(len(sectors))
self.battlesTable.setIconSize(QSize(32, 16))
for row, battle in enumerate(sectors): for row, battle in enumerate(sectors):
sector_item = QtWidgets.QTableWidgetItem(battle.sector_name) sector_item = QtWidgets.QTableWidgetItem(battle.sector_name)
if battle.state_icon: if battle.state_icon: