show player stats in table
This commit is contained in:
parent
261c64942d
commit
2901dcd68c
10 changed files with 193 additions and 17 deletions
|
|
@ -26,6 +26,7 @@ class AppController:
|
||||||
self.current_file: Path | None = None
|
self.current_file: Path | None = None
|
||||||
self.view.on_close_callback = self.on_app_close
|
self.view.on_close_callback = self.on_app_close
|
||||||
self.is_dirty: bool = False
|
self.is_dirty: bool = False
|
||||||
|
self.players_stats_dirty = True
|
||||||
self.__connect()
|
self.__connect()
|
||||||
self.navigation.refresh_players_view()
|
self.navigation.refresh_players_view()
|
||||||
self.navigation.refresh_wars_view()
|
self.navigation.refresh_wars_view()
|
||||||
|
|
@ -60,6 +61,12 @@ class AppController:
|
||||||
self.view.on_add_item = self.add_item
|
self.view.on_add_item = self.add_item
|
||||||
self.view.on_edit_item = self.edit_item
|
self.view.on_edit_item = self.edit_item
|
||||||
self.view.on_delete_item = self.delete_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:
|
def on_app_close(self) -> bool:
|
||||||
if self.is_dirty:
|
if self.is_dirty:
|
||||||
|
|
@ -239,7 +246,7 @@ class AppController:
|
||||||
e.action()
|
e.action()
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
self.is_dirty = True
|
self.mark_model_dirty(players_stats=True) # participation may affect stats
|
||||||
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
|
|
||||||
def edit_item(self, item_type: str, item_id: str) -> None:
|
def edit_item(self, item_type: str, item_id: str) -> None:
|
||||||
|
|
@ -302,7 +309,7 @@ class AppController:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
self.is_dirty = True
|
self.mark_model_dirty(players_stats=(item_type == ItemType.PLAYER))
|
||||||
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
|
|
||||||
def delete_item(self, item_type: str, item_id: str) -> None:
|
def delete_item(self, item_type: str, item_id: str) -> None:
|
||||||
|
|
@ -374,5 +381,5 @@ class AppController:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
self.is_dirty = True
|
self.mark_model_dirty(players_stats=True) # participation may affect stats
|
||||||
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,7 @@ class CampaignController:
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
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(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=campaign_id
|
RefreshScope.WARS_TREE, item_type=ItemType.CAMPAIGN, item_id=campaign_id
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ from PyQt6.QtGui import QIcon
|
||||||
class ParticipantOption:
|
class ParticipantOption:
|
||||||
id: str
|
id: str
|
||||||
name: 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)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ if TYPE_CHECKING:
|
||||||
from warchron.controller.app_controller import AppController
|
from warchron.controller.app_controller import AppController
|
||||||
from warchron.controller.dtos import (
|
from warchron.controller.dtos import (
|
||||||
TreeSelection,
|
TreeSelection,
|
||||||
ParticipantOption,
|
|
||||||
WarDTO,
|
WarDTO,
|
||||||
CampaignDTO,
|
CampaignDTO,
|
||||||
RoundDTO,
|
RoundDTO,
|
||||||
)
|
)
|
||||||
|
from warchron.controller.dtos import ParticipantOption
|
||||||
|
|
||||||
|
|
||||||
class NavigationController:
|
class NavigationController:
|
||||||
|
|
@ -23,13 +23,6 @@ class NavigationController:
|
||||||
|
|
||||||
# Display methods
|
# 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:
|
def refresh_wars_view(self) -> None:
|
||||||
wars = self.app.model.get_all_wars()
|
wars = self.app.model.get_all_wars()
|
||||||
wars_dto: List[WarDTO] = [
|
wars_dto: List[WarDTO] = [
|
||||||
|
|
@ -74,6 +67,31 @@ class NavigationController:
|
||||||
self.app.wars._fill_war_details(first_war.id)
|
self.app.wars._fill_war_details(first_war.id)
|
||||||
self.update_actions_state()
|
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:
|
def refresh(self, scope: RefreshScope) -> None:
|
||||||
match scope:
|
match scope:
|
||||||
case RefreshScope.PLAYERS_LIST:
|
case RefreshScope.PLAYERS_LIST:
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,7 @@ class RoundController:
|
||||||
break
|
break
|
||||||
if stop:
|
if stop:
|
||||||
return
|
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(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
|
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
|
||||||
|
|
@ -260,7 +260,7 @@ class RoundController:
|
||||||
e.action()
|
e.action()
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
self.app.is_dirty = True
|
self.app.mark_model_dirty()
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
|
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ class WarController:
|
||||||
str(e),
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
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(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
||||||
|
|
@ -225,7 +225,7 @@ class WarController:
|
||||||
e.action()
|
e.action()
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
self.is_dirty = True
|
self.app.mark_model_dirty()
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
RefreshScope.WARS_TREE, item_type=ItemType.WAR, item_id=war_id
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,14 @@ class Battle:
|
||||||
return
|
return
|
||||||
raise DomainError("Battle has no available places")
|
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:
|
def clear_battle_players(self) -> None:
|
||||||
self.player_1_id = None
|
self.player_1_id = None
|
||||||
self.player_2_id = None
|
self.player_2_id = None
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ from warchron.model.sector import Sector
|
||||||
from warchron.model.round import Round
|
from warchron.model.round import Round
|
||||||
from warchron.model.choice import Choice
|
from warchron.model.choice import Choice
|
||||||
from warchron.model.battle import Battle
|
from warchron.model.battle import Battle
|
||||||
|
from warchron.model.statistics import PlayerStats, StatisticsComputer
|
||||||
|
|
||||||
|
|
||||||
class Model:
|
class Model:
|
||||||
|
|
@ -98,6 +99,9 @@ class Model:
|
||||||
)
|
)
|
||||||
del self.players[player_id]
|
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
|
# War methods
|
||||||
|
|
||||||
def get_default_war_values(self) -> Dict[str, Any]:
|
def get_default_war_values(self) -> Dict[str, Any]:
|
||||||
|
|
|
||||||
101
src/warchron/model/statistics.py
Normal file
101
src/warchron/model/statistics.py
Normal 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
|
||||||
|
|
@ -200,15 +200,47 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||||
elif action == delete_action and self.on_delete_item:
|
elif action == delete_action and self.on_delete_item:
|
||||||
self.on_delete_item(ItemType.PLAYER, player_id)
|
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:
|
def display_players(self, players: List[ParticipantOption]) -> None:
|
||||||
# TODO display stats (war, campaign battles...)
|
|
||||||
table = self.playersTable
|
table = self.playersTable
|
||||||
table.setSortingEnabled(False)
|
table.setSortingEnabled(False)
|
||||||
|
table.setColumnCount(4)
|
||||||
|
table.setHorizontalHeaderLabels(["Name", "Wars", "Campaigns", "Battles"])
|
||||||
table.setRowCount(len(players))
|
table.setRowCount(len(players))
|
||||||
for row, player in enumerate(players):
|
for row, player in enumerate(players):
|
||||||
play_item = QtWidgets.QTableWidgetItem(player.name)
|
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)
|
play_item.setData(Qt.ItemDataRole.UserRole, player.id)
|
||||||
table.setItem(row, 0, play_item)
|
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.setSortingEnabled(True)
|
||||||
table.sortItems(0, Qt.SortOrder.AscendingOrder)
|
table.sortItems(0, Qt.SortOrder.AscendingOrder)
|
||||||
table.resizeColumnsToContents()
|
table.resizeColumnsToContents()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue