diff --git a/src/warchron/constants.py b/src/warchron/constants.py index 5e2d857..951f91d 100644 --- a/src/warchron/constants.py +++ b/src/warchron/constants.py @@ -71,6 +71,9 @@ class IconName(StrEnum): NP3RDDRAW = auto() NP3RDBREAK = auto() NP3RDTIEDRAW = auto() + ALLOCATED = auto() + ALLOCATEDTOKEN = auto() + FALLBACK = auto() VP_RANK_TO_ICON = { @@ -125,6 +128,8 @@ class Icons: IconName.NP2ND: "medal-silver.png", IconName.NP3RD: "medal-bronze.png", IconName.WARCHRONBACK: "warchron_background.png", + IconName.ALLOCATED: "map.png", + IconName.FALLBACK: "cross-script.png", } @classmethod @@ -255,6 +260,11 @@ class Icons: cls.get_pixmap(IconName.DRAW), cls.get_pixmap(IconName.TOKEN), ) + elif name == IconName.ALLOCATEDTOKEN: + pix = cls._compose( + cls.get_pixmap(IconName.ALLOCATED), + cls.get_pixmap(IconName.TOKEN), + ) else: path = RESOURCES_DIR / cls._paths[name] pix = QPixmap(path.as_posix()) @@ -313,3 +323,16 @@ class ContextType(StrEnum): class ScoreKind(Enum): VP = auto() NP = auto() + + +class ChoiceStatus(StrEnum): + NONE = auto() + TOKEN = auto() + ALLOCATED = auto() + ALLOCATEDTOKEN = auto() + + +class AllocationType(Enum): + PRIORITY = auto() + SECONDARY = auto() + FALLBACK = auto() diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py index 7882d08..97a08e8 100644 --- a/src/warchron/controller/dtos.py +++ b/src/warchron/controller/dtos.py @@ -88,6 +88,9 @@ class ChoiceDTO: priority_sector: str | None secondary_sector: str | None comment: str | None + priority_icon: QIcon | None = None + secondary_icon: QIcon | None = None + fallback_icon: QIcon | None = None @dataclass(frozen=True, slots=True) @@ -103,8 +106,6 @@ 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(frozen=True, slots=True) diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 79a9729..ec3ad78 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -4,7 +4,14 @@ from PyQt6.QtWidgets import QDialog from PyQt6.QtWidgets import QMessageBox from PyQt6.QtGui import QIcon -from warchron.constants import ItemType, RefreshScope, Icons, IconName, ContextType +from warchron.constants import ( + ItemType, + RefreshScope, + Icons, + IconName, + ContextType, + ChoiceStatus, +) from warchron.model.exception import ( AbortedOperation, DomainError, @@ -12,6 +19,7 @@ from warchron.model.exception import ( ) 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 @@ -62,6 +70,20 @@ class RoundController: if choice.secondary_sector_id is not None else "" ) + priority_icon = None + secondary_icon = None + fallback_icon = None + alloc = Pairing.get_round_allocation( + war, + rnd, + part.id, + ) + if alloc.priority != ChoiceStatus.NONE: + priority_icon = QIcon(Icons.get_pixmap(IconName[alloc.priority.name])) + if alloc.secondary != ChoiceStatus.NONE: + secondary_icon = QIcon(Icons.get_pixmap(IconName[alloc.secondary.name])) + if alloc.fallback: + fallback_icon = QIcon(Icons.get_pixmap(IconName.FALLBACK)) choices_for_display.append( ChoiceDTO( id=choice.participant_id, @@ -71,9 +93,11 @@ class RoundController: priority_sector=priority_name, secondary_sector=secondary_name, comment=choice.comment, + priority_icon=priority_icon, + secondary_icon=secondary_icon, + fallback_icon=fallback_icon, ) ) - # TODO display allocated sectors and used token self.app.view.display_round_choices(choices_for_display) battles_for_display: List[BattleDTO] = [] for sect in sectors: @@ -111,8 +135,7 @@ class RoundController: winner_name = "" p1_icon = None p2_icon = None - p1_tooltip = None - p2_tooltip = 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) @@ -131,10 +154,8 @@ class RoundController: 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) @@ -153,8 +174,6 @@ 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) diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py index a6dcabd..d2c8f88 100644 --- a/src/warchron/model/pairing.py +++ b/src/warchron/model/pairing.py @@ -1,10 +1,11 @@ from __future__ import annotations from typing import Dict, List, Callable, Tuple +from dataclasses import dataclass from uuid import uuid4 import random -from warchron.constants import ContextType, ScoreKind +from warchron.constants import ContextType, ScoreKind, ChoiceStatus, AllocationType from warchron.model.exception import ( DomainError, ForbiddenOperation, @@ -15,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 +from warchron.model.war_event import TieResolved, InfluenceSpent from warchron.model.score_service import ParticipantScore ResolveTiesCallback = Callable[ @@ -24,6 +25,13 @@ ResolveTiesCallback = Callable[ ] +@dataclass(frozen=True, slots=True) +class AllocationResult: + priority: ChoiceStatus + secondary: ChoiceStatus + fallback: bool + + class Pairing: @staticmethod @@ -218,6 +226,10 @@ class Pairing: ) or len(active) <= places ): + print( + f"Natural or acceptable draw for sector {sector_id} with participants:\n", + context.participants, + ) war.events.append( TieResolved( None, @@ -234,6 +246,10 @@ class Pairing: bids = bids_map[current_context.key()] # confirmed draw if current bids are 0 if bids is not None and not any(bids.values()): + print( + f"Confirmed draw for sector {sector_id} with participants:\n", + context.participants, + ) war.events.append( TieResolved( None, @@ -278,3 +294,100 @@ class Pairing: remaining.remove(pid) continue raise DomainError(f"Ambiguous fallback for participant {pid}") + + @staticmethod + def get_allocation_kind( + war: War, + round_id: str, + participant_id: str, + sector_id: str, + ) -> AllocationType: + round = war.get_round(round_id) + choice = round.choices.get(participant_id) + if not choice: + raise DomainError(f"No choice found for participant {participant_id}") + if choice.priority_sector_id == sector_id: + return AllocationType.PRIORITY + if choice.secondary_sector_id == sector_id: + 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, + round: Round, + campaign_participant_id: str, + ) -> AllocationResult: + choice = round.choices[campaign_participant_id] + campaign = war.get_campaign_by_round(round.id) + if campaign is None: + 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( + war, + round.id, + choice.priority_sector_id, + war_pid, + ) + token_secondary = Pairing.participant_spent_token( + war, + round.id, + choice.secondary_sector_id, + war_pid, + ) + battle = round.get_battle_for_participant(campaign_participant_id) + allocation = AllocationType.FALLBACK + if battle: + allocation = Pairing.get_allocation_kind( + war, + round.id, + campaign_participant_id, + battle.sector_id, + ) + priority_status = ChoiceStatus.NONE + secondary_status = ChoiceStatus.NONE + fallback = allocation == AllocationType.FALLBACK + if allocation == AllocationType.PRIORITY: + priority_status = ( + ChoiceStatus.ALLOCATEDTOKEN + if token_priority + else ChoiceStatus.ALLOCATED + ) + elif token_priority: + priority_status = ChoiceStatus.TOKEN + if allocation == AllocationType.SECONDARY: + secondary_status = ( + ChoiceStatus.ALLOCATEDTOKEN + if token_secondary + else ChoiceStatus.ALLOCATED + ) + elif token_secondary: + secondary_status = ChoiceStatus.TOKEN + return AllocationResult( + priority=priority_status, + secondary=secondary_status, + fallback=fallback, + ) diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py index 05b7935..a7b4363 100644 --- a/src/warchron/model/round.py +++ b/src/warchron/model/round.py @@ -132,6 +132,18 @@ class Round: def get_battle(self, sector_id: str) -> Battle | None: return self.battles.get(sector_id) + def get_battle_for_participant( + self, + campaign_participant_id: str, + ) -> Battle | None: + for battle in self.battles.values(): + if ( + battle.player_1_id == campaign_participant_id + or battle.player_2_id == campaign_participant_id + ): + return battle + return None + def has_battle_with_sector(self, sector_id: str) -> bool: return any(bat.sector_id == sector_id for bat in self.battles.values()) diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py index 4694f57..0d416eb 100644 --- a/src/warchron/model/tie_manager.py +++ b/src/warchron/model/tie_manager.py @@ -327,6 +327,7 @@ class TieResolver: context_id=context.context_id, tie_id=tie_id, objective_id=context.objective_id, + sector_id=context.sector_id, ) ) @@ -418,6 +419,7 @@ class TieResolver: tie_id=tie_id, score_value=context.score_value, objective_id=context.objective_id, + sector_id=context.sector_id, ) ) return @@ -432,6 +434,7 @@ class TieResolver: tie_id=tie_id, score_value=context.score_value, objective_id=context.objective_id, + sector_id=context.sector_id, ) ) return diff --git a/src/warchron/view/resources/cross-script.png b/src/warchron/view/resources/cross-script.png new file mode 100644 index 0000000..70b59dc Binary files /dev/null and b/src/warchron/view/resources/cross-script.png differ diff --git a/src/warchron/view/resources/map.png b/src/warchron/view/resources/map.png new file mode 100644 index 0000000..79d2648 Binary files /dev/null and b/src/warchron/view/resources/map.png differ diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py index 160719a..80ac522 100644 --- a/src/warchron/view/view.py +++ b/src/warchron/view/view.py @@ -603,15 +603,26 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow): table = self.choicesTable table.setSortingEnabled(False) table.clearContents() + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["Participant", "Priority", "Secondary", ""]) table.setRowCount(len(participants)) + table.setIconSize(QSize(32, 16)) for row, choice in enumerate(participants): participant_item = QtWidgets.QTableWidgetItem(choice.participant_name) priority_item = QtWidgets.QTableWidgetItem(choice.priority_sector) + if choice.priority_icon: + priority_item.setIcon(choice.priority_icon) secondary_item = QtWidgets.QTableWidgetItem(choice.secondary_sector) + if choice.secondary_icon: + secondary_item.setIcon(choice.secondary_icon) + status_item = QtWidgets.QTableWidgetItem() + if choice.fallback_icon: + status_item.setIcon(choice.fallback_icon) participant_item.setData(Qt.ItemDataRole.UserRole, choice.id) table.setItem(row, 0, participant_item) table.setItem(row, 1, priority_item) table.setItem(row, 2, secondary_item) + table.setItem(row, 3, status_item) table.setSortingEnabled(True) table.resizeColumnsToContents()