detect and resolve battle tie with influence_token
This commit is contained in:
parent
115ddf8d50
commit
818d2886f4
23 changed files with 808 additions and 172 deletions
|
|
@ -1,6 +1,7 @@
|
|||
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
|
||||
|
|
@ -40,7 +41,7 @@ class IconName(str, Enum):
|
|||
|
||||
|
||||
class Icons:
|
||||
_cache: dict[str, QIcon] = {}
|
||||
_cache: Dict[str, QIcon] = {}
|
||||
|
||||
_paths = {
|
||||
IconName.UNDO: "arrow-curve-180-left",
|
||||
|
|
|
|||
77
src/warchron/controller/closure_workflow.py
Normal file
77
src/warchron/controller/closure_workflow.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
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],
|
||||
)
|
||||
|
|
@ -3,6 +3,8 @@ from dataclasses import dataclass
|
|||
|
||||
from PyQt6.QtGui import QIcon
|
||||
|
||||
from warchron.constants import ContextType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParticipantOption:
|
||||
|
|
@ -103,3 +105,10 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,16 +1,27 @@
|
|||
from typing import List, TYPE_CHECKING
|
||||
from typing import List, Dict, TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtWidgets import QDialog, QMessageBox
|
||||
from PyQt6.QtWidgets import QDialog
|
||||
from PyQt6.QtWidgets import 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
|
||||
from warchron.model.closure_service import ClosureService
|
||||
|
||||
from warchron.controller.dtos import (
|
||||
ParticipantOption,
|
||||
SectorDTO,
|
||||
ChoiceDTO,
|
||||
BattleDTO,
|
||||
TieContext,
|
||||
)
|
||||
from warchron.controller.closure_workflow import RoundClosureWorkflow
|
||||
from warchron.view.choice_dialog import ChoiceDialog
|
||||
from warchron.view.battle_dialog import BattleDialog
|
||||
from warchron.view.tie_dialog import TieDialog
|
||||
|
||||
|
||||
class RoundController:
|
||||
|
|
@ -123,26 +134,47 @@ class RoundController:
|
|||
if not round_id:
|
||||
return
|
||||
rnd = self.app.model.get_round(round_id)
|
||||
if rnd.is_over:
|
||||
return
|
||||
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:
|
||||
ties = ClosureService.close_round(rnd)
|
||||
except RuntimeError as e:
|
||||
QMessageBox.warning(self.app.view, "Cannot close round", str(e))
|
||||
return
|
||||
if ties:
|
||||
QMessageBox.information(
|
||||
workflow.start(war, camp, rnd)
|
||||
except DomainError as e:
|
||||
QMessageBox.warning(
|
||||
self.app.view,
|
||||
"Tie detected",
|
||||
"Round has unresolved ties. Resolution system not implemented yet.",
|
||||
"Deletion forbidden",
|
||||
str(e),
|
||||
)
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -19,17 +19,17 @@ class Battle:
|
|||
self.victory_condition: str | None = None
|
||||
self.comment: str | None = None
|
||||
|
||||
def set_id(self, new_id: str) -> None:
|
||||
self.sector_id = new_id
|
||||
def set_sector(self, new_sector_id: str) -> None:
|
||||
self.sector_id = new_sector_id
|
||||
|
||||
def set_player_1(self, new_player_id: str | None) -> None:
|
||||
self.player_1_id = new_player_id
|
||||
def set_player_1(self, new_camp_part_id: str | None) -> None:
|
||||
self.player_1_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_player_2(self, new_camp_part_id: str | None) -> None:
|
||||
self.player_2_id = new_camp_part_id
|
||||
|
||||
def set_winner(self, new_player_id: str | None) -> None:
|
||||
self.winner_id = new_player_id
|
||||
def set_winner(self, new_camp_part_id: str | None) -> None:
|
||||
self.winner_id = new_camp_part_id
|
||||
|
||||
def set_score(self, new_score: str | None) -> None:
|
||||
self.score = new_score
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ class CampaignParticipant:
|
|||
def set_id(self, new_id: str) -> None:
|
||||
self.id = new_id
|
||||
|
||||
def set_war_participant(self, new_participant: str) -> None:
|
||||
self.war_participant_id = new_participant
|
||||
def set_war_participant(self, new_war_part_id: str) -> None:
|
||||
self.war_participant_id = new_war_part_id
|
||||
|
||||
def set_leader(self, new_faction: str) -> None:
|
||||
self.leader = new_faction
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ class Choice:
|
|||
)
|
||||
self.comment: str | None = None
|
||||
|
||||
def set_id(self, new_id: str) -> None:
|
||||
self.participant_id = new_id
|
||||
def set_participant(self, new_camp_part_id: str) -> None:
|
||||
self.participant_id = new_camp_part_id
|
||||
|
||||
def set_priority(self, new_priority_id: str | None) -> None:
|
||||
self.priority_sector_id = new_priority_id
|
||||
|
|
|
|||
|
|
@ -2,44 +2,69 @@ from __future__ import annotations
|
|||
from typing import List
|
||||
|
||||
from warchron.constants import ContextType
|
||||
from warchron.model.tie_manager import ResolutionContext
|
||||
from warchron.model.exception import ForbiddenOperation
|
||||
from warchron.model.tie_manager import TieResolver
|
||||
from warchron.model.war_event import InfluenceGained
|
||||
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:
|
||||
|
||||
@staticmethod
|
||||
def close_round(round: Round) -> List[ResolutionContext]:
|
||||
if not round.all_battles_finished():
|
||||
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,
|
||||
)
|
||||
)
|
||||
if ties:
|
||||
return ties
|
||||
round.is_over = True
|
||||
return []
|
||||
# Round methods
|
||||
|
||||
@staticmethod
|
||||
def close_campaign(campaign: Campaign) -> List[ResolutionContext]:
|
||||
def check_round_closable(round: Round) -> 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"
|
||||
)
|
||||
|
||||
@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}",
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def finalize_round(round: Round) -> None:
|
||||
round.is_over = True
|
||||
|
||||
# Campaign methods
|
||||
|
||||
@staticmethod
|
||||
def close_campaign(campaign: Campaign) -> List[str]:
|
||||
if not campaign.all_rounds_finished():
|
||||
raise RuntimeError("All rounds must be finished to close their campaign")
|
||||
ties: List[ResolutionContext] = []
|
||||
ties: List[str] = []
|
||||
# for round in campaign.rounds:
|
||||
# # compute score
|
||||
# # if participants have same score
|
||||
|
|
@ -59,11 +84,13 @@ class ClosureService:
|
|||
campaign.is_over = True
|
||||
return []
|
||||
|
||||
# War methods
|
||||
|
||||
@staticmethod
|
||||
def close_war(war: War) -> List[ResolutionContext]:
|
||||
def close_war(war: War) -> List[str]:
|
||||
if not war.all_campaigns_finished():
|
||||
raise RuntimeError("All campaigns must be finished to close their war")
|
||||
ties: List[ResolutionContext] = []
|
||||
ties: List[str] = []
|
||||
# for campaign in war.campaigns:
|
||||
# # compute score
|
||||
# # if participants have same score
|
||||
|
|
|
|||
|
|
@ -1,19 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
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}",
|
||||
)
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
from typing import TYPE_CHECKING
|
||||
from typing import Dict, 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:
|
||||
|
|
|
|||
|
|
@ -1,46 +1,108 @@
|
|||
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.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
|
||||
from warchron.model.round import Round
|
||||
from warchron.model.battle import Battle
|
||||
from warchron.model.war_event import InfluenceSpent, TieResolved
|
||||
|
||||
|
||||
class TieResolver:
|
||||
|
||||
@staticmethod
|
||||
def resolve(
|
||||
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(
|
||||
war: War,
|
||||
context: ResolutionContext,
|
||||
bids: dict[str, int],
|
||||
) -> str | None:
|
||||
# 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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
# 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
|
||||
)
|
||||
|
||||
@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,
|
||||
) -> 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
|
||||
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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
|
||||
|
|
@ -25,6 +26,7 @@ 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:
|
||||
|
|
@ -53,6 +55,9 @@ 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:
|
||||
|
|
@ -69,6 +74,7 @@ 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,
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +93,8 @@ 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
|
||||
|
||||
|
|
@ -439,3 +447,18 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,30 +1,143 @@
|
|||
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:
|
||||
def __init__(self, participant_id: str):
|
||||
TYPE = "WarEvent"
|
||||
|
||||
def __init__(self, participant_id: str | None = None):
|
||||
self.id: str = str(uuid4())
|
||||
self.participant_id: str = participant_id
|
||||
self.participant_id: str | None = 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):
|
||||
def __init__(self, participant_id: str, context_type: str, context_id: str):
|
||||
TYPE = "TieResolved"
|
||||
|
||||
def __init__(self, participant_id: str | None, 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):
|
||||
def __init__(self, participant_id: str, amount: int, context: str):
|
||||
TYPE = "InfluenceSpent"
|
||||
|
||||
def __init__(self, participant_id: str, amount: int, context_type: str):
|
||||
super().__init__(participant_id)
|
||||
self.amount = amount
|
||||
self.context = context # "battle_tie", "campaign_tie", etc.
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,6 @@
|
|||
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:
|
||||
|
|
@ -14,13 +8,12 @@ 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: str) -> None:
|
||||
self.player_id = new_player
|
||||
def set_player(self, new_player_id: str) -> None:
|
||||
self.player_id = new_player_id
|
||||
|
||||
def set_faction(self, new_faction: str) -> None:
|
||||
self.faction = new_faction
|
||||
|
|
@ -40,16 +33,3 @@ 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)
|
||||
|
|
|
|||
40
src/warchron/view/tie_dialog.py
Normal file
40
src/warchron/view/tie_dialog.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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(),
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ class Ui_battleDialog(object):
|
|||
|
||||
def retranslateUi(self, battleDialog):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
battleDialog.setWindowTitle(_translate("battleDialog", "Battle result"))
|
||||
battleDialog.setWindowTitle(_translate("battleDialog", "Battle"))
|
||||
self.label_7.setText(_translate("battleDialog", "Sector"))
|
||||
self.label_5.setText(_translate("battleDialog", "Player 1"))
|
||||
self.label_6.setText(_translate("battleDialog", "Player 2"))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Battle result</string>
|
||||
<string>Battle</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class Ui_choiceDialog(object):
|
|||
|
||||
def retranslateUi(self, choiceDialog):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
choiceDialog.setWindowTitle(_translate("choiceDialog", "Choices"))
|
||||
choiceDialog.setWindowTitle(_translate("choiceDialog", "Choice"))
|
||||
self.label.setText(_translate("choiceDialog", "Player"))
|
||||
self.label_2.setText(_translate("choiceDialog", "Priority"))
|
||||
self.label_3.setText(_translate("choiceDialog", "Secondary"))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Choices</string>
|
||||
<string>Choice</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
|
|
|
|||
118
src/warchron/view/ui/ui_tie_dialog.py
Normal file
118
src/warchron/view/ui/ui_tie_dialog.py
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# 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())
|
||||
196
src/warchron/view/ui/ui_tie_dialog.ui
Normal file
196
src/warchron/view/ui/ui_tie_dialog.ui
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>tieDialog</class>
|
||||
<widget class="QDialog" name="tieDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>477</width>
|
||||
<height>174</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Tie</string>
|
||||
</property>
|
||||
<property name="windowIcon">
|
||||
<iconset>
|
||||
<normaloff>../resources/warchron_logo.png</normaloff>../resources/warchron_logo.png</iconset>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="tieContext">
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>10</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Battle tie</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_1">
|
||||
<property name="title">
|
||||
<string>Player 1</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Spend token</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Remaining token(s)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="tokenSpend_1">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="tokenCount_1">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Player 2</string>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string>Spend token</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Remaining token(s)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="tokenSpend_2">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="tokenCount_2">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>tieDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>tieDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
from __future__ import annotations
|
||||
from typing import Callable, List
|
||||
from typing import Callable, List, Dict
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue