Compare commits

...

5 commits

Author SHA1 Message Date
Maxime Réaux
2901dcd68c show player stats in table 2026-03-28 11:47:46 +01:00
Maxime Réaux
261c64942d clarify war settings 2026-03-26 15:42:40 +01:00
Maxime Réaux
69942a3cff allow closing round with incomplete battles 2026-03-26 11:30:00 +01:00
Maxime Réaux
ae6c033bbe fix useless fallback at end of pairing 2026-03-25 12:17:45 +01:00
Maxime Réaux
a3144dc3c9 display tiebreak place 2026-03-24 16:26:52 +01:00
21 changed files with 644 additions and 160 deletions

View file

@ -26,6 +26,7 @@ class AppController:
self.current_file: Path | None = None
self.view.on_close_callback = self.on_app_close
self.is_dirty: bool = False
self.players_stats_dirty = True
self.__connect()
self.navigation.refresh_players_view()
self.navigation.refresh_wars_view()
@ -60,6 +61,12 @@ class AppController:
self.view.on_add_item = self.add_item
self.view.on_edit_item = self.edit_item
self.view.on_delete_item = self.delete_item
self.view.tabWidget.currentChanged.connect(self.navigation.on_tab_changed)
def mark_model_dirty(self, *, players_stats: bool = False) -> None:
self.is_dirty = True
if players_stats:
self.players_stats_dirty = True
def on_app_close(self) -> bool:
if self.is_dirty:
@ -235,10 +242,11 @@ class AppController:
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
e.action()
if e.action:
e.action()
else:
return
self.is_dirty = True
self.mark_model_dirty(players_stats=True) # participation may affect stats
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
def edit_item(self, item_type: str, item_id: str) -> None:
@ -290,7 +298,8 @@ class AppController:
)
if reply == QMessageBox.StandardButton.Yes:
try:
e.action()
if e.action:
e.action()
except DomainError as inner:
QMessageBox.warning(
self.view,
@ -300,7 +309,7 @@ class AppController:
return
else:
return
self.is_dirty = True
self.mark_model_dirty(players_stats=(item_type == ItemType.PLAYER))
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
def delete_item(self, item_type: str, item_id: str) -> None:
@ -361,7 +370,8 @@ class AppController:
)
if reply == QMessageBox.StandardButton.Yes:
try:
e.action()
if e.action:
e.action()
except DomainError as inner:
QMessageBox.warning(
self.view,
@ -371,5 +381,5 @@ class AppController:
return
else:
return
self.is_dirty = True
self.mark_model_dirty(players_stats=True) # participation may affect stats
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)

View file

@ -160,7 +160,7 @@ class CampaignController:
str(e),
)
return
self.app.is_dirty = True
self.app.mark_model_dirty(players_stats=True)
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=campaign_id

View file

@ -8,6 +8,12 @@ from PyQt6.QtGui import QIcon
class ParticipantOption:
id: str
name: str
wars_played: int | None = None
wars_won: int | None = None
campaigns_played: int | None = None
campaigns_won: int | None = None
battles_played: int | None = None
battles_won: int | None = None
@dataclass(frozen=True, slots=True)
@ -135,11 +141,6 @@ class WarParticipantScoreDTO:
objective_icons: Dict[str, QIcon] = field(default_factory=dict)
@dataclass
class TieDialogData:
title: str
@dataclass
class WarSettingsDTO:
major_value: int

View file

@ -6,11 +6,11 @@ if TYPE_CHECKING:
from warchron.controller.app_controller import AppController
from warchron.controller.dtos import (
TreeSelection,
ParticipantOption,
WarDTO,
CampaignDTO,
RoundDTO,
)
from warchron.controller.dtos import ParticipantOption
class NavigationController:
@ -23,13 +23,6 @@ class NavigationController:
# Display methods
def refresh_players_view(self) -> None:
players = self.app.model.get_all_players()
players_for_display: List[ParticipantOption] = [
ParticipantOption(id=p.id, name=p.name) for p in players
]
self.app.view.display_players(players_for_display)
def refresh_wars_view(self) -> None:
wars = self.app.model.get_all_wars()
wars_dto: List[WarDTO] = [
@ -74,6 +67,31 @@ class NavigationController:
self.app.wars._fill_war_details(first_war.id)
self.update_actions_state()
def on_tab_changed(self, index: int) -> None:
tab = self.app.view.get_current_tab()
if tab == "players" and self.app.players_stats_dirty:
self.refresh_players_view()
def refresh_players_view(self) -> None:
players = self.app.model.get_all_players()
players_for_display: List[ParticipantOption] = []
for p in players:
stats = self.app.model.get_player_stats(p.id)
players_for_display.append(
ParticipantOption(
id=p.id,
name=p.name,
wars_played=stats.wars_played,
wars_won=stats.wars_won,
campaigns_played=stats.campaigns_played,
campaigns_won=stats.campaigns_won,
battles_played=stats.battles_played,
battles_won=stats.battles_won,
)
)
self.app.players_stats_dirty = False
self.app.view.display_players(players_for_display)
def refresh(self, scope: RefreshScope) -> None:
match scope:
case RefreshScope.PLAYERS_LIST:

View file

@ -1,4 +1,5 @@
from typing import Dict
from dataclasses import dataclass
from PyQt6.QtGui import QIcon
@ -11,16 +12,20 @@ from warchron.constants import (
ScoreKind,
)
from warchron.controller.dtos import TieDialogData
from warchron.model.tiebreaking import TieContext, TieBreaker
from warchron.model.war import War
from warchron.model.campaign import Campaign
from warchron.model.round import Round
from warchron.model.scoring import ParticipantScore
from warchron.model.scoring import ParticipantScore, ScoreComputer
from warchron.model.checking import ResultChecker
from warchron.model.exception import DomainError
@dataclass
class TieDialogData:
title: str
class Presenter:
@staticmethod
@ -30,17 +35,16 @@ class Presenter:
campaign: Campaign | None = None,
round: Round | None = None,
) -> TieDialogData:
# TODO display Nth place
if ctx.context_type == ContextType.WAR:
if ctx.objective_id:
if ctx.context_type in (ContextType.WAR, ContextType.CAMPAIGN):
rank = Presenter._get_tie_rank(war, ctx)
rank_label = Presenter._build_rank_label(rank)
level = str(ctx.context_type).capitalize()
if ctx.objective_id is not None:
obj = war.objectives[ctx.objective_id]
return TieDialogData(f"War objective tie — {obj.name}")
return TieDialogData("War tie")
if ctx.context_type == ContextType.CAMPAIGN:
if ctx.objective_id:
obj = war.objectives[ctx.objective_id]
return TieDialogData(f"Campaign objective tie — {obj.name}")
return TieDialogData("Campaign tie")
return TieDialogData(
f"{level} objective tie for {rank_label}{obj.name}"
)
return TieDialogData(f"{level} tie for {rank_label}")
if ctx.context_type == ContextType.BATTLE:
if campaign:
sector = campaign.sectors[ctx.context_id]
@ -177,3 +181,57 @@ class Presenter:
compute_icon(battle.player_1_id),
compute_icon(battle.player_2_id),
)
@staticmethod
def _ordinal(n: int) -> str:
if 10 <= n % 100 <= 20:
suffix = "th"
else:
suffix = {1: "st", 2: "nd", 3: "rd"}.get(n % 10, "th")
return f"{n}{suffix}"
@staticmethod
def _build_rank_label(rank: int | None) -> str:
if rank is None:
return ""
return f"{Presenter._ordinal(rank)} place"
@staticmethod
def _get_tie_rank(
war: War,
ctx: TieContext,
) -> int | None:
from warchron.model.checking import ResultChecker
scores = ScoreComputer.compute_scores(
war,
ctx.context_type,
ctx.context_id,
)
if ctx.objective_id is None:
def value_getter(score: ParticipantScore) -> int:
return score.victory_points
score_kind = ScoreKind.VP
else:
obj_id = ctx.objective_id
def value_getter(score: ParticipantScore) -> int:
return score.narrative_points.get(obj_id, 0)
score_kind = ScoreKind.NP
ranking = ResultChecker.get_effective_ranking(
war,
ctx.context_type,
ctx.context_id,
score_kind,
scores,
value_getter,
ctx.objective_id,
)
tied = set(ctx.participants)
for rank, group, _ in ranking:
if tied.intersection(group):
return rank
return None

View file

@ -186,16 +186,38 @@ class RoundController:
camp = self.app.model.get_campaign_by_round(round_id)
war = self.app.model.get_war_by_round(round_id)
workflow = RoundClosureWorkflow(self.app)
try:
workflow.start(war, camp, rnd)
except DomainError as e:
QMessageBox.warning(
self.app.view,
"Closure forbidden",
str(e),
)
confirmed = False
stop = False
while True:
try:
workflow.start(war, camp, rnd, confirmed)
break
except RequiresConfirmation as e:
reply = QMessageBox.question(
self.app.view,
"Confirm closing",
str(e),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
if e.action:
e.action()
confirmed = True
continue
else:
stop = True
break
except DomainError as e:
QMessageBox.warning(
self.app.view,
"Closure forbidden",
str(e),
)
stop = True
break
if stop:
return
self.app.is_dirty = True
self.app.mark_model_dirty(players_stats=True)
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
@ -234,10 +256,11 @@ class RoundController:
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
e.action()
if e.action:
e.action()
else:
return
self.app.is_dirty = True
self.app.mark_model_dirty()
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id

View file

@ -147,7 +147,7 @@ class WarController:
str(e),
)
return
self.app.is_dirty = True
self.app.mark_model_dirty(players_stats=True)
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
@ -221,10 +221,11 @@ class WarController:
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
)
if reply == QMessageBox.StandardButton.Yes:
e.action()
if e.action:
e.action()
else:
return
self.is_dirty = True
self.app.mark_model_dirty()
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
self.app.navigation.refresh_and_select(
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id

View file

@ -18,8 +18,10 @@ class Workflow:
class RoundClosureWorkflow(Workflow):
def start(self, war: War, campaign: Campaign, round: Round) -> None:
Closer.check_round_closable(round)
def start(
self, war: War, campaign: Campaign, round: Round, confirmed: bool = False
) -> None:
Closer.check_round_closable(round, confirmed)
ties = TieBreaker.find_battle_ties(war, round.id)
while ties:
for tie in ties:

View file

@ -73,6 +73,14 @@ class Battle:
return
raise DomainError("Battle has no available places")
def get_participants_ids(self) -> List[str]:
players: List[str] = []
if self.player_1_id:
players.append(self.player_1_id)
if self.player_2_id:
players.append(self.player_2_id)
return players
def clear_battle_players(self) -> None:
self.player_1_id = None
self.player_2_id = None
@ -80,6 +88,12 @@ class Battle:
def is_finished(self) -> bool:
return self.winner_id is not None or self.is_draw()
def has_player(self) -> bool:
return self.player_1_id is not None or self.player_2_id is not None
def is_complete(self) -> bool:
return self.player_1_id is not None and self.player_2_id is not None
def toDict(self) -> Dict[str, Any]:
return {
"sector_id": self.sector_id,

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from warchron.constants import ContextType
from warchron.model.exception import ForbiddenOperation
from warchron.model.exception import ForbiddenOperation, RequiresConfirmation
from warchron.model.war_event import InfluenceGained
from warchron.model.war import War
from warchron.model.campaign import Campaign
@ -14,13 +14,19 @@ class Closer:
# Round methods
@staticmethod
def check_round_closable(round: Round) -> None:
def check_round_closable(round: Round, confirmed: bool) -> None:
if round.is_over:
raise ForbiddenOperation("Round already closed")
if not round.all_battles_finished():
raise ForbiddenOperation(
"All battles must be finished to close their round"
)
if not confirmed:
if any(not bat.is_complete() for bat in round.battles.values()):
raise RequiresConfirmation(
"Battle(s) in this round miss player(s).\n"
"Do you want to continue?",
)
if not round.all_battles_finished():
raise ForbiddenOperation(
"All battles must be finished to close their round"
)
@staticmethod
def apply_battle_outcomes(war: War, campaign: Campaign, battle: Battle) -> None:

View file

@ -26,6 +26,6 @@ class DomainDecision(Exception):
class RequiresConfirmation(DomainDecision):
def __init__(self, message: str, action: Callable[[], None]):
def __init__(self, message: str, action: Callable[[], None] | None = None):
super().__init__(message)
self.action = action

View file

@ -15,6 +15,7 @@ from warchron.model.sector import Sector
from warchron.model.round import Round
from warchron.model.choice import Choice
from warchron.model.battle import Battle
from warchron.model.statistics import PlayerStats, StatisticsComputer
class Model:
@ -98,6 +99,9 @@ class Model:
)
del self.players[player_id]
def get_player_stats(self, player_id: str) -> PlayerStats:
return StatisticsComputer.compute_player_stats(self.wars, player_id)
# War methods
def get_default_war_values(self) -> Dict[str, Any]:

View file

@ -63,10 +63,7 @@ class Pairing:
bat.set_winner(None)
war.revert_choice_ties(round.id)
if any(
bat.player_1_id is not None or bat.player_2_id is not None
for bat in round.battles.values()
):
if any(bat.has_player() for bat in round.battles.values()):
raise RequiresConfirmation(
"Battle(s) already have player(s) assigned for this round.\n"
"Battle players will be cleared.\n"
@ -329,6 +326,8 @@ class Pairing:
round: Round,
remaining: List[str],
) -> None:
if not remaining:
return
campaign = war.get_campaign_by_round(round.id)
if campaign is None:
raise DomainError("Campaign not found")

View file

@ -166,7 +166,6 @@ class Round:
return any(b.is_finished() for b in self.battles.values())
def all_battles_finished(self) -> bool:
# TODO exception for participant alone
return all(
b.winner_id is not None or b.is_draw() for b in self.battles.values()
)

View file

@ -0,0 +1,101 @@
from __future__ import annotations
from typing import Dict
from dataclasses import dataclass
from warchron.constants import ContextType, ScoreKind
from warchron.model.war import War
from warchron.model.scoring import ScoreComputer
from warchron.model.checking import ResultChecker
@dataclass
class PlayerStats:
wars_played: int = 0
wars_won: int = 0
campaigns_played: int = 0
campaigns_won: int = 0
battles_played: int = 0
battles_won: int = 0
class StatisticsComputer:
@staticmethod
def compute_player_stats(
wars: Dict[str, War],
player_id: str,
) -> PlayerStats:
stats = PlayerStats()
for war in wars.values():
# --- WAR PARTICIPANT ---
war_part = next(
(wp for wp in war.participants.values() if wp.player_id == player_id),
None,
)
if not war_part:
continue
stats.wars_played += 1
if war.is_over:
scores = ScoreComputer.compute_scores(war, ContextType.WAR, war.id)
ranking = ResultChecker.get_effective_ranking(
war,
ContextType.WAR,
war.id,
ScoreKind.VP,
scores,
lambda s: s.victory_points,
)
if ranking and war_part.id in ranking[0][1]:
stats.wars_won += 1
# --- CAMPAIGNS ---
for campaign in war.campaigns:
campaign_part = next(
(
cp
for cp in campaign.participants.values()
if cp.war_participant_id == war_part.id
),
None,
)
if not campaign_part:
continue
stats.campaigns_played += 1
if campaign.is_over:
scores = ScoreComputer.compute_scores(
war, ContextType.CAMPAIGN, campaign.id
)
ranking = ResultChecker.get_effective_ranking(
war,
ContextType.CAMPAIGN,
campaign.id,
ScoreKind.VP,
scores,
lambda s: s.victory_points,
)
if ranking and war_part.id in ranking[0][1]:
stats.campaigns_won += 1
# --- BATTLES ---
for rnd in campaign.rounds:
for battle in rnd.battles.values():
if not battle.is_finished():
continue
if campaign_part.id not in (
battle.player_1_id,
battle.player_2_id,
):
continue
stats.battles_played += 1
base_winner = None
if battle.winner_id is not None:
base_winner = campaign.campaign_to_war_part_id(
battle.winner_id
)
winner = ResultChecker.get_effective_winner_id(
war,
ContextType.BATTLE,
battle.sector_id,
base_winner,
)
if winner == war_part.id:
stats.battles_won += 1
return stats

View file

@ -63,8 +63,9 @@ class TieBreaker:
for battle in round.battles.values():
if campaign is None:
raise DomainError("No campaign for this battle tie")
if battle.player_1_id is None or battle.player_2_id is None:
raise DomainError("Missing player(s) in this battle context.")
if not battle.is_complete():
continue
assert battle.player_1_id is not None and battle.player_2_id is not None
p1_id = campaign.campaign_to_war_part_id(battle.player_1_id)
p2_id = campaign.campaign_to_war_part_id(battle.player_2_id)
if not battle.is_draw():

View file

@ -13,7 +13,7 @@ class Ui_settingsDialog(object):
def setupUi(self, settingsDialog):
settingsDialog.setObjectName("settingsDialog")
settingsDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
settingsDialog.resize(661, 377)
settingsDialog.resize(621, 387)
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/warchron_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
settingsDialog.setWindowIcon(icon)
@ -24,8 +24,8 @@ class Ui_settingsDialog(object):
font.setPointSize(10)
self.groupBox_2.setFont(font)
self.groupBox_2.setObjectName("groupBox_2")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox_2)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_2)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.label_5 = QtWidgets.QLabel(parent=self.groupBox_2)
@ -56,25 +56,58 @@ class Ui_settingsDialog(object):
self.horizontalLayout.addWidget(self.label_15)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout.addItem(spacerItem1)
self.verticalLayout_2.addLayout(self.horizontalLayout)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.verticalLayout_3.addLayout(self.horizontalLayout)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.label_12 = QtWidgets.QLabel(parent=self.groupBox_2)
self.label_12.setObjectName("label_12")
self.horizontalLayout_5.addWidget(self.label_12)
self.rankingComboBox = QtWidgets.QComboBox(parent=self.groupBox_2)
self.rankingComboBox.setEnabled(False)
self.horizontalLayout_2.addWidget(self.label_12)
self.pointsComboBox = QtWidgets.QComboBox(parent=self.groupBox_2)
self.pointsComboBox.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.rankingComboBox.sizePolicy().hasHeightForWidth())
self.rankingComboBox.setSizePolicy(sizePolicy)
self.rankingComboBox.setObjectName("rankingComboBox")
self.rankingComboBox.addItem("")
self.horizontalLayout_5.addWidget(self.rankingComboBox)
sizePolicy.setHeightForWidth(self.pointsComboBox.sizePolicy().hasHeightForWidth())
self.pointsComboBox.setSizePolicy(sizePolicy)
self.pointsComboBox.setObjectName("pointsComboBox")
self.pointsComboBox.addItem("")
self.pointsComboBox.addItem("")
self.horizontalLayout_2.addWidget(self.pointsComboBox)
spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_5.addItem(spacerItem2)
self.verticalLayout_2.addLayout(self.horizontalLayout_5)
self.horizontalLayout_2.addItem(spacerItem2)
self.internalTiebreak = QtWidgets.QCheckBox(parent=self.groupBox_2)
self.internalTiebreak.setEnabled(False)
self.internalTiebreak.setText("")
self.internalTiebreak.setCheckable(True)
self.internalTiebreak.setChecked(True)
self.internalTiebreak.setObjectName("internalTiebreak")
self.horizontalLayout_2.addWidget(self.internalTiebreak)
self.label_20 = QtWidgets.QLabel(parent=self.groupBox_2)
self.label_20.setObjectName("label_20")
self.horizontalLayout_2.addWidget(self.label_20)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_2.addItem(spacerItem3)
self.verticalLayout_3.addLayout(self.horizontalLayout_2)
self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
self.horizontalLayout_5.setObjectName("horizontalLayout_5")
self.label_19 = QtWidgets.QLabel(parent=self.groupBox_2)
self.label_19.setObjectName("label_19")
self.horizontalLayout_5.addWidget(self.label_19)
self.rankingComboBox_2 = QtWidgets.QComboBox(parent=self.groupBox_2)
self.rankingComboBox_2.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.rankingComboBox_2.sizePolicy().hasHeightForWidth())
self.rankingComboBox_2.setSizePolicy(sizePolicy)
self.rankingComboBox_2.setObjectName("rankingComboBox_2")
self.rankingComboBox_2.addItem("")
self.rankingComboBox_2.addItem("")
self.rankingComboBox_2.addItem("")
self.horizontalLayout_5.addWidget(self.rankingComboBox_2)
spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_5.addItem(spacerItem4)
self.verticalLayout_3.addLayout(self.horizontalLayout_5)
self.verticalLayout_4.addWidget(self.groupBox_2)
self.groupBox = QtWidgets.QGroupBox(parent=settingsDialog)
font = QtGui.QFont()
@ -95,8 +128,8 @@ class Ui_settingsDialog(object):
self.label_8 = QtWidgets.QLabel(parent=self.groupBox)
self.label_8.setObjectName("label_8")
self.horizontalLayout_3.addWidget(self.label_8)
spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addItem(spacerItem3)
spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addItem(spacerItem5)
self.label_9 = QtWidgets.QLabel(parent=self.groupBox)
self.label_9.setObjectName("label_9")
self.horizontalLayout_3.addWidget(self.label_9)
@ -107,8 +140,8 @@ class Ui_settingsDialog(object):
self.label_10 = QtWidgets.QLabel(parent=self.groupBox)
self.label_10.setObjectName("label_10")
self.horizontalLayout_3.addWidget(self.label_10)
spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addItem(spacerItem4)
spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_3.addItem(spacerItem6)
self.verticalLayout.addLayout(self.horizontalLayout_3)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
@ -121,8 +154,8 @@ class Ui_settingsDialog(object):
self.influenceToken.setChecked(True)
self.influenceToken.setObjectName("influenceToken")
self.horizontalLayout_4.addWidget(self.influenceToken)
spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_4.addItem(spacerItem5)
spacerItem7 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_4.addItem(spacerItem7)
self.verticalLayout.addLayout(self.horizontalLayout_4)
self.verticalLayout_4.addWidget(self.groupBox)
self.groupBox_3 = QtWidgets.QGroupBox(parent=settingsDialog)
@ -130,13 +163,45 @@ class Ui_settingsDialog(object):
font.setPointSize(10)
self.groupBox_3.setFont(font)
self.groupBox_3.setObjectName("groupBox_3")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.groupBox_3)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox_3)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
self.horizontalLayout_7.setObjectName("horizontalLayout_7")
self.label_21 = QtWidgets.QLabel(parent=self.groupBox_3)
self.label_21.setObjectName("label_21")
self.horizontalLayout_7.addWidget(self.label_21)
self.drawComboBox = QtWidgets.QComboBox(parent=self.groupBox_3)
self.drawComboBox.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.drawComboBox.sizePolicy().hasHeightForWidth())
self.drawComboBox.setSizePolicy(sizePolicy)
self.drawComboBox.setObjectName("drawComboBox")
self.drawComboBox.addItem("")
self.drawComboBox.addItem("")
self.drawComboBox.addItem("")
self.horizontalLayout_7.addWidget(self.drawComboBox)
spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_7.addItem(spacerItem8)
self.label_18 = QtWidgets.QLabel(parent=self.groupBox_3)
self.label_18.setObjectName("label_18")
self.horizontalLayout_7.addWidget(self.label_18)
self.shuffle = QtWidgets.QCheckBox(parent=self.groupBox_3)
self.shuffle.setEnabled(False)
self.shuffle.setText("")
self.shuffle.setCheckable(True)
self.shuffle.setChecked(True)
self.shuffle.setObjectName("shuffle")
self.horizontalLayout_7.addWidget(self.shuffle)
spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_7.addItem(spacerItem9)
self.verticalLayout_2.addLayout(self.horizontalLayout_7)
self.horizontalLayout_8 = QtWidgets.QHBoxLayout()
self.horizontalLayout_8.setObjectName("horizontalLayout_8")
self.label_17 = QtWidgets.QLabel(parent=self.groupBox_3)
self.label_17.setObjectName("label_17")
self.horizontalLayout_7.addWidget(self.label_17)
self.horizontalLayout_8.addWidget(self.label_17)
self.fallbackComboBox = QtWidgets.QComboBox(parent=self.groupBox_3)
self.fallbackComboBox.setEnabled(False)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed)
@ -146,22 +211,11 @@ class Ui_settingsDialog(object):
self.fallbackComboBox.setSizePolicy(sizePolicy)
self.fallbackComboBox.setObjectName("fallbackComboBox")
self.fallbackComboBox.addItem("")
self.horizontalLayout_7.addWidget(self.fallbackComboBox)
spacerItem6 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_7.addItem(spacerItem6)
self.label_18 = QtWidgets.QLabel(parent=self.groupBox_3)
self.label_18.setObjectName("label_18")
self.horizontalLayout_7.addWidget(self.label_18)
self.influenceToken_2 = QtWidgets.QCheckBox(parent=self.groupBox_3)
self.influenceToken_2.setEnabled(False)
self.influenceToken_2.setText("")
self.influenceToken_2.setCheckable(True)
self.influenceToken_2.setChecked(True)
self.influenceToken_2.setObjectName("influenceToken_2")
self.horizontalLayout_7.addWidget(self.influenceToken_2)
spacerItem7 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_7.addItem(spacerItem7)
self.verticalLayout_3.addLayout(self.horizontalLayout_7)
self.fallbackComboBox.addItem("")
self.horizontalLayout_8.addWidget(self.fallbackComboBox)
spacerItem10 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_8.addItem(spacerItem10)
self.verticalLayout_2.addLayout(self.horizontalLayout_8)
self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
self.horizontalLayout_6.setObjectName("horizontalLayout_6")
self.label_13 = QtWidgets.QLabel(parent=self.groupBox_3)
@ -172,8 +226,8 @@ class Ui_settingsDialog(object):
self.rematchValue.setMinimum(1)
self.rematchValue.setObjectName("rematchValue")
self.horizontalLayout_6.addWidget(self.rematchValue)
spacerItem8 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_6.addItem(spacerItem8)
spacerItem11 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_6.addItem(spacerItem11)
self.label_16 = QtWidgets.QLabel(parent=self.groupBox_3)
self.label_16.setObjectName("label_16")
self.horizontalLayout_6.addWidget(self.label_16)
@ -182,9 +236,9 @@ class Ui_settingsDialog(object):
self.occupancyValue.setMinimum(1)
self.occupancyValue.setObjectName("occupancyValue")
self.horizontalLayout_6.addWidget(self.occupancyValue)
spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_6.addItem(spacerItem9)
self.verticalLayout_3.addLayout(self.horizontalLayout_6)
spacerItem12 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_6.addItem(spacerItem12)
self.verticalLayout_2.addLayout(self.horizontalLayout_6)
self.verticalLayout_4.addWidget(self.groupBox_3)
self.buttonBox = QtWidgets.QDialogButtonBox(parent=settingsDialog)
self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
@ -205,8 +259,14 @@ class Ui_settingsDialog(object):
self.label_14.setText(_translate("settingsDialog", "victory points"))
self.label_6.setText(_translate("settingsDialog", "Draw"))
self.label_15.setText(_translate("settingsDialog", "victory points"))
self.label_12.setText(_translate("settingsDialog", "Ranking mode"))
self.rankingComboBox.setItemText(0, _translate("settingsDialog", "Sum points & tie-breaks"))
self.label_12.setText(_translate("settingsDialog", "War points mode"))
self.pointsComboBox.setItemText(0, _translate("settingsDialog", "Sum battle points"))
self.pointsComboBox.setItemText(1, _translate("settingsDialog", "Sum campaign ranking"))
self.label_20.setText(_translate("settingsDialog", "Count internal tie-breaks"))
self.label_19.setText(_translate("settingsDialog", "Ranking mode"))
self.rankingComboBox_2.setItemText(0, _translate("settingsDialog", "Dense (1-2-2-3)"))
self.rankingComboBox_2.setItemText(1, _translate("settingsDialog", "Shift-up (1-2-2-4)"))
self.rankingComboBox_2.setItemText(2, _translate("settingsDialog", "Shift-down (1-3-3-4)"))
self.groupBox.setTitle(_translate("settingsDialog", "Objectives"))
self.label_4.setText(_translate("settingsDialog", "Major objective"))
self.label_8.setText(_translate("settingsDialog", "narrative points"))
@ -215,9 +275,14 @@ class Ui_settingsDialog(object):
self.label_11.setText(_translate("settingsDialog", "Underlying influence"))
self.influenceToken.setText(_translate("settingsDialog", "Token"))
self.groupBox_3.setTitle(_translate("settingsDialog", "Pairing"))
self.label_21.setText(_translate("settingsDialog", "Draw priority"))
self.drawComboBox.setItemText(0, _translate("settingsDialog", "Best war ranking"))
self.drawComboBox.setItemText(1, _translate("settingsDialog", "Random"))
self.drawComboBox.setItemText(2, _translate("settingsDialog", "Avoid rematch"))
self.label_18.setText(_translate("settingsDialog", "Shuffle groups"))
self.label_17.setText(_translate("settingsDialog", "Fallback mode"))
self.fallbackComboBox.setItemText(0, _translate("settingsDialog", "Best ranking first & avoid rematch"))
self.label_18.setText(_translate("settingsDialog", "Shuffle"))
self.fallbackComboBox.setItemText(0, _translate("settingsDialog", "Avoid rematch"))
self.fallbackComboBox.setItemText(1, _translate("settingsDialog", "Random"))
self.label_13.setText(_translate("settingsDialog", "Rematch weight"))
self.label_16.setText(_translate("settingsDialog", "Occupancy weight"))

View file

@ -9,8 +9,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>661</width>
<height>377</height>
<width>621</width>
<height>387</height>
</rect>
</property>
<property name="windowTitle">
@ -31,7 +31,7 @@
<property name="title">
<string>Scores</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
@ -117,16 +117,16 @@
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_12">
<property name="text">
<string>Ranking mode</string>
<string>War points mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="rankingComboBox">
<widget class="QComboBox" name="pointsComboBox">
<property name="enabled">
<bool>false</bool>
</property>
@ -138,7 +138,12 @@
</property>
<item>
<property name="text">
<string>Sum points &amp; tie-breaks</string>
<string>Sum battle points</string>
</property>
</item>
<item>
<property name="text">
<string>Sum campaign ranking</string>
</property>
</item>
</widget>
@ -156,6 +161,94 @@
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="internalTiebreak">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_20">
<property name="text">
<string>Count internal tie-breaks</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_18">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_19">
<property name="text">
<string>Ranking mode</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="rankingComboBox_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Dense (1-2-2-3)</string>
</property>
</item>
<item>
<property name="text">
<string>Shift-up (1-2-2-4)</string>
</property>
</item>
<item>
<property name="text">
<string>Shift-down (1-3-3-4)</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout>
@ -297,9 +390,97 @@
<property name="title">
<string>Pairing</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_7">
<item>
<widget class="QLabel" name="label_21">
<property name="text">
<string>Draw priority</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="drawComboBox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<item>
<property name="text">
<string>Best war ranking</string>
</property>
</item>
<item>
<property name="text">
<string>Random</string>
</property>
</item>
<item>
<property name="text">
<string>Avoid rematch</string>
</property>
</item>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_6">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_18">
<property name="text">
<string>Shuffle groups</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="shuffle">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_17">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_17">
<property name="text">
@ -320,7 +501,12 @@
</property>
<item>
<property name="text">
<string>Best ranking first &amp; avoid rematch</string>
<string>Avoid rematch</string>
</property>
</item>
<item>
<property name="text">
<string>Random</string>
</property>
</item>
</widget>
@ -338,42 +524,6 @@
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="label_18">
<property name="text">
<string>Shuffle</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="influenceToken_2">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string/>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_17">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>

