detect and resolve battle tie with influence_token

This commit is contained in:
Maxime Réaux 2026-02-17 16:37:36 +01:00
parent 115ddf8d50
commit 818d2886f4
23 changed files with 808 additions and 172 deletions

View file

@ -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",

View 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],
)

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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}",
)
)

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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)

View 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(),
}

View file

@ -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"))

View file

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

View file

@ -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"))

View file

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

View 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())

View 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>

View file

@ -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