display pairing results in choice table

This commit is contained in:
Maxime Réaux 2026-03-19 09:02:22 +01:00
parent 9e602e8ca4
commit 4396b15c3a
9 changed files with 194 additions and 12 deletions

View file

@ -71,6 +71,9 @@ class IconName(StrEnum):
NP3RDDRAW = auto() NP3RDDRAW = auto()
NP3RDBREAK = auto() NP3RDBREAK = auto()
NP3RDTIEDRAW = auto() NP3RDTIEDRAW = auto()
ALLOCATED = auto()
ALLOCATEDTOKEN = auto()
FALLBACK = auto()
VP_RANK_TO_ICON = { VP_RANK_TO_ICON = {
@ -125,6 +128,8 @@ class Icons:
IconName.NP2ND: "medal-silver.png", IconName.NP2ND: "medal-silver.png",
IconName.NP3RD: "medal-bronze.png", IconName.NP3RD: "medal-bronze.png",
IconName.WARCHRONBACK: "warchron_background.png", IconName.WARCHRONBACK: "warchron_background.png",
IconName.ALLOCATED: "map.png",
IconName.FALLBACK: "cross-script.png",
} }
@classmethod @classmethod
@ -255,6 +260,11 @@ class Icons:
cls.get_pixmap(IconName.DRAW), cls.get_pixmap(IconName.DRAW),
cls.get_pixmap(IconName.TOKEN), cls.get_pixmap(IconName.TOKEN),
) )
elif name == IconName.ALLOCATEDTOKEN:
pix = cls._compose(
cls.get_pixmap(IconName.ALLOCATED),
cls.get_pixmap(IconName.TOKEN),
)
else: else:
path = RESOURCES_DIR / cls._paths[name] path = RESOURCES_DIR / cls._paths[name]
pix = QPixmap(path.as_posix()) pix = QPixmap(path.as_posix())
@ -313,3 +323,16 @@ class ContextType(StrEnum):
class ScoreKind(Enum): class ScoreKind(Enum):
VP = auto() VP = auto()
NP = auto() NP = auto()
class ChoiceStatus(StrEnum):
NONE = auto()
TOKEN = auto()
ALLOCATED = auto()
ALLOCATEDTOKEN = auto()
class AllocationType(Enum):
PRIORITY = auto()
SECONDARY = auto()
FALLBACK = auto()

View file

@ -88,6 +88,9 @@ class ChoiceDTO:
priority_sector: str | None priority_sector: str | None
secondary_sector: str | None secondary_sector: str | None
comment: 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) @dataclass(frozen=True, slots=True)
@ -103,8 +106,6 @@ 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(frozen=True, slots=True) @dataclass(frozen=True, slots=True)

View file

@ -4,7 +4,14 @@ from PyQt6.QtWidgets import QDialog
from PyQt6.QtWidgets import QMessageBox from PyQt6.QtWidgets import QMessageBox
from PyQt6.QtGui import QIcon 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 ( from warchron.model.exception import (
AbortedOperation, AbortedOperation,
DomainError, DomainError,
@ -12,6 +19,7 @@ from warchron.model.exception import (
) )
from warchron.model.tie_manager import TieResolver, TieContext from warchron.model.tie_manager import TieResolver, TieContext
from warchron.model.result_checker import ResultChecker from warchron.model.result_checker import ResultChecker
from warchron.model.pairing import Pairing
from warchron.model.round import Round from warchron.model.round import Round
from warchron.model.war import War from warchron.model.war import War
@ -62,6 +70,20 @@ class RoundController:
if choice.secondary_sector_id is not None if choice.secondary_sector_id is not None
else "" 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( choices_for_display.append(
ChoiceDTO( ChoiceDTO(
id=choice.participant_id, id=choice.participant_id,
@ -71,9 +93,11 @@ class RoundController:
priority_sector=priority_name, priority_sector=priority_name,
secondary_sector=secondary_name, secondary_sector=secondary_name,
comment=choice.comment, 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) self.app.view.display_round_choices(choices_for_display)
battles_for_display: List[BattleDTO] = [] battles_for_display: List[BattleDTO] = []
for sect in sectors: for sect in sectors:
@ -111,8 +135,7 @@ class RoundController:
winner_name = "" winner_name = ""
p1_icon = None p1_icon = None
p2_icon = None p2_icon = None
p1_tooltip = None # TODO use uniform draw/tie icon logic with choice, war, campaign...
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)
@ -131,10 +154,8 @@ class RoundController:
pixmap = Icons.get_pixmap(IconName.TIEBREAK_TOKEN) pixmap = Icons.get_pixmap(IconName.TIEBREAK_TOKEN)
if effective_winner == p1_war: if effective_winner == p1_war:
p1_icon = QIcon(pixmap) p1_icon = QIcon(pixmap)
p1_tooltip = "Won by tie-break"
else: else:
p2_icon = QIcon(pixmap) 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)
@ -153,8 +174,6 @@ 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)

View file

@ -1,10 +1,11 @@
from __future__ import annotations from __future__ import annotations
from typing import Dict, List, Callable, Tuple from typing import Dict, List, Callable, Tuple
from dataclasses import dataclass
from uuid import uuid4 from uuid import uuid4
import random import random
from warchron.constants import ContextType, ScoreKind from warchron.constants import ContextType, ScoreKind, ChoiceStatus, AllocationType
from warchron.model.exception import ( from warchron.model.exception import (
DomainError, DomainError,
ForbiddenOperation, ForbiddenOperation,
@ -15,7 +16,7 @@ from warchron.model.round import Round
from warchron.model.battle import Battle from warchron.model.battle import Battle
from warchron.model.score_service import ScoreService from warchron.model.score_service import ScoreService
from warchron.model.tie_manager import TieResolver, TieContext 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 from warchron.model.score_service import ParticipantScore
ResolveTiesCallback = Callable[ ResolveTiesCallback = Callable[
@ -24,6 +25,13 @@ ResolveTiesCallback = Callable[
] ]
@dataclass(frozen=True, slots=True)
class AllocationResult:
priority: ChoiceStatus
secondary: ChoiceStatus
fallback: bool
class Pairing: class Pairing:
@staticmethod @staticmethod
@ -218,6 +226,10 @@ class Pairing:
) )
or len(active) <= places or len(active) <= places
): ):
print(
f"Natural or acceptable draw for sector {sector_id} with participants:\n",
context.participants,
)
war.events.append( war.events.append(
TieResolved( TieResolved(
None, None,
@ -234,6 +246,10 @@ class Pairing:
bids = bids_map[current_context.key()] bids = bids_map[current_context.key()]
# confirmed draw if current bids are 0 # confirmed draw if current bids are 0
if bids is not None and not any(bids.values()): 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( war.events.append(
TieResolved( TieResolved(
None, None,
@ -278,3 +294,100 @@ class Pairing:
remaining.remove(pid) remaining.remove(pid)
continue continue
raise DomainError(f"Ambiguous fallback for participant {pid}") 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,
)

View file

@ -132,6 +132,18 @@ class Round:
def get_battle(self, sector_id: str) -> Battle | None: def get_battle(self, sector_id: str) -> Battle | None:
return self.battles.get(sector_id) 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: def has_battle_with_sector(self, sector_id: str) -> bool:
return any(bat.sector_id == sector_id for bat in self.battles.values()) return any(bat.sector_id == sector_id for bat in self.battles.values())

View file

@ -327,6 +327,7 @@ class TieResolver:
context_id=context.context_id, context_id=context.context_id,
tie_id=tie_id, tie_id=tie_id,
objective_id=context.objective_id, objective_id=context.objective_id,
sector_id=context.sector_id,
) )
) )
@ -418,6 +419,7 @@ class TieResolver:
tie_id=tie_id, tie_id=tie_id,
score_value=context.score_value, score_value=context.score_value,
objective_id=context.objective_id, objective_id=context.objective_id,
sector_id=context.sector_id,
) )
) )
return return
@ -432,6 +434,7 @@ class TieResolver:
tie_id=tie_id, tie_id=tie_id,
score_value=context.score_value, score_value=context.score_value,
objective_id=context.objective_id, objective_id=context.objective_id,
sector_id=context.sector_id,
) )
) )
return return

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -603,15 +603,26 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
table = self.choicesTable table = self.choicesTable
table.setSortingEnabled(False) table.setSortingEnabled(False)
table.clearContents() table.clearContents()
table.setColumnCount(4)
table.setHorizontalHeaderLabels(["Participant", "Priority", "Secondary", ""])
table.setRowCount(len(participants)) table.setRowCount(len(participants))
table.setIconSize(QSize(32, 16))
for row, choice in enumerate(participants): for row, choice in enumerate(participants):
participant_item = QtWidgets.QTableWidgetItem(choice.participant_name) participant_item = QtWidgets.QTableWidgetItem(choice.participant_name)
priority_item = QtWidgets.QTableWidgetItem(choice.priority_sector) priority_item = QtWidgets.QTableWidgetItem(choice.priority_sector)
if choice.priority_icon:
priority_item.setIcon(choice.priority_icon)
secondary_item = QtWidgets.QTableWidgetItem(choice.secondary_sector) 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) participant_item.setData(Qt.ItemDataRole.UserRole, choice.id)
table.setItem(row, 0, participant_item) table.setItem(row, 0, participant_item)
table.setItem(row, 1, priority_item) table.setItem(row, 1, priority_item)
table.setItem(row, 2, secondary_item) table.setItem(row, 2, secondary_item)
table.setItem(row, 3, status_item)
table.setSortingEnabled(True) table.setSortingEnabled(True)
table.resizeColumnsToContents() table.resizeColumnsToContents()