View file

@ -67,7 +67,7 @@ class Ui_tieDialog(object):
def retranslateUi(self, tieDialog):
_translate = QtCore.QCoreApplication.translate
tieDialog.setWindowTitle(_translate("tieDialog", "Tie"))
tieDialog.setWindowTitle(_translate("tieDialog", "Tie-break"))
self.tieContext.setText(_translate("tieDialog", "Battle tie"))

View file

@ -14,7 +14,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Tie</string>
<string>Tie-break</string>
</property>
<property name="windowIcon">
<iconset>

View file

@ -200,15 +200,47 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
elif action == delete_action and self.on_delete_item:
self.on_delete_item(ItemType.PLAYER, player_id)
def _make_ratio_item(
self, won: int | None, played: int | None
) -> QtWidgets.QTableWidgetItem:
if not played:
text = ""
ratio = -1.0
else:
won = won or 0
text = f"{won} / {played}"
ratio = won / played
item = QtWidgets.QTableWidgetItem(text)
item.setData(Qt.ItemDataRole.UserRole, ratio)
return item
def display_players(self, players: List[ParticipantOption]) -> None:
# TODO display stats (war, campaign battles...)
table = self.playersTable
table.setSortingEnabled(False)
table.setColumnCount(4)
table.setHorizontalHeaderLabels(["Name", "Wars", "Campaigns", "Battles"])
table.setRowCount(len(players))
for row, player in enumerate(players):
play_item = QtWidgets.QTableWidgetItem(player.name)
wars_item = self._make_ratio_item(
player.wars_won,
player.wars_played,
)
camp_item = self._make_ratio_item(
player.campaigns_won,
player.campaigns_played,
)
bat_item = self._make_ratio_item(
player.battles_won,
player.battles_played,
)
play_item.setData(Qt.ItemDataRole.UserRole, player.id)
table.setItem(row, 0, play_item)
table.setItem(row, 1, wars_item)
table.setItem(row, 2, camp_item)
table.setItem(row, 3, bat_item)
table.setSortingEnabled(True)
table.sortItems(0, Qt.SortOrder.AscendingOrder)
table.resizeColumnsToContents()