diff --git a/src/warchron/controller/app_controller.py b/src/warchron/controller/app_controller.py
index fd47b91..693cc60 100644
--- a/src/warchron/controller/app_controller.py
+++ b/src/warchron/controller/app_controller.py
@@ -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)
diff --git a/src/warchron/controller/campaign_controller.py b/src/warchron/controller/campaign_controller.py
index 1775509..5ebc964 100644
--- a/src/warchron/controller/campaign_controller.py
+++ b/src/warchron/controller/campaign_controller.py
@@ -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
diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py
index cc9b211..d646954 100644
--- a/src/warchron/controller/dtos.py
+++ b/src/warchron/controller/dtos.py
@@ -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
diff --git a/src/warchron/controller/navigation_controller.py b/src/warchron/controller/navigation_controller.py
index 2a6aa47..a6035bd 100644
--- a/src/warchron/controller/navigation_controller.py
+++ b/src/warchron/controller/navigation_controller.py
@@ -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:
diff --git a/src/warchron/controller/presenter.py b/src/warchron/controller/presenter.py
index d41a336..a7d8f44 100644
--- a/src/warchron/controller/presenter.py
+++ b/src/warchron/controller/presenter.py
@@ -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
diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py
index fb11b7b..25585a0 100644
--- a/src/warchron/controller/round_controller.py
+++ b/src/warchron/controller/round_controller.py
@@ -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
diff --git a/src/warchron/controller/war_controller.py b/src/warchron/controller/war_controller.py
index 4b7c3ef..8546e58 100644
--- a/src/warchron/controller/war_controller.py
+++ b/src/warchron/controller/war_controller.py
@@ -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
diff --git a/src/warchron/controller/workflows.py b/src/warchron/controller/workflows.py
index ea0a4b3..ce0cc19 100644
--- a/src/warchron/controller/workflows.py
+++ b/src/warchron/controller/workflows.py
@@ -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:
diff --git a/src/warchron/model/battle.py b/src/warchron/model/battle.py
index 02a9e90..d54caaf 100644
--- a/src/warchron/model/battle.py
+++ b/src/warchron/model/battle.py
@@ -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,
diff --git a/src/warchron/model/closing.py b/src/warchron/model/closing.py
index 0b70b39..7140ac8 100644
--- a/src/warchron/model/closing.py
+++ b/src/warchron/model/closing.py
@@ -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:
diff --git a/src/warchron/model/exception.py b/src/warchron/model/exception.py
index 26194e3..5c27295 100644
--- a/src/warchron/model/exception.py
+++ b/src/warchron/model/exception.py
@@ -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
diff --git a/src/warchron/model/model.py b/src/warchron/model/model.py
index d1bab74..a122fe0 100644
--- a/src/warchron/model/model.py
+++ b/src/warchron/model/model.py
@@ -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]:
diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py
index 3c8efae..09d9d58 100644
--- a/src/warchron/model/pairing.py
+++ b/src/warchron/model/pairing.py
@@ -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")
diff --git a/src/warchron/model/round.py b/src/warchron/model/round.py
index 4b1a630..ce4c73b 100644
--- a/src/warchron/model/round.py
+++ b/src/warchron/model/round.py
@@ -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()
)
diff --git a/src/warchron/model/statistics.py b/src/warchron/model/statistics.py
new file mode 100644
index 0000000..c1bcb11
--- /dev/null
+++ b/src/warchron/model/statistics.py
@@ -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
diff --git a/src/warchron/model/tiebreaking.py b/src/warchron/model/tiebreaking.py
index ed11ed0..a71767b 100644
--- a/src/warchron/model/tiebreaking.py
+++ b/src/warchron/model/tiebreaking.py
@@ -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():
diff --git a/src/warchron/view/ui/ui_settings_dialog.py b/src/warchron/view/ui/ui_settings_dialog.py
index bf0f161..d051846 100644
--- a/src/warchron/view/ui/ui_settings_dialog.py
+++ b/src/warchron/view/ui/ui_settings_dialog.py
@@ -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"))
diff --git a/src/warchron/view/ui/ui_settings_dialog.ui b/src/warchron/view/ui/ui_settings_dialog.ui
index 2b371bc..cc251f0 100644
--- a/src/warchron/view/ui/ui_settings_dialog.ui
+++ b/src/warchron/view/ui/ui_settings_dialog.ui
@@ -9,8 +9,8 @@
0
0
- 661
- 377
+ 621
+ 387
@@ -31,7 +31,7 @@
Scores
-
+
-
-
@@ -117,16 +117,16 @@
-
-
+
-
- Ranking mode
+ War points mode
-
-
+
false
@@ -138,7 +138,12 @@
-
- Sum points & tie-breaks
+ Sum battle points
+
+
+ -
+
+ Sum campaign ranking
@@ -156,6 +161,94 @@
+ -
+
+
+ false
+
+
+
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Count internal tie-breaks
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
-
+
+
+ Ranking mode
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
-
+
+ Dense (1-2-2-3)
+
+
+ -
+
+ Shift-up (1-2-2-4)
+
+
+ -
+
+ Shift-down (1-3-3-4)
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
@@ -297,9 +390,97 @@
Pairing
-
+
-
+
-
+
+
+ Draw priority
+
+
+
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
-
+
+ Best war ranking
+
+
+ -
+
+ Random
+
+
+ -
+
+ Avoid rematch
+
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Shuffle groups
+
+
+
+ -
+
+
+ false
+
+
+
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
-
@@ -320,7 +501,12 @@
-
- Best ranking first & avoid rematch
+ Avoid rematch
+
+
+ -
+
+ Random
@@ -338,42 +524,6 @@
- -
-
-
- Shuffle
-
-
-
- -
-
-
- false
-
-
-
-
-
- true
-
-
- true
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
diff --git a/src/warchron/view/ui/ui_tie_dialog.py b/src/warchron/view/ui/ui_tie_dialog.py
index 0361373..f6af573 100644
--- a/src/warchron/view/ui/ui_tie_dialog.py
+++ b/src/warchron/view/ui/ui_tie_dialog.py
@@ -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"))
diff --git a/src/warchron/view/ui/ui_tie_dialog.ui b/src/warchron/view/ui/ui_tie_dialog.ui
index 38e6744..69b9fef 100644
--- a/src/warchron/view/ui/ui_tie_dialog.ui
+++ b/src/warchron/view/ui/ui_tie_dialog.ui
@@ -14,7 +14,7 @@
- Tie
+ Tie-break
diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py
index c34077d..d6a8709 100644
--- a/src/warchron/view/view.py
+++ b/src/warchron/view/view.py
@@ -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()