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 StrEnum
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from PyQt6.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
|
|
@ -40,7 +41,7 @@ class IconName(str, Enum):
|
||||||
|
|
||||||
|
|
||||||
class Icons:
|
class Icons:
|
||||||
_cache: dict[str, QIcon] = {}
|
_cache: Dict[str, QIcon] = {}
|
||||||
|
|
||||||
_paths = {
|
_paths = {
|
||||||
IconName.UNDO: "arrow-curve-180-left",
|
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 PyQt6.QtGui import QIcon
|
||||||
|
|
||||||
|
from warchron.constants import ContextType
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class ParticipantOption:
|
class ParticipantOption:
|
||||||
|
|
@ -103,3 +105,10 @@ class BattleDTO:
|
||||||
state_icon: QIcon | None
|
state_icon: QIcon | None
|
||||||
player1_icon: QIcon | None
|
player1_icon: QIcon | None
|
||||||
player2_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.constants import ItemType, RefreshScope, Icons, IconName
|
||||||
|
from warchron.model.exception import ForbiddenOperation, DomainError
|
||||||
from warchron.model.round import Round
|
from warchron.model.round import Round
|
||||||
|
from warchron.model.war import War
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from warchron.controller.app_controller import AppController
|
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.choice_dialog import ChoiceDialog
|
||||||
from warchron.view.battle_dialog import BattleDialog
|
from warchron.view.battle_dialog import BattleDialog
|
||||||
|
from warchron.view.tie_dialog import TieDialog
|
||||||
|
|
||||||
|
|
||||||
class RoundController:
|
class RoundController:
|
||||||
|
|
@ -123,26 +134,47 @@ class RoundController:
|
||||||
if not round_id:
|
if not round_id:
|
||||||
return
|
return
|
||||||
rnd = self.app.model.get_round(round_id)
|
rnd = self.app.model.get_round(round_id)
|
||||||
if rnd.is_over:
|
camp = self.app.model.get_campaign_by_round(round_id)
|
||||||
return
|
war = self.app.model.get_war_by_round(round_id)
|
||||||
|
workflow = RoundClosureWorkflow(self.app)
|
||||||
try:
|
try:
|
||||||
ties = ClosureService.close_round(rnd)
|
workflow.start(war, camp, rnd)
|
||||||
except RuntimeError as e:
|
except DomainError as e:
|
||||||
QMessageBox.warning(self.app.view, "Cannot close round", str(e))
|
QMessageBox.warning(
|
||||||
return
|
|
||||||
if ties:
|
|
||||||
QMessageBox.information(
|
|
||||||
self.app.view,
|
self.app.view,
|
||||||
"Tie detected",
|
"Deletion forbidden",
|
||||||
"Round has unresolved ties. Resolution system not implemented yet.",
|
str(e),
|
||||||
)
|
)
|
||||||
return
|
|
||||||
self.app.is_dirty = True
|
self.app.is_dirty = True
|
||||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||||
self.app.navigation.refresh_and_select(
|
self.app.navigation.refresh_and_select(
|
||||||
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
|
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=round_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
# Choice methods
|
||||||
|
|
||||||
def edit_round_choice(self, choice_id: str) -> None:
|
def edit_round_choice(self, choice_id: str) -> None:
|
||||||
|
|
|
||||||
|
|
@ -19,17 +19,17 @@ class Battle:
|
||||||
self.victory_condition: str | None = None
|
self.victory_condition: str | None = None
|
||||||
self.comment: str | None = None
|
self.comment: str | None = None
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_sector(self, new_sector_id: str) -> None:
|
||||||
self.sector_id = new_id
|
self.sector_id = new_sector_id
|
||||||
|
|
||||||
def set_player_1(self, new_player_id: str | None) -> None:
|
def set_player_1(self, new_camp_part_id: str | None) -> None:
|
||||||
self.player_1_id = new_player_id
|
self.player_1_id = new_camp_part_id
|
||||||
|
|
||||||
def set_player_2(self, new_player_id: str | None) -> None:
|
def set_player_2(self, new_camp_part_id: str | None) -> None:
|
||||||
self.player_2_id = new_player_id
|
self.player_2_id = new_camp_part_id
|
||||||
|
|
||||||
def set_winner(self, new_player_id: str | None) -> None:
|
def set_winner(self, new_camp_part_id: str | None) -> None:
|
||||||
self.winner_id = new_player_id
|
self.winner_id = new_camp_part_id
|
||||||
|
|
||||||
def set_score(self, new_score: str | None) -> None:
|
def set_score(self, new_score: str | None) -> None:
|
||||||
self.score = new_score
|
self.score = new_score
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ class CampaignParticipant:
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_id(self, new_id: str) -> None:
|
||||||
self.id = new_id
|
self.id = new_id
|
||||||
|
|
||||||
def set_war_participant(self, new_participant: str) -> None:
|
def set_war_participant(self, new_war_part_id: str) -> None:
|
||||||
self.war_participant_id = new_participant
|
self.war_participant_id = new_war_part_id
|
||||||
|
|
||||||
def set_leader(self, new_faction: str) -> None:
|
def set_leader(self, new_faction: str) -> None:
|
||||||
self.leader = new_faction
|
self.leader = new_faction
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ class Choice:
|
||||||
)
|
)
|
||||||
self.comment: str | None = None
|
self.comment: str | None = None
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_participant(self, new_camp_part_id: str) -> None:
|
||||||
self.participant_id = new_id
|
self.participant_id = new_camp_part_id
|
||||||
|
|
||||||
def set_priority(self, new_priority_id: str | None) -> None:
|
def set_priority(self, new_priority_id: str | None) -> None:
|
||||||
self.priority_sector_id = new_priority_id
|
self.priority_sector_id = new_priority_id
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,69 @@ from __future__ import annotations
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from warchron.constants import ContextType
|
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.war import War
|
||||||
from warchron.model.campaign import Campaign
|
from warchron.model.campaign import Campaign
|
||||||
from warchron.model.round import Round
|
from warchron.model.round import Round
|
||||||
|
from warchron.model.battle import Battle
|
||||||
|
|
||||||
|
|
||||||
class ClosureService:
|
class ClosureService:
|
||||||
|
|
||||||
@staticmethod
|
# Round methods
|
||||||
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 []
|
|
||||||
|
|
||||||
@staticmethod
|
@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():
|
if not campaign.all_rounds_finished():
|
||||||
raise RuntimeError("All rounds must be finished to close their campaign")
|
raise RuntimeError("All rounds must be finished to close their campaign")
|
||||||
ties: List[ResolutionContext] = []
|
ties: List[str] = []
|
||||||
# for round in campaign.rounds:
|
# for round in campaign.rounds:
|
||||||
# # compute score
|
# # compute score
|
||||||
# # if participants have same score
|
# # if participants have same score
|
||||||
|
|
@ -59,11 +84,13 @@ class ClosureService:
|
||||||
campaign.is_over = True
|
campaign.is_over = True
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# War methods
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def close_war(war: War) -> List[ResolutionContext]:
|
def close_war(war: War) -> List[str]:
|
||||||
if not war.all_campaigns_finished():
|
if not war.all_campaigns_finished():
|
||||||
raise RuntimeError("All campaigns must be finished to close their war")
|
raise RuntimeError("All campaigns must be finished to close their war")
|
||||||
ties: List[ResolutionContext] = []
|
ties: List[str] = []
|
||||||
# for campaign in war.campaigns:
|
# for campaign in war.campaigns:
|
||||||
# # compute score
|
# # compute score
|
||||||
# # if participants have same 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:
|
if TYPE_CHECKING:
|
||||||
from warchron.model.war import War
|
from warchron.model.war import War
|
||||||
|
|
@ -23,8 +23,8 @@ class ScoreService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compute_narrative_points_for_participant(
|
def compute_narrative_points_for_participant(
|
||||||
war: "War", participant_id: str
|
war: "War", participant_id: str
|
||||||
) -> dict[str, int]:
|
) -> Dict[str, int]:
|
||||||
totals: dict[str, int] = {}
|
totals: Dict[str, int] = {}
|
||||||
for obj_id in war.objectives:
|
for obj_id in war.objectives:
|
||||||
totals[obj_id] = 0
|
totals[obj_id] = 0
|
||||||
for campaign in war.campaigns:
|
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 import War
|
||||||
from warchron.model.war_event import InfluenceSpent
|
from warchron.model.round import Round
|
||||||
|
from warchron.model.battle import Battle
|
||||||
|
from warchron.model.war_event import InfluenceSpent, TieResolved
|
||||||
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:
|
class TieResolver:
|
||||||
|
|
||||||
@staticmethod
|
@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,
|
war: War,
|
||||||
context: ResolutionContext,
|
context_type: ContextType,
|
||||||
bids: dict[str, int],
|
context_id: str,
|
||||||
) -> str | None:
|
bids: Dict[str, bool], # war_participant_id -> spend?
|
||||||
# verify available token for each player
|
) -> None:
|
||||||
for pid, amount in bids.items():
|
for war_part_id, spend in bids.items():
|
||||||
participant = war.participants[pid]
|
if not spend:
|
||||||
if participant.influence_tokens() < amount:
|
continue
|
||||||
raise ValueError("Not enough influence tokens")
|
if war.get_influence_tokens(war_part_id) < 1:
|
||||||
# apply spending
|
raise ForbiddenOperation("Not enough tokens")
|
||||||
for pid, amount in bids.items():
|
war.events.append(
|
||||||
if amount > 0:
|
InfluenceSpent(
|
||||||
war.participants[pid].events.append(
|
participant_id=war_part_id,
|
||||||
InfluenceSpent(
|
amount=1,
|
||||||
participant_id=pid,
|
context_type=context_type,
|
||||||
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]
|
@staticmethod
|
||||||
if len(winners) == 1:
|
def try_tie_break(
|
||||||
context.is_resolved = True
|
war: War,
|
||||||
return winners[0]
|
context_type: ContextType,
|
||||||
# persisting tie → None
|
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
|
return None
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ from uuid import uuid4
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
from warchron.model.war_event import WarEvent, InfluenceGained, InfluenceSpent
|
||||||
from warchron.model.exception import ForbiddenOperation
|
from warchron.model.exception import ForbiddenOperation
|
||||||
from warchron.model.war_participant import WarParticipant
|
from warchron.model.war_participant import WarParticipant
|
||||||
from warchron.model.objective import Objective
|
from warchron.model.objective import Objective
|
||||||
|
|
@ -25,6 +26,7 @@ class War:
|
||||||
self.participants: Dict[str, WarParticipant] = {}
|
self.participants: Dict[str, WarParticipant] = {}
|
||||||
self.objectives: Dict[str, Objective] = {}
|
self.objectives: Dict[str, Objective] = {}
|
||||||
self.campaigns: List[Campaign] = []
|
self.campaigns: List[Campaign] = []
|
||||||
|
self.events: List[WarEvent] = []
|
||||||
self.is_over: bool = False
|
self.is_over: bool = False
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_id(self, new_id: str) -> None:
|
||||||
|
|
@ -53,6 +55,9 @@ class War:
|
||||||
def set_influence_token(self, new_state: bool) -> None:
|
def set_influence_token(self, new_state: bool) -> None:
|
||||||
if self.is_over:
|
if self.is_over:
|
||||||
raise ForbiddenOperation("Can't set influence token of a closed war.")
|
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
|
self.influence_token = new_state
|
||||||
|
|
||||||
def set_state(self, new_state: bool) -> None:
|
def set_state(self, new_state: bool) -> None:
|
||||||
|
|
@ -69,6 +74,7 @@ class War:
|
||||||
"participants": [part.toDict() for part in self.participants.values()],
|
"participants": [part.toDict() for part in self.participants.values()],
|
||||||
"objectives": [obj.toDict() for obj in self.objectives.values()],
|
"objectives": [obj.toDict() for obj in self.objectives.values()],
|
||||||
"campaigns": [camp.toDict() for camp in self.campaigns],
|
"campaigns": [camp.toDict() for camp in self.campaigns],
|
||||||
|
"events": [ev.toDict() for ev in self.events],
|
||||||
"is_over": self.is_over,
|
"is_over": self.is_over,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,6 +93,8 @@ class War:
|
||||||
war.objectives[obj.id] = obj
|
war.objectives[obj.id] = obj
|
||||||
for camp_data in data.get("campaigns", []):
|
for camp_data in data.get("campaigns", []):
|
||||||
war.campaigns.append(Campaign.fromDict(camp_data))
|
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))
|
war.set_state(data.get("is_over", False))
|
||||||
return war
|
return war
|
||||||
|
|
||||||
|
|
@ -439,3 +447,18 @@ class War:
|
||||||
camp = self.get_campaign_by_round(round_id)
|
camp = self.get_campaign_by_round(round_id)
|
||||||
if camp is not None:
|
if camp is not None:
|
||||||
camp.remove_battle(round_id, sector_id)
|
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 datetime import datetime
|
||||||
from uuid import uuid4
|
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:
|
class WarEvent:
|
||||||
def __init__(self, participant_id: str):
|
TYPE = "WarEvent"
|
||||||
|
|
||||||
|
def __init__(self, participant_id: str | None = None):
|
||||||
self.id: str = str(uuid4())
|
self.id: str = str(uuid4())
|
||||||
self.participant_id: str = participant_id
|
self.participant_id: str | None = participant_id
|
||||||
self.timestamp: datetime = datetime.now()
|
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):
|
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)
|
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_type = context_type # battle, round, campaign, war
|
||||||
self.context_id = context_id
|
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):
|
class InfluenceGained(WarEvent):
|
||||||
|
TYPE = "InfluenceGained"
|
||||||
|
|
||||||
def __init__(self, participant_id: str, amount: int, source: str):
|
def __init__(self, participant_id: str, amount: int, source: str):
|
||||||
super().__init__(participant_id)
|
super().__init__(participant_id)
|
||||||
self.amount = amount
|
self.amount = amount
|
||||||
self.source = source # "battle", "tie_resolution", etc.
|
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):
|
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)
|
super().__init__(participant_id)
|
||||||
self.amount = amount
|
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 __future__ import annotations
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
from uuid import uuid4
|
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:
|
class WarParticipant:
|
||||||
|
|
@ -14,13 +8,12 @@ class WarParticipant:
|
||||||
self.id: str = str(uuid4())
|
self.id: str = str(uuid4())
|
||||||
self.player_id: str = player_id # ref to Model.players
|
self.player_id: str = player_id # ref to Model.players
|
||||||
self.faction: str = faction
|
self.faction: str = faction
|
||||||
self.events: list[WarEvent] = []
|
|
||||||
|
|
||||||
def set_id(self, new_id: str) -> None:
|
def set_id(self, new_id: str) -> None:
|
||||||
self.id = new_id
|
self.id = new_id
|
||||||
|
|
||||||
def set_player(self, new_player: str) -> None:
|
def set_player(self, new_player_id: str) -> None:
|
||||||
self.player_id = new_player
|
self.player_id = new_player_id
|
||||||
|
|
||||||
def set_faction(self, new_faction: str) -> None:
|
def set_faction(self, new_faction: str) -> None:
|
||||||
self.faction = new_faction
|
self.faction = new_faction
|
||||||
|
|
@ -40,16 +33,3 @@ class WarParticipant:
|
||||||
)
|
)
|
||||||
part.set_id(data["id"])
|
part.set_id(data["id"])
|
||||||
return part
|
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):
|
def retranslateUi(self, battleDialog):
|
||||||
_translate = QtCore.QCoreApplication.translate
|
_translate = QtCore.QCoreApplication.translate
|
||||||
battleDialog.setWindowTitle(_translate("battleDialog", "Battle result"))
|
battleDialog.setWindowTitle(_translate("battleDialog", "Battle"))
|
||||||
self.label_7.setText(_translate("battleDialog", "Sector"))
|
self.label_7.setText(_translate("battleDialog", "Sector"))
|
||||||
self.label_5.setText(_translate("battleDialog", "Player 1"))
|
self.label_5.setText(_translate("battleDialog", "Player 1"))
|
||||||
self.label_6.setText(_translate("battleDialog", "Player 2"))
|
self.label_6.setText(_translate("battleDialog", "Player 2"))
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Battle result</string>
|
<string>Battle</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset>
|
<iconset>
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ class Ui_choiceDialog(object):
|
||||||
|
|
||||||
def retranslateUi(self, choiceDialog):
|
def retranslateUi(self, choiceDialog):
|
||||||
_translate = QtCore.QCoreApplication.translate
|
_translate = QtCore.QCoreApplication.translate
|
||||||
choiceDialog.setWindowTitle(_translate("choiceDialog", "Choices"))
|
choiceDialog.setWindowTitle(_translate("choiceDialog", "Choice"))
|
||||||
self.label.setText(_translate("choiceDialog", "Player"))
|
self.label.setText(_translate("choiceDialog", "Player"))
|
||||||
self.label_2.setText(_translate("choiceDialog", "Priority"))
|
self.label_2.setText(_translate("choiceDialog", "Priority"))
|
||||||
self.label_3.setText(_translate("choiceDialog", "Secondary"))
|
self.label_3.setText(_translate("choiceDialog", "Secondary"))
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Choices</string>
|
<string>Choice</string>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowIcon">
|
<property name="windowIcon">
|
||||||
<iconset>
|
<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 __future__ import annotations
|
||||||
from typing import Callable, List
|
from typing import Callable, List, Dict
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import calendar
|
import calendar
|
||||||
|
|
||||||
|
|
@ -288,7 +288,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
|
||||||
if item is not None and walk(item):
|
if item is not None and walk(item):
|
||||||
return
|
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()
|
item = self.warsTree.currentItem()
|
||||||
if not item:
|
if not item:
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue