diff --git a/src/warchron/constants.py b/src/warchron/constants.py
index d4f69e9..670a466 100644
--- a/src/warchron/constants.py
+++ b/src/warchron/constants.py
@@ -1,7 +1,6 @@
from enum import StrEnum
from enum import Enum, auto
from pathlib import Path
-from typing import Dict
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QIcon
@@ -41,7 +40,7 @@ class IconName(str, Enum):
class Icons:
- _cache: Dict[str, QIcon] = {}
+ _cache: dict[str, QIcon] = {}
_paths = {
IconName.UNDO: "arrow-curve-180-left",
diff --git a/src/warchron/controller/closure_workflow.py b/src/warchron/controller/closure_workflow.py
deleted file mode 100644
index 71a1e3d..0000000
--- a/src/warchron/controller/closure_workflow.py
+++ /dev/null
@@ -1,77 +0,0 @@
-from typing import TYPE_CHECKING
-
-if TYPE_CHECKING:
- from warchron.controller.app_controller import AppController
-
-from warchron.constants import ContextType
-from warchron.model.exception import ForbiddenOperation
-from warchron.model.war_event import TieResolved
-from warchron.model.war import War
-from warchron.model.campaign import Campaign
-from warchron.model.battle import Battle
-from warchron.model.round import Round
-from warchron.model.closure_service import ClosureService
-from warchron.model.tie_manager import TieResolver
-from warchron.controller.dtos import TieContext
-
-
-class ClosureWorkflow:
-
- def __init__(self, controller: "AppController"):
- self.app = controller
-
-
-class RoundClosureWorkflow(ClosureWorkflow):
-
- def start(self, war: War, campaign: Campaign, round: Round) -> None:
- ClosureService.check_round_closable(round)
- ties = TieResolver.find_round_ties(round, war)
- while ties:
- contexts = [
- RoundClosureWorkflow.build_battle_context(campaign, b) for b in ties
- ]
- resolvable = []
- for ctx in contexts:
- if TieResolver.can_tie_be_resolved(war, ctx.participants):
- resolvable.append(ctx)
- else:
- war.events.append(
- TieResolved(
- participant_id=None,
- context_type=ctx.context_type,
- context_id=ctx.context_id,
- )
- )
- if not resolvable:
- break
- bids_map = self.app.rounds.resolve_ties(war, contexts)
- for ctx in contexts:
- bids = bids_map[ctx.context_id]
- TieResolver.apply_bids(
- war,
- ctx.context_type,
- ctx.context_id,
- bids,
- )
- TieResolver.try_tie_break(
- war,
- ctx.context_type,
- ctx.context_id,
- ctx.participants,
- )
- ties = TieResolver.find_round_ties(round, war)
- for battle in round.battles.values():
- ClosureService.apply_battle_outcomes(war, campaign, battle)
- ClosureService.finalize_round(round)
-
- @staticmethod
- def build_battle_context(campaign: Campaign, battle: Battle) -> TieContext:
- if battle.player_1_id is None or battle.player_2_id is None:
- raise ForbiddenOperation("Missing player(s) in this battle context.")
- p1 = campaign.participants[battle.player_1_id].war_participant_id
- p2 = campaign.participants[battle.player_2_id].war_participant_id
- return TieContext(
- context_type=ContextType.BATTLE,
- context_id=battle.sector_id,
- participants=[p1, p2],
- )
diff --git a/src/warchron/controller/dtos.py b/src/warchron/controller/dtos.py
index 454aa61..0e1b48a 100644
--- a/src/warchron/controller/dtos.py
+++ b/src/warchron/controller/dtos.py
@@ -3,8 +3,6 @@ from dataclasses import dataclass
from PyQt6.QtGui import QIcon
-from warchron.constants import ContextType
-
@dataclass(frozen=True)
class ParticipantOption:
@@ -105,10 +103,3 @@ class BattleDTO:
state_icon: QIcon | None
player1_icon: QIcon | None
player2_icon: QIcon | None
-
-
-@dataclass
-class TieContext:
- context_type: ContextType
- context_id: str
- participants: List[str] # war_participant_ids
diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py
index 429d5ec..93e39ae 100644
--- a/src/warchron/controller/round_controller.py
+++ b/src/warchron/controller/round_controller.py
@@ -1,27 +1,16 @@
-from typing import List, Dict, TYPE_CHECKING
+from typing import List, TYPE_CHECKING
-from PyQt6.QtWidgets import QDialog
-from PyQt6.QtWidgets import QMessageBox
+from PyQt6.QtWidgets import QDialog, QMessageBox
from warchron.constants import ItemType, RefreshScope, Icons, IconName
-from warchron.model.exception import ForbiddenOperation, DomainError
from warchron.model.round import Round
-from warchron.model.war import War
if TYPE_CHECKING:
from warchron.controller.app_controller import AppController
-
-from warchron.controller.dtos import (
- ParticipantOption,
- SectorDTO,
- ChoiceDTO,
- BattleDTO,
- TieContext,
-)
-from warchron.controller.closure_workflow import RoundClosureWorkflow
+from warchron.controller.dtos import ParticipantOption, SectorDTO, ChoiceDTO, BattleDTO
+from warchron.model.closure_service import ClosureService
from warchron.view.choice_dialog import ChoiceDialog
from warchron.view.battle_dialog import BattleDialog
-from warchron.view.tie_dialog import TieDialog
class RoundController:
@@ -134,47 +123,26 @@ class RoundController:
if not round_id:
return
rnd = self.app.model.get_round(round_id)
- camp = self.app.model.get_campaign_by_round(round_id)
- war = self.app.model.get_war_by_round(round_id)
- workflow = RoundClosureWorkflow(self.app)
+ if rnd.is_over:
+ return
try:
- workflow.start(war, camp, rnd)
- except DomainError as e:
- QMessageBox.warning(
+ ties = ClosureService.close_round(rnd)
+ except RuntimeError as e:
+ QMessageBox.warning(self.app.view, "Cannot close round", str(e))
+ return
+ if ties:
+ QMessageBox.information(
self.app.view,
- "Deletion forbidden",
- str(e),
+ "Tie detected",
+ "Round has unresolved ties. Resolution system not implemented yet.",
)
+ return
self.app.is_dirty = 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
)
- def resolve_ties(
- self, war: War, contexts: List[TieContext]
- ) -> Dict[str, Dict[str, bool]]:
- bids_map = {}
- for ctx in contexts:
- players = [
- ParticipantOption(
- id=pid,
- name=self.app.model.get_participant_name(pid),
- )
- for pid in ctx.participants
- ]
- counters = [war.get_influence_tokens(pid) for pid in ctx.participants]
- dialog = TieDialog(
- parent=self.app.view,
- players=players,
- counters=counters,
- context_id=ctx.context_id,
- )
- if not dialog.exec():
- raise ForbiddenOperation("Tie resolution cancelled")
- bids_map[ctx.context_id] = dialog.get_bids()
- return bids_map
-
# Choice methods
def edit_round_choice(self, choice_id: str) -> None:
diff --git a/src/warchron/model/battle.py b/src/warchron/model/battle.py
index 1af9b2b..1b4a0e4 100644
--- a/src/warchron/model/battle.py
+++ b/src/warchron/model/battle.py
@@ -19,17 +19,17 @@ class Battle:
self.victory_condition: str | None = None
self.comment: str | None = None
- def set_sector(self, new_sector_id: str) -> None:
- self.sector_id = new_sector_id
+ def set_id(self, new_id: str) -> None:
+ self.sector_id = new_id
- def set_player_1(self, new_camp_part_id: str | None) -> None:
- self.player_1_id = new_camp_part_id
+ def set_player_1(self, new_player_id: str | None) -> None:
+ self.player_1_id = new_player_id
- def set_player_2(self, new_camp_part_id: str | None) -> None:
- self.player_2_id = new_camp_part_id
+ def set_player_2(self, new_player_id: str | None) -> None:
+ self.player_2_id = new_player_id
- def set_winner(self, new_camp_part_id: str | None) -> None:
- self.winner_id = new_camp_part_id
+ def set_winner(self, new_player_id: str | None) -> None:
+ self.winner_id = new_player_id
def set_score(self, new_score: str | None) -> None:
self.score = new_score
diff --git a/src/warchron/model/campaign_participant.py b/src/warchron/model/campaign_participant.py
index 5ccea4e..d7fbc63 100644
--- a/src/warchron/model/campaign_participant.py
+++ b/src/warchron/model/campaign_participant.py
@@ -15,8 +15,8 @@ class CampaignParticipant:
def set_id(self, new_id: str) -> None:
self.id = new_id
- def set_war_participant(self, new_war_part_id: str) -> None:
- self.war_participant_id = new_war_part_id
+ def set_war_participant(self, new_participant: str) -> None:
+ self.war_participant_id = new_participant
def set_leader(self, new_faction: str) -> None:
self.leader = new_faction
diff --git a/src/warchron/model/choice.py b/src/warchron/model/choice.py
index 0ed6aee..f92107a 100644
--- a/src/warchron/model/choice.py
+++ b/src/warchron/model/choice.py
@@ -18,8 +18,8 @@ class Choice:
)
self.comment: str | None = None
- def set_participant(self, new_camp_part_id: str) -> None:
- self.participant_id = new_camp_part_id
+ def set_id(self, new_id: str) -> None:
+ self.participant_id = new_id
def set_priority(self, new_priority_id: str | None) -> None:
self.priority_sector_id = new_priority_id
diff --git a/src/warchron/model/closure_service.py b/src/warchron/model/closure_service.py
index fb31372..a3234a7 100644
--- a/src/warchron/model/closure_service.py
+++ b/src/warchron/model/closure_service.py
@@ -2,69 +2,44 @@ from __future__ import annotations
from typing import List
from warchron.constants import ContextType
-from warchron.model.exception import ForbiddenOperation
-from warchron.model.tie_manager import TieResolver
-from warchron.model.war_event import InfluenceGained
+from warchron.model.tie_manager import ResolutionContext
from warchron.model.war import War
from warchron.model.campaign import Campaign
from warchron.model.round import Round
-from warchron.model.battle import Battle
class ClosureService:
- # Round methods
-
@staticmethod
- def check_round_closable(round: Round) -> None:
- if round.is_over:
- raise ForbiddenOperation("Round already closed")
+ def close_round(round: Round) -> List[ResolutionContext]:
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:
- already_granted = any(
- isinstance(e, InfluenceGained) and e.source == f"battle:{battle.sector_id}"
- for e in war.events
- )
- if already_granted:
- return
- if battle.winner_id is not None:
- base_winner = campaign.participants[battle.winner_id].war_participant_id
- else:
- base_winner = None
- effective_winner = TieResolver.get_effective_winner_id(
- war,
- ContextType.BATTLE,
- battle.sector_id,
- base_winner,
- )
- if effective_winner is None:
- return
- sector = campaign.sectors[battle.sector_id]
- if sector.influence_objective_id and war.influence_token:
- war.events.append(
- InfluenceGained(
- participant_id=effective_winner,
- amount=1,
- source=f"battle:{battle.sector_id}",
+ raise RuntimeError("All battles must be finished to close their round")
+ ties = []
+ for battle in round.battles.values():
+ if battle.is_draw():
+ participants: list[str] = []
+ if battle.player_1_id is not None:
+ participants.append(battle.player_1_id)
+ if battle.player_2_id is not None:
+ participants.append(battle.player_2_id)
+ ties.append(
+ ResolutionContext(
+ context_type=ContextType.BATTLE,
+ context_id=battle.sector_id,
+ # TODO ref to War.participants at some point
+ participant_ids=participants,
+ )
)
- )
-
- @staticmethod
- def finalize_round(round: Round) -> None:
+ if ties:
+ return ties
round.is_over = True
-
- # Campaign methods
+ return []
@staticmethod
- def close_campaign(campaign: Campaign) -> List[str]:
+ def close_campaign(campaign: Campaign) -> List[ResolutionContext]:
if not campaign.all_rounds_finished():
raise RuntimeError("All rounds must be finished to close their campaign")
- ties: List[str] = []
+ ties: List[ResolutionContext] = []
# for round in campaign.rounds:
# # compute score
# # if participants have same score
@@ -84,13 +59,11 @@ class ClosureService:
campaign.is_over = True
return []
- # War methods
-
@staticmethod
- def close_war(war: War) -> List[str]:
+ def close_war(war: War) -> List[ResolutionContext]:
if not war.all_campaigns_finished():
raise RuntimeError("All campaigns must be finished to close their war")
- ties: List[str] = []
+ ties: List[ResolutionContext] = []
# for campaign in war.campaigns:
# # compute score
# # if participants have same score
diff --git a/src/warchron/model/closure_workflow.py b/src/warchron/model/closure_workflow.py
new file mode 100644
index 0000000..db7ee66
--- /dev/null
+++ b/src/warchron/model/closure_workflow.py
@@ -0,0 +1,19 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from warchron.model.war import War
+ from warchron.model.war import War
+ from warchron.model.war import War
+ from warchron.model.closure_service import ClosureService
+
+
+class RoundClosureWorkflow:
+
+ def close_round(self, round_id):
+ rnd = repo.get_round(round_id)
+
+ ties = ClosureService.close_round(rnd)
+
+ repo.save()
+
+ return ties
diff --git a/src/warchron/model/influence_service.py b/src/warchron/model/influence_service.py
new file mode 100644
index 0000000..1ed4e4d
--- /dev/null
+++ b/src/warchron/model/influence_service.py
@@ -0,0 +1,23 @@
+from warchron.model.war import War
+from warchron.model.campaign import Campaign
+from warchron.model.battle import Battle
+from warchron.model.war_event import InfluenceGained
+
+
+class InfluenceService:
+
+ @staticmethod
+ def apply_battle_result(war: War, campaign: Campaign, battle: Battle) -> None:
+ if battle.winner_id is None:
+ return
+ sector = campaign.sectors[battle.sector_id]
+ # if sector grants influence
+ if sector.influence_objective_id and war.influence_token:
+ participant = war.participants[battle.winner_id]
+ participant.events.append(
+ InfluenceGained(
+ participant_id=participant.id,
+ amount=1,
+ source=f"battle:{battle.sector_id}",
+ )
+ )
diff --git a/src/warchron/model/score_service.py b/src/warchron/model/score_service.py
index 298139f..2bfda9b 100644
--- a/src/warchron/model/score_service.py
+++ b/src/warchron/model/score_service.py
@@ -1,4 +1,4 @@
-from typing import Dict, TYPE_CHECKING
+from typing import TYPE_CHECKING
if TYPE_CHECKING:
from warchron.model.war import War
@@ -23,8 +23,8 @@ class ScoreService:
@staticmethod
def compute_narrative_points_for_participant(
war: "War", participant_id: str
- ) -> Dict[str, int]:
- totals: Dict[str, int] = {}
+ ) -> dict[str, int]:
+ totals: dict[str, int] = {}
for obj_id in war.objectives:
totals[obj_id] = 0
for campaign in war.campaigns:
diff --git a/src/warchron/model/tie_manager.py b/src/warchron/model/tie_manager.py
index da031f9..84c81b4 100644
--- a/src/warchron/model/tie_manager.py
+++ b/src/warchron/model/tie_manager.py
@@ -1,108 +1,46 @@
-from typing import List, Dict
-
-from warchron.constants import ContextType
-from warchron.model.exception import ForbiddenOperation
from warchron.model.war import War
-from warchron.model.round import Round
-from warchron.model.battle import Battle
-from warchron.model.war_event import InfluenceSpent, TieResolved
+from warchron.model.war_event import InfluenceSpent
+
+
+class ResolutionContext:
+ def __init__(self, context_type: str, context_id: str, participant_ids: list[str]):
+ self.context_type = context_type
+ self.context_id = context_id
+ self.participant_ids = participant_ids
+
+ self.current_bids: dict[str, int] = {}
+ self.round_index: int = 0
+ self.is_resolved: bool = False
class TieResolver:
@staticmethod
- def find_round_ties(round: Round, war: War) -> List[Battle]:
- ties = []
- for battle in round.battles.values():
- if not battle.is_draw():
- continue
- resolved = any(
- isinstance(e, TieResolved)
- and e.context_type == ContextType.BATTLE
- and e.context_id == battle.sector_id
- for e in war.events
- )
- if not resolved:
- ties.append(battle)
- return ties
-
- @staticmethod
- def apply_bids(
+ def resolve(
war: War,
- context_type: ContextType,
- context_id: str,
- bids: Dict[str, bool], # war_participant_id -> spend?
- ) -> None:
- for war_part_id, spend in bids.items():
- if not spend:
- continue
- if war.get_influence_tokens(war_part_id) < 1:
- raise ForbiddenOperation("Not enough tokens")
- war.events.append(
- InfluenceSpent(
- participant_id=war_part_id,
- amount=1,
- context_type=context_type,
- )
- )
-
- @staticmethod
- def try_tie_break(
- war: War,
- context_type: ContextType,
- context_id: str,
- participants: List[str], # war_participant_ids
- ) -> bool:
- spent: Dict[str, int] = {}
- for war_part_id in participants:
- spent[war_part_id] = sum(
- e.amount
- for e in war.events
- if isinstance(e, InfluenceSpent)
- and e.participant_id == war_part_id
- and e.context_type == context_type
- )
- values = set(spent.values())
- if values == {0}: # no bid = confirmed draw
- war.events.append(
- TieResolved(
- participant_id=None,
- context_type=context_type,
- context_id=context_id,
- )
- )
- return True
- if len(values) == 1: # tie again, continue
- return False
- winner = max(spent.items(), key=lambda item: item[1])[0]
- war.events.append(
- TieResolved(
- participant_id=winner,
- context_type=context_type,
- context_id=context_id,
- )
- )
- return True
-
- @staticmethod
- def can_tie_be_resolved(war: War, participants: List[str]) -> bool:
- return any(war.get_influence_tokens(pid) > 0 for pid in participants)
-
- @staticmethod
- def get_effective_winner_id(
- war: War,
- context_type: ContextType,
- context_id: str,
- base_winner_id: str | None,
+ context: ResolutionContext,
+ bids: dict[str, int],
) -> str | None:
- if base_winner_id is not None:
- return base_winner_id
- for ev in reversed(war.events):
- if (
- isinstance(ev, TieResolved)
- and ev.context_type == context_type
- and ev.context_id == context_id
- ):
- return ev.participant_id # None if confirmed draw
-
+ # verify available token for each player
+ for pid, amount in bids.items():
+ participant = war.participants[pid]
+ if participant.influence_tokens() < amount:
+ raise ValueError("Not enough influence tokens")
+ # apply spending
+ for pid, amount in bids.items():
+ if amount > 0:
+ war.participants[pid].events.append(
+ InfluenceSpent(
+ participant_id=pid,
+ amount=amount,
+ context=context.context_type,
+ )
+ )
+ # determine winner
+ max_bid = max(bids.values())
+ winners = [pid for pid, b in bids.items() if b == max_bid]
+ if len(winners) == 1:
+ context.is_resolved = True
+ return winners[0]
+ # persisting tie → None
return None
diff --git a/src/warchron/model/war.py b/src/warchron/model/war.py
index 93e9cc5..1f7d068 100644
--- a/src/warchron/model/war.py
+++ b/src/warchron/model/war.py
@@ -3,7 +3,6 @@ from uuid import uuid4
from datetime import datetime
from typing import Any, Dict, List
-from warchron.model.war_event import WarEvent, InfluenceGained, InfluenceSpent
from warchron.model.exception import ForbiddenOperation
from warchron.model.war_participant import WarParticipant
from warchron.model.objective import Objective
@@ -26,7 +25,6 @@ class War:
self.participants: Dict[str, WarParticipant] = {}
self.objectives: Dict[str, Objective] = {}
self.campaigns: List[Campaign] = []
- self.events: List[WarEvent] = []
self.is_over: bool = False
def set_id(self, new_id: str) -> None:
@@ -55,9 +53,6 @@ class War:
def set_influence_token(self, new_state: bool) -> None:
if self.is_over:
raise ForbiddenOperation("Can't set influence token of a closed war.")
- # TODO raise RequiresConfirmation
- # * disable: cleanup if any token has already been gained/spent
- # * enable: retrigger battle_outcomes and resolve tie again if any draw
self.influence_token = new_state
def set_state(self, new_state: bool) -> None:
@@ -74,7 +69,6 @@ class War:
"participants": [part.toDict() for part in self.participants.values()],
"objectives": [obj.toDict() for obj in self.objectives.values()],
"campaigns": [camp.toDict() for camp in self.campaigns],
- "events": [ev.toDict() for ev in self.events],
"is_over": self.is_over,
}
@@ -93,8 +87,6 @@ class War:
war.objectives[obj.id] = obj
for camp_data in data.get("campaigns", []):
war.campaigns.append(Campaign.fromDict(camp_data))
- for ev_data in data.get("events", []):
- war.events.append(WarEvent.fromDict(ev_data))
war.set_state(data.get("is_over", False))
return war
@@ -242,13 +234,11 @@ class War:
return camp
raise KeyError(f"Sector {sector_id} not found in any Campaign")
- def get_campaign_by_campaign_participant(
- self, participant_id: str
- ) -> Campaign | None:
+ def get_campaign_by_campaign_participant(self, participant_id: str) -> Campaign:
for camp in self.campaigns:
if camp.has_participant(participant_id):
return camp
- return None
+ raise KeyError(f"Participant {participant_id} not found in any Campaign")
def update_campaign(self, campaign_id: str, *, name: str, month: int) -> None:
if self.is_over:
@@ -361,13 +351,11 @@ class War:
self, participant_id: str, *, leader: str, theme: str
) -> None:
camp = self.get_campaign_by_campaign_participant(participant_id)
- if camp is not None:
- camp.update_campaign_participant(participant_id, leader=leader, theme=theme)
+ camp.update_campaign_participant(participant_id, leader=leader, theme=theme)
def remove_campaign_participant(self, participant_id: str) -> None:
camp = self.get_campaign_by_campaign_participant(participant_id)
- if camp is not None:
- camp.remove_campaign_participant(participant_id)
+ camp.remove_campaign_participant(participant_id)
# Round methods
@@ -447,18 +435,3 @@ class War:
camp = self.get_campaign_by_round(round_id)
if camp is not None:
camp.remove_battle(round_id, sector_id)
-
- # Event methods
-
- def get_influence_tokens(self, participant_id: str) -> int:
- gained = sum(
- e.amount
- for e in self.events
- if isinstance(e, InfluenceGained) and e.participant_id == participant_id
- )
- spent = sum(
- e.amount
- for e in self.events
- if isinstance(e, InfluenceSpent) and e.participant_id == participant_id
- )
- return gained - spent
diff --git a/src/warchron/model/war_event.py b/src/warchron/model/war_event.py
index 72c03f3..c4092c0 100644
--- a/src/warchron/model/war_event.py
+++ b/src/warchron/model/war_event.py
@@ -1,143 +1,30 @@
-from __future__ import annotations
-from typing import Dict, Any, TypeVar, Type, cast
from datetime import datetime
from uuid import uuid4
-T = TypeVar("T", bound="WarEvent")
-EVENT_REGISTRY: Dict[str, Type["WarEvent"]] = {}
-
-
-def register_event(cls: Type[T]) -> Type[T]:
- EVENT_REGISTRY[cls.TYPE] = cls
- return cls
-
class WarEvent:
- TYPE = "WarEvent"
-
- def __init__(self, participant_id: str | None = None):
+ def __init__(self, participant_id: str):
self.id: str = str(uuid4())
- self.participant_id: str | None = participant_id
+ self.participant_id: str = participant_id
self.timestamp: datetime = datetime.now()
- def set_id(self, new_id: str) -> None:
- self.id = new_id
- def set_participant(self, new_war_part_id: str | None) -> None:
- self.participant_id = new_war_part_id
-
- def set_timestamp(self, new_timestamp: datetime) -> None:
- self.timestamp = new_timestamp
-
- def toDict(self) -> Dict[str, Any]:
- return {
- "type": self.TYPE,
- "id": self.id,
- "participant_id": self.participant_id,
- "timestamp": self.timestamp.isoformat(),
- }
-
- @classmethod
- def _base_fromDict(cls: Type[T], ev: T, data: Dict[str, Any]) -> T:
- ev.id = data["id"]
- ev.participant_id = data["participant_id"]
- ev.timestamp = datetime.fromisoformat(data["timestamp"])
- return ev
-
- @staticmethod
- def fromDict(data: Dict[str, Any]) -> "WarEvent":
- ev_type = data["type"]
- cls = cast(Type[WarEvent], EVENT_REGISTRY.get(ev_type))
- if cls is None:
- raise ValueError(f"Unknown event type: {ev_type}")
- return cls.fromDict(data)
-
-
-@register_event
class TieResolved(WarEvent):
- TYPE = "TieResolved"
-
- def __init__(self, participant_id: str | None, context_type: str, context_id: str):
+ def __init__(self, participant_id: str, context_type: str, context_id: str):
super().__init__(participant_id)
- self.participant_id: str | None = (
- participant_id # winner or None (confirmed tie)
- )
self.context_type = context_type # battle, round, campaign, war
self.context_id = context_id
- def toDict(self) -> Dict[str, Any]:
- d = super().toDict()
- d.update(
- {
- "context_type": self.context_type,
- "context_id": self.context_id,
- }
- )
- return d
- @classmethod
- def fromDict(cls, data: Dict[str, Any]) -> TieResolved:
- ev = cls(
- data["participant_id"],
- data["context_type"],
- data["context_id"],
- )
- return cls._base_fromDict(ev, data)
-
-
-@register_event
class InfluenceGained(WarEvent):
- TYPE = "InfluenceGained"
-
def __init__(self, participant_id: str, amount: int, source: str):
super().__init__(participant_id)
self.amount = amount
self.source = source # "battle", "tie_resolution", etc.
- def toDict(self) -> Dict[str, Any]:
- d = super().toDict()
- d.update(
- {
- "amount": self.amount,
- "source": self.source,
- }
- )
- return d
- @classmethod
- def fromDict(cls, data: Dict[str, Any]) -> InfluenceGained:
- ev = cls(
- data["participant_id"],
- data["amount"],
- data["source"],
- )
- return cls._base_fromDict(ev, data)
-
-
-@register_event
class InfluenceSpent(WarEvent):
- TYPE = "InfluenceSpent"
-
- def __init__(self, participant_id: str, amount: int, context_type: str):
+ def __init__(self, participant_id: str, amount: int, context: str):
super().__init__(participant_id)
self.amount = amount
- self.context_type = context_type # "battle_tie", "campaign_tie", etc.
-
- def toDict(self) -> Dict[str, Any]:
- d = super().toDict()
- d.update(
- {
- "amount": self.amount,
- "context_type": self.context_type,
- }
- )
- return d
-
- @classmethod
- def fromDict(cls, data: Dict[str, Any]) -> InfluenceSpent:
- ev = cls(
- data["participant_id"],
- data["amount"],
- data["context_type"],
- )
- return cls._base_fromDict(ev, data)
+ self.context = context # "battle_tie", "campaign_tie", etc.
diff --git a/src/warchron/model/war_participant.py b/src/warchron/model/war_participant.py
index 8708572..78f304e 100644
--- a/src/warchron/model/war_participant.py
+++ b/src/warchron/model/war_participant.py
@@ -1,6 +1,12 @@
from __future__ import annotations
from typing import Any, Dict
from uuid import uuid4
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from warchron.model.war import War
+from warchron.model.war_event import WarEvent, InfluenceSpent, InfluenceGained
+from warchron.model.score_service import ScoreService
class WarParticipant:
@@ -8,12 +14,13 @@ class WarParticipant:
self.id: str = str(uuid4())
self.player_id: str = player_id # ref to Model.players
self.faction: str = faction
+ self.events: list[WarEvent] = []
def set_id(self, new_id: str) -> None:
self.id = new_id
- def set_player(self, new_player_id: str) -> None:
- self.player_id = new_player_id
+ def set_player(self, new_player: str) -> None:
+ self.player_id = new_player
def set_faction(self, new_faction: str) -> None:
self.faction = new_faction
@@ -33,3 +40,16 @@ class WarParticipant:
)
part.set_id(data["id"])
return part
+
+ # Computed properties
+
+ def influence_tokens(self) -> int:
+ gained = sum(e.amount for e in self.events if isinstance(e, InfluenceGained))
+ spent = sum(e.amount for e in self.events if isinstance(e, InfluenceSpent))
+ return gained - spent
+
+ def victory_points(self, war: "War") -> int:
+ return ScoreService.compute_victory_points_for_participant(war, self.id)
+
+ def narrative_points(self, war: "War") -> Dict[str, int]:
+ return ScoreService.compute_narrative_points_for_participant(war, self.id)
diff --git a/src/warchron/view/tie_dialog.py b/src/warchron/view/tie_dialog.py
deleted file mode 100644
index 495033c..0000000
--- a/src/warchron/view/tie_dialog.py
+++ /dev/null
@@ -1,40 +0,0 @@
-from typing import List, Dict
-
-from PyQt6.QtWidgets import QWidget, QDialog
-
-from warchron.constants import Icons, IconName
-from warchron.controller.dtos import ParticipantOption
-from warchron.view.ui.ui_tie_dialog import Ui_tieDialog
-
-
-class TieDialog(QDialog):
- def __init__(
- self,
- parent: QWidget | None = None,
- *,
- players: List[ParticipantOption],
- counters: List[int],
- context_id: str,
- ) -> None:
- super().__init__(parent)
- self._context_id = context_id
- self._p1_id = players[0].id
- self._p2_id = players[1].id
- self.ui: Ui_tieDialog = Ui_tieDialog()
- self.ui.setupUi(self) # type: ignore
- self.ui.tieContext.setText("Battle tie") # Change with context
- self.ui.groupBox_1.setTitle(players[0].name)
- self.ui.groupBox_2.setTitle(players[1].name)
- self.ui.tokenCount_1.setText(str(counters[0]))
- self.ui.tokenCount_2.setText(str(counters[1]))
- if counters[0] < 1:
- self.ui.tokenSpend_1.setDisabled(True)
- if counters[1] < 1:
- self.ui.tokenSpend_2.setDisabled(True)
- self.setWindowIcon(Icons.get(IconName.WARCHRON))
-
- def get_bids(self) -> Dict[str, bool]:
- return {
- self._p1_id: self.ui.tokenSpend_1.isChecked(),
- self._p2_id: self.ui.tokenSpend_2.isChecked(),
- }
diff --git a/src/warchron/view/ui/ui_battle_dialog.py b/src/warchron/view/ui/ui_battle_dialog.py
index c55f4c5..7e50a17 100644
--- a/src/warchron/view/ui/ui_battle_dialog.py
+++ b/src/warchron/view/ui/ui_battle_dialog.py
@@ -117,7 +117,7 @@ class Ui_battleDialog(object):
def retranslateUi(self, battleDialog):
_translate = QtCore.QCoreApplication.translate
- battleDialog.setWindowTitle(_translate("battleDialog", "Battle"))
+ battleDialog.setWindowTitle(_translate("battleDialog", "Battle result"))
self.label_7.setText(_translate("battleDialog", "Sector"))
self.label_5.setText(_translate("battleDialog", "Player 1"))
self.label_6.setText(_translate("battleDialog", "Player 2"))
diff --git a/src/warchron/view/ui/ui_battle_dialog.ui b/src/warchron/view/ui/ui_battle_dialog.ui
index 4fbe3ac..47c1ffb 100644
--- a/src/warchron/view/ui/ui_battle_dialog.ui
+++ b/src/warchron/view/ui/ui_battle_dialog.ui
@@ -14,7 +14,7 @@
- Battle
+ Battle result
diff --git a/src/warchron/view/ui/ui_choice_dialog.py b/src/warchron/view/ui/ui_choice_dialog.py
index 4c1deaa..cffcef6 100644
--- a/src/warchron/view/ui/ui_choice_dialog.py
+++ b/src/warchron/view/ui/ui_choice_dialog.py
@@ -71,7 +71,7 @@ class Ui_choiceDialog(object):
def retranslateUi(self, choiceDialog):
_translate = QtCore.QCoreApplication.translate
- choiceDialog.setWindowTitle(_translate("choiceDialog", "Choice"))
+ choiceDialog.setWindowTitle(_translate("choiceDialog", "Choices"))
self.label.setText(_translate("choiceDialog", "Player"))
self.label_2.setText(_translate("choiceDialog", "Priority"))
self.label_3.setText(_translate("choiceDialog", "Secondary"))
diff --git a/src/warchron/view/ui/ui_choice_dialog.ui b/src/warchron/view/ui/ui_choice_dialog.ui
index 24142ec..a6eff58 100644
--- a/src/warchron/view/ui/ui_choice_dialog.ui
+++ b/src/warchron/view/ui/ui_choice_dialog.ui
@@ -14,7 +14,7 @@
- Choice
+ Choices
diff --git a/src/warchron/view/ui/ui_tie_dialog.py b/src/warchron/view/ui/ui_tie_dialog.py
deleted file mode 100644
index 1bfc512..0000000
--- a/src/warchron/view/ui/ui_tie_dialog.py
+++ /dev/null
@@ -1,118 +0,0 @@
-# Form implementation generated from reading ui file '.\src\warchron\view\ui\ui_tie_dialog.ui'
-#
-# Created by: PyQt6 UI code generator 6.7.1
-#
-# WARNING: Any manual changes made to this file will be lost when pyuic6 is
-# run again. Do not edit this file unless you know what you are doing.
-
-
-from PyQt6 import QtCore, QtGui, QtWidgets
-
-
-class Ui_tieDialog(object):
- def setupUi(self, tieDialog):
- tieDialog.setObjectName("tieDialog")
- tieDialog.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
- tieDialog.resize(477, 174)
- icon = QtGui.QIcon()
- icon.addPixmap(QtGui.QPixmap(".\\src\\warchron\\view\\ui\\../resources/warchron_logo.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
- tieDialog.setWindowIcon(icon)
- self.verticalLayout_5 = QtWidgets.QVBoxLayout(tieDialog)
- self.verticalLayout_5.setObjectName("verticalLayout_5")
- self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
- self.horizontalLayout_3.setObjectName("horizontalLayout_3")
- self.tieContext = QtWidgets.QLabel(parent=tieDialog)
- font = QtGui.QFont()
- font.setPointSize(10)
- font.setBold(True)
- font.setWeight(75)
- self.tieContext.setFont(font)
- self.tieContext.setObjectName("tieContext")
- self.horizontalLayout_3.addWidget(self.tieContext)
- spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
- self.horizontalLayout_3.addItem(spacerItem)
- self.verticalLayout_5.addLayout(self.horizontalLayout_3)
- self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
- self.horizontalLayout_4.setObjectName("horizontalLayout_4")
- self.groupBox_1 = QtWidgets.QGroupBox(parent=tieDialog)
- self.groupBox_1.setObjectName("groupBox_1")
- self.horizontalLayout = QtWidgets.QHBoxLayout(self.groupBox_1)
- self.horizontalLayout.setObjectName("horizontalLayout")
- self.verticalLayout = QtWidgets.QVBoxLayout()
- self.verticalLayout.setObjectName("verticalLayout")
- self.label_5 = QtWidgets.QLabel(parent=self.groupBox_1)
- self.label_5.setObjectName("label_5")
- self.verticalLayout.addWidget(self.label_5)
- self.label_2 = QtWidgets.QLabel(parent=self.groupBox_1)
- self.label_2.setObjectName("label_2")
- self.verticalLayout.addWidget(self.label_2)
- self.horizontalLayout.addLayout(self.verticalLayout)
- self.verticalLayout_2 = QtWidgets.QVBoxLayout()
- self.verticalLayout_2.setObjectName("verticalLayout_2")
- self.tokenSpend_1 = QtWidgets.QCheckBox(parent=self.groupBox_1)
- self.tokenSpend_1.setText("")
- self.tokenSpend_1.setObjectName("tokenSpend_1")
- self.verticalLayout_2.addWidget(self.tokenSpend_1)
- self.tokenCount_1 = QtWidgets.QLineEdit(parent=self.groupBox_1)
- self.tokenCount_1.setEnabled(False)
- self.tokenCount_1.setObjectName("tokenCount_1")
- self.verticalLayout_2.addWidget(self.tokenCount_1)
- self.horizontalLayout.addLayout(self.verticalLayout_2)
- self.horizontalLayout_4.addWidget(self.groupBox_1)
- self.groupBox_2 = QtWidgets.QGroupBox(parent=tieDialog)
- self.groupBox_2.setObjectName("groupBox_2")
- self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.groupBox_2)
- self.horizontalLayout_2.setObjectName("horizontalLayout_2")
- self.verticalLayout_3 = QtWidgets.QVBoxLayout()
- self.verticalLayout_3.setObjectName("verticalLayout_3")
- self.label_6 = QtWidgets.QLabel(parent=self.groupBox_2)
- self.label_6.setObjectName("label_6")
- self.verticalLayout_3.addWidget(self.label_6)
- self.label_3 = QtWidgets.QLabel(parent=self.groupBox_2)
- self.label_3.setObjectName("label_3")
- self.verticalLayout_3.addWidget(self.label_3)
- self.horizontalLayout_2.addLayout(self.verticalLayout_3)
- self.verticalLayout_4 = QtWidgets.QVBoxLayout()
- self.verticalLayout_4.setObjectName("verticalLayout_4")
- self.tokenSpend_2 = QtWidgets.QCheckBox(parent=self.groupBox_2)
- self.tokenSpend_2.setText("")
- self.tokenSpend_2.setObjectName("tokenSpend_2")
- self.verticalLayout_4.addWidget(self.tokenSpend_2)
- self.tokenCount_2 = QtWidgets.QLineEdit(parent=self.groupBox_2)
- self.tokenCount_2.setEnabled(False)
- self.tokenCount_2.setObjectName("tokenCount_2")
- self.verticalLayout_4.addWidget(self.tokenCount_2)
- self.horizontalLayout_2.addLayout(self.verticalLayout_4)
- self.horizontalLayout_4.addWidget(self.groupBox_2)
- self.verticalLayout_5.addLayout(self.horizontalLayout_4)
- self.buttonBox = QtWidgets.QDialogButtonBox(parent=tieDialog)
- self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal)
- self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.StandardButton.Cancel|QtWidgets.QDialogButtonBox.StandardButton.Ok)
- self.buttonBox.setObjectName("buttonBox")
- self.verticalLayout_5.addWidget(self.buttonBox)
-
- self.retranslateUi(tieDialog)
- self.buttonBox.accepted.connect(tieDialog.accept) # type: ignore
- self.buttonBox.rejected.connect(tieDialog.reject) # type: ignore
- QtCore.QMetaObject.connectSlotsByName(tieDialog)
-
- def retranslateUi(self, tieDialog):
- _translate = QtCore.QCoreApplication.translate
- tieDialog.setWindowTitle(_translate("tieDialog", "Tie"))
- self.tieContext.setText(_translate("tieDialog", "Battle tie"))
- self.groupBox_1.setTitle(_translate("tieDialog", "Player 1"))
- self.label_5.setText(_translate("tieDialog", "Spend token"))
- self.label_2.setText(_translate("tieDialog", "Remaining token(s)"))
- self.groupBox_2.setTitle(_translate("tieDialog", "Player 2"))
- self.label_6.setText(_translate("tieDialog", "Spend token"))
- self.label_3.setText(_translate("tieDialog", "Remaining token(s)"))
-
-
-if __name__ == "__main__":
- import sys
- app = QtWidgets.QApplication(sys.argv)
- tieDialog = QtWidgets.QDialog()
- ui = Ui_tieDialog()
- ui.setupUi(tieDialog)
- tieDialog.show()
- sys.exit(app.exec())
diff --git a/src/warchron/view/ui/ui_tie_dialog.ui b/src/warchron/view/ui/ui_tie_dialog.ui
deleted file mode 100644
index ccceb54..0000000
--- a/src/warchron/view/ui/ui_tie_dialog.ui
+++ /dev/null
@@ -1,196 +0,0 @@
-
-
- tieDialog
-
-
- Qt::ApplicationModal
-
-
-
- 0
- 0
- 477
- 174
-
-
-
- Tie
-
-
-
- ../resources/warchron_logo.png../resources/warchron_logo.png
-
-
- -
-
-
-
-
-
-
- 10
- 75
- true
-
-
-
- Battle tie
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
- -
-
-
-
-
-
- Player 1
-
-
-
-
-
-
-
-
-
- Spend token
-
-
-
- -
-
-
- Remaining token(s)
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
- false
-
-
-
-
-
-
-
-
- -
-
-
- Player 2
-
-
-
-
-
-
-
-
-
- Spend token
-
-
-
- -
-
-
- Remaining token(s)
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
- -
-
-
- false
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- tieDialog
- accept()
-
-
- 248
- 254
-
-
- 157
- 274
-
-
-
-
- buttonBox
- rejected()
- tieDialog
- reject()
-
-
- 316
- 260
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/warchron/view/view.py b/src/warchron/view/view.py
index 15e1e29..5950dae 100644
--- a/src/warchron/view/view.py
+++ b/src/warchron/view/view.py
@@ -1,5 +1,5 @@
from __future__ import annotations
-from typing import Callable, List, Dict
+from typing import Callable, List
from pathlib import Path
import calendar
@@ -288,7 +288,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
if item is not None and walk(item):
return
- def get_selected_tree_item(self) -> Dict[str, str] | None:
+ def get_selected_tree_item(self) -> dict[str, str] | None:
item = self.warsTree.currentItem()
if not item:
return None