wip close round/campaign/war + refacto json None
This commit is contained in:
parent
4c8086caf4
commit
6cbb7c6534
26 changed files with 474 additions and 108 deletions
|
|
@ -28,3 +28,10 @@ class RefreshScope(Enum):
|
|||
CAMPAIGN_DETAILS = auto()
|
||||
ROUND_DETAILS = auto()
|
||||
CURRENT_SELECTION_DETAILS = auto()
|
||||
|
||||
|
||||
class ContextType(StrEnum):
|
||||
WAR = "war"
|
||||
CAMPAIGN = "campaign"
|
||||
CHOICE = "choice"
|
||||
BATTLE = "battle"
|
||||
|
|
|
|||
|
|
@ -52,10 +52,13 @@ class AppController:
|
|||
self.view.influenceToken.toggled.connect(self.wars.set_influence_token)
|
||||
self.view.addObjectiveBtn.clicked.connect(self.wars.add_objective)
|
||||
self.view.addWarParticipantBtn.clicked.connect(self.wars.add_war_participant)
|
||||
self.view.endWarBtn.clicked.connect(self.wars.close_war)
|
||||
self.view.addSectorBtn.clicked.connect(self.campaigns.add_sector)
|
||||
self.view.addCampaignParticipantBtn.clicked.connect(
|
||||
self.campaigns.add_campaign_participant
|
||||
)
|
||||
self.view.endCampaignBtn.clicked.connect(self.campaigns.close_campaign)
|
||||
self.view.endRoundBtn.clicked.connect(self.rounds.close_round)
|
||||
self.view.on_edit_item = self.edit_item
|
||||
self.view.on_delete_item = self.delete_item
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ from warchron.controller.dtos import (
|
|||
SectorDTO,
|
||||
RoundDTO,
|
||||
)
|
||||
from warchron.model.closure_service import ClosureService
|
||||
from warchron.view.campaign_dialog import CampaignDialog
|
||||
from warchron.view.campaign_participant_dialog import CampaignParticipantDialog
|
||||
from warchron.view.sector_dialog import SectorDialog
|
||||
|
|
@ -50,6 +51,7 @@ class CampaignController:
|
|||
for p in camp_parts
|
||||
]
|
||||
self.app.view.display_campaign_participants(participants_for_display)
|
||||
self.app.view.endCampaignBtn.setEnabled(not camp.is_over)
|
||||
|
||||
def _validate_campaign_inputs(self, name: str, month: int) -> bool:
|
||||
if not name.strip():
|
||||
|
|
@ -99,6 +101,28 @@ class CampaignController:
|
|||
return
|
||||
self.app.model.update_campaign(campaign_id, name=name, month=month)
|
||||
|
||||
def close_campaign(self) -> None:
|
||||
campaign_id = self.app.navigation.selected_campaign_id
|
||||
if not campaign_id:
|
||||
return
|
||||
camp = self.app.model.get_campaign(campaign_id)
|
||||
if camp.is_over:
|
||||
return
|
||||
try:
|
||||
ties = ClosureService.close_campaign(camp)
|
||||
except RuntimeError as e:
|
||||
QMessageBox.warning(self.app.view, "Cannot close campaign", str(e))
|
||||
return
|
||||
if ties:
|
||||
QMessageBox.information(
|
||||
self.app.view,
|
||||
"Tie detected",
|
||||
"Campaign has unresolved ties.",
|
||||
)
|
||||
return
|
||||
self.app.is_dirty = True
|
||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||
|
||||
# Campaign participant methods
|
||||
|
||||
def add_campaign_participant(self) -> None:
|
||||
|
|
@ -148,7 +172,12 @@ class CampaignController:
|
|||
# Sector methods
|
||||
|
||||
def _validate_sector_inputs(
|
||||
self, name: str, round_id: str, major_id: str, minor_id: str, influence_id: str
|
||||
self,
|
||||
name: str,
|
||||
round_id: str | None,
|
||||
major_id: str | None,
|
||||
minor_id: str | None,
|
||||
influence_id: str | None,
|
||||
) -> bool:
|
||||
|
||||
if not name.strip():
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ class WarDTO:
|
|||
class ObjectiveDTO:
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
description: str | None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -63,9 +63,9 @@ class SectorDTO:
|
|||
id: str
|
||||
name: str
|
||||
round_index: int | None
|
||||
major: str
|
||||
minor: str
|
||||
influence: str
|
||||
major: str | None
|
||||
minor: str | None
|
||||
influence: str | None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -78,8 +78,8 @@ class RoundDTO:
|
|||
class ChoiceDTO:
|
||||
id: str
|
||||
participant_name: str
|
||||
priority_sector: str
|
||||
secondary_sector: str
|
||||
priority_sector: str | None
|
||||
secondary_sector: str | None
|
||||
comment: str | None
|
||||
|
||||
|
||||
|
|
@ -87,8 +87,8 @@ class ChoiceDTO:
|
|||
class BattleDTO:
|
||||
id: str
|
||||
sector_name: str
|
||||
player_1: str
|
||||
player_2: str
|
||||
player_1: str | None
|
||||
player_2: str | None
|
||||
winner: str | None
|
||||
score: str | None
|
||||
victory_condition: str | None
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
from typing import List, TYPE_CHECKING
|
||||
|
||||
from PyQt6.QtWidgets import QDialog
|
||||
from PyQt6.QtWidgets import QDialog, QMessageBox
|
||||
|
||||
from warchron.constants import ItemType, RefreshScope
|
||||
|
||||
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.view.choices_dialog import ChoicesDialog
|
||||
from warchron.view.battles_dialog import BattlesDialog
|
||||
|
||||
|
|
@ -92,6 +92,7 @@ class RoundController:
|
|||
)
|
||||
)
|
||||
self.app.view.display_round_battles(battles_for_display)
|
||||
self.app.view.endRoundBtn.setEnabled(not rnd.is_over)
|
||||
|
||||
def add_round(self) -> None:
|
||||
if not self.app.navigation.selected_campaign_id:
|
||||
|
|
@ -102,6 +103,28 @@ class RoundController:
|
|||
RefreshScope.WARS_TREE, item_type=ItemType.ROUND, item_id=rnd.id
|
||||
)
|
||||
|
||||
def close_round(self) -> None:
|
||||
round_id = self.app.navigation.selected_round_id
|
||||
if not round_id:
|
||||
return
|
||||
rnd = self.app.model.get_round(round_id)
|
||||
if rnd.is_over:
|
||||
return
|
||||
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(
|
||||
self.app.view,
|
||||
"Tie detected",
|
||||
"Round has unresolved ties. Resolution system not implemented yet.",
|
||||
)
|
||||
return
|
||||
self.app.is_dirty = True
|
||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||
|
||||
# Choice methods
|
||||
|
||||
def edit_round_choice(self, choice_id: str) -> None:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ from warchron.controller.dtos import (
|
|||
WarParticipantDTO,
|
||||
ObjectiveDTO,
|
||||
)
|
||||
from warchron.model.closure_service import ClosureService
|
||||
from warchron.view.war_dialog import WarDialog
|
||||
from warchron.view.objective_dialog import ObjectiveDialog
|
||||
from warchron.view.war_participant_dialog import WarParticipantDialog
|
||||
|
|
@ -44,6 +45,7 @@ class WarController:
|
|||
for p in war_parts
|
||||
]
|
||||
self.app.view.display_war_participants(participants_for_display)
|
||||
self.app.view.endWarBtn.setEnabled(not war.is_over)
|
||||
|
||||
def _validate_war_inputs(self, name: str, year: int) -> bool:
|
||||
if not name.strip():
|
||||
|
|
@ -86,6 +88,28 @@ class WarController:
|
|||
return
|
||||
self.app.model.update_war(war_id, name=name, year=year)
|
||||
|
||||
def close_war(self) -> None:
|
||||
war_id = self.app.navigation.selected_war_id
|
||||
if not war_id:
|
||||
return
|
||||
war = self.app.model.get_war(war_id)
|
||||
if war.is_over:
|
||||
return
|
||||
try:
|
||||
ties = ClosureService.close_war(war)
|
||||
except RuntimeError as e:
|
||||
QMessageBox.warning(self.app.view, "Cannot close war", str(e))
|
||||
return
|
||||
if ties:
|
||||
QMessageBox.information(
|
||||
self.app.view,
|
||||
"Tie detected",
|
||||
"War has unresolved ties.",
|
||||
)
|
||||
return
|
||||
self.app.is_dirty = True
|
||||
self.app.navigation.refresh(RefreshScope.CURRENT_SELECTION_DETAILS)
|
||||
|
||||
def set_major_value(self, value: int) -> None:
|
||||
war_id = self.app.navigation.selected_war_id
|
||||
if not war_id:
|
||||
|
|
@ -109,7 +133,7 @@ class WarController:
|
|||
|
||||
# Objective methods
|
||||
|
||||
def _validate_objective_inputs(self, name: str, description: str) -> bool:
|
||||
def _validate_objective_inputs(self, name: str, description: str | None) -> bool:
|
||||
if not name.strip():
|
||||
QMessageBox.warning(
|
||||
self.app.view, "Invalid name", "Objective name cannot be empty."
|
||||
|
|
|
|||
|
|
@ -40,26 +40,30 @@ class Battle:
|
|||
def set_comment(self, new_comment: str | None) -> None:
|
||||
self.comment = new_comment
|
||||
|
||||
# TODO improve draw detection
|
||||
def is_draw(self) -> bool:
|
||||
return self.winner_id is None and self.score is not None
|
||||
|
||||
def toDict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"sector_id": self.sector_id,
|
||||
"player_1_id": self.player_1_id,
|
||||
"player_2_id": self.player_2_id,
|
||||
"winner_id": self.winner_id,
|
||||
"score": self.score,
|
||||
"victory_condition": self.victory_condition,
|
||||
"comment": self.comment,
|
||||
"player_1_id": self.player_1_id or None,
|
||||
"player_2_id": self.player_2_id or None,
|
||||
"winner_id": self.winner_id or None,
|
||||
"score": self.score or None,
|
||||
"victory_condition": self.victory_condition or None,
|
||||
"comment": self.comment or None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fromDict(data: Dict[str, Any]) -> Battle:
|
||||
battle = Battle(
|
||||
data["sector_id"],
|
||||
data.get("player_1_id"),
|
||||
data.get("player_2_id"),
|
||||
data.get("player_1_id") or None,
|
||||
data.get("player_2_id") or None,
|
||||
)
|
||||
battle.winner_id = data.get("winner_id")
|
||||
battle.score = data.get("score")
|
||||
battle.victory_condition = data.get("victory_condition")
|
||||
battle.comment = data.get("comment")
|
||||
battle.winner_id = data.get("winner_id") or None
|
||||
battle.score = data.get("score") or None
|
||||
battle.victory_condition = data.get("victory_condition") or None
|
||||
battle.comment = data.get("comment") or None
|
||||
return battle
|
||||
|
|
|
|||
|
|
@ -146,7 +146,12 @@ class Campaign:
|
|||
)
|
||||
|
||||
def add_sector(
|
||||
self, name: str, round_id: str, major_id: str, minor_id: str, influence_id: str
|
||||
self,
|
||||
name: str,
|
||||
round_id: str | None,
|
||||
major_id: str | None,
|
||||
minor_id: str | None,
|
||||
influence_id: str | None,
|
||||
) -> Sector:
|
||||
sect = Sector(name, round_id, major_id, minor_id, influence_id)
|
||||
self.sectors[sect.id] = sect
|
||||
|
|
@ -168,10 +173,10 @@ class Campaign:
|
|||
sector_id: str,
|
||||
*,
|
||||
name: str,
|
||||
round_id: str,
|
||||
major_id: str,
|
||||
minor_id: str,
|
||||
influence_id: str,
|
||||
round_id: str | None,
|
||||
major_id: str | None,
|
||||
minor_id: str | None,
|
||||
influence_id: str | None,
|
||||
) -> None:
|
||||
sect = self.get_sector(sector_id)
|
||||
old_round_id = sect.round_id
|
||||
|
|
@ -308,6 +313,9 @@ class Campaign:
|
|||
|
||||
# Battle methods
|
||||
|
||||
def all_rounds_finished(self) -> bool:
|
||||
return all(r.is_over for r in self.rounds)
|
||||
|
||||
def create_battle(self, round_id: str, sector_id: str) -> Battle:
|
||||
rnd = self.get_round(round_id)
|
||||
return rnd.create_battle(sector_id)
|
||||
|
|
|
|||
|
|
@ -33,17 +33,17 @@ class Choice:
|
|||
def toDict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"participant_id": self.participant_id,
|
||||
"priority_sector_id": self.priority_sector_id,
|
||||
"secondary_sector_id": self.secondary_sector_id,
|
||||
"comment": self.comment,
|
||||
"priority_sector_id": self.priority_sector_id or None,
|
||||
"secondary_sector_id": self.secondary_sector_id or None,
|
||||
"comment": self.comment or None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fromDict(data: Dict[str, Any]) -> Choice:
|
||||
choice = Choice(
|
||||
data["participant_id"],
|
||||
data.get("priority_sector_id"),
|
||||
data.get("secondary_sector_id"),
|
||||
data.get("priority_sector_id") or None,
|
||||
data.get("secondary_sector_id") or None,
|
||||
)
|
||||
choice.comment = data.get("comment")
|
||||
choice.comment = data.get("comment") or None
|
||||
return choice
|
||||
|
|
|
|||
83
src/warchron/model/closure_service.py
Normal file
83
src/warchron/model/closure_service.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from __future__ import annotations
|
||||
from typing import List
|
||||
|
||||
from warchron.constants import ContextType
|
||||
from warchron.model.tie_manager import ResolutionContext
|
||||
from warchron.model.war import War
|
||||
from warchron.model.campaign import Campaign
|
||||
from warchron.model.round import Round
|
||||
|
||||
|
||||
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 []
|
||||
|
||||
@staticmethod
|
||||
def close_campaign(campaign: Campaign) -> List[ResolutionContext]:
|
||||
if not campaign.all_rounds_finished():
|
||||
raise RuntimeError("All rounds must be finished to close their campaign")
|
||||
ties: List[ResolutionContext] = []
|
||||
# for round in campaign.rounds:
|
||||
# # compute score
|
||||
# # if participants have same score
|
||||
# ties.append(
|
||||
# ResolutionContext(
|
||||
# context_type=ContextType.CAMPAIGN,
|
||||
# context_id=campaign.id,
|
||||
# participant_ids=[
|
||||
# # TODO ref to War.participants at some point
|
||||
# campaign.participants[campaign_participant_id],
|
||||
# campaign.participants[campaign_participant_id],
|
||||
# ],
|
||||
# )
|
||||
# )
|
||||
if ties:
|
||||
return ties
|
||||
campaign.is_over = True
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def close_war(war: War) -> List[ResolutionContext]:
|
||||
if not war.all_campaigns_finished():
|
||||
raise RuntimeError("All campaigns must be finished to close their war")
|
||||
ties: List[ResolutionContext] = []
|
||||
# for campaign in war.campaigns:
|
||||
# # compute score
|
||||
# # if participants have same score
|
||||
# ties.append(
|
||||
# ResolutionContext(
|
||||
# context_type=ContextType.WAR,
|
||||
# context_id=war.id,
|
||||
# participant_ids=[
|
||||
# war.participants[war_participant_id],
|
||||
# war.participants[war_participant_id],
|
||||
# ],
|
||||
# )
|
||||
# )
|
||||
if ties:
|
||||
return ties
|
||||
war.is_over = True
|
||||
return []
|
||||
23
src/warchron/model/influence_service.py
Normal file
23
src/warchron/model/influence_service.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from warchron.model.war import War
|
||||
from warchron.model.campaign import Campaign
|
||||
from warchron.model.battle import Battle
|
||||
from warchron.model.war_event import InfluenceGained
|
||||
|
||||
|
||||
class InfluenceService:
|
||||
|
||||
@staticmethod
|
||||
def apply_battle_result(war: War, campaign: Campaign, battle: Battle) -> None:
|
||||
if battle.winner_id is None:
|
||||
return
|
||||
sector = campaign.sectors[battle.sector_id]
|
||||
# if sector grants influence
|
||||
if sector.influence_objective_id and war.influence_token:
|
||||
participant = war.participants[battle.winner_id]
|
||||
participant.events.append(
|
||||
InfluenceGained(
|
||||
participant_id=participant.id,
|
||||
amount=1,
|
||||
source=f"battle:{battle.sector_id}",
|
||||
)
|
||||
)
|
||||
|
|
@ -182,7 +182,9 @@ class Model:
|
|||
|
||||
# Objective methods
|
||||
|
||||
def add_objective(self, war_id: str, name: str, description: str) -> Objective:
|
||||
def add_objective(
|
||||
self, war_id: str, name: str, description: str | None
|
||||
) -> Objective:
|
||||
war = self.get_war(war_id)
|
||||
return war.add_objective(name, description)
|
||||
|
||||
|
|
@ -195,7 +197,7 @@ class Model:
|
|||
raise KeyError("Objective not found")
|
||||
|
||||
def update_objective(
|
||||
self, objective_id: str, *, name: str, description: str
|
||||
self, objective_id: str, *, name: str, description: str | None
|
||||
) -> None:
|
||||
war = self.get_war_by_objective(objective_id)
|
||||
war.update_objective(objective_id, name=name, description=description)
|
||||
|
|
@ -229,6 +231,11 @@ class Model:
|
|||
def get_player_from_war_participant(self, war_part: WarParticipant) -> Player:
|
||||
return self.get_player(war_part.player_id)
|
||||
|
||||
def get_participant_name(self, participant_id: str) -> str:
|
||||
war = self.get_war_by_war_participant(participant_id)
|
||||
war_part = war.get_war_participant(participant_id)
|
||||
return self.players[war_part.player_id].name
|
||||
|
||||
def update_war_participant(self, participant_id: str, *, faction: str) -> None:
|
||||
war = self.get_war_by_war_participant(participant_id)
|
||||
war.update_war_participant(participant_id, faction=faction)
|
||||
|
|
@ -290,10 +297,10 @@ class Model:
|
|||
self,
|
||||
campaign_id: str,
|
||||
name: str,
|
||||
round_id: str,
|
||||
major_id: str,
|
||||
minor_id: str,
|
||||
influence_id: str,
|
||||
round_id: str | None,
|
||||
major_id: str | None,
|
||||
minor_id: str | None,
|
||||
influence_id: str | None,
|
||||
) -> Sector:
|
||||
camp = self.get_campaign(campaign_id)
|
||||
return camp.add_sector(name, round_id, major_id, minor_id, influence_id)
|
||||
|
|
@ -312,10 +319,10 @@ class Model:
|
|||
sector_id: str,
|
||||
*,
|
||||
name: str,
|
||||
round_id: str,
|
||||
major_id: str,
|
||||
minor_id: str,
|
||||
influence_id: str,
|
||||
round_id: str | None,
|
||||
major_id: str | None,
|
||||
minor_id: str | None,
|
||||
influence_id: str | None,
|
||||
) -> None:
|
||||
war = self.get_war_by_sector(sector_id)
|
||||
war.update_sector(
|
||||
|
|
@ -343,11 +350,6 @@ class Model:
|
|||
camp = self.get_campaign(camp_id)
|
||||
return camp.add_campaign_participant(player_id, leader, theme)
|
||||
|
||||
def get_participant_name(self, participant_id: str) -> str:
|
||||
war = self.get_war_by_war_participant(participant_id)
|
||||
war_part = war.get_war_participant(participant_id)
|
||||
return self.players[war_part.player_id].name
|
||||
|
||||
# TODO replace multiloops by internal has_* method
|
||||
def get_campaign_participant(self, participant_id: str) -> CampaignParticipant:
|
||||
for war in self.wars.values():
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ from uuid import uuid4
|
|||
|
||||
|
||||
class Objective:
|
||||
def __init__(self, name: str, description: str):
|
||||
def __init__(self, name: str, description: str | None):
|
||||
self.id: str = str(uuid4())
|
||||
self.name: str = name
|
||||
self.description: str = description
|
||||
self.description: str | None = description
|
||||
|
||||
def set_id(self, new_id: str) -> None:
|
||||
self.id = new_id
|
||||
|
|
@ -15,18 +15,18 @@ class Objective:
|
|||
def set_name(self, new_name: str) -> None:
|
||||
self.name = new_name
|
||||
|
||||
def set_description(self, new_description: str) -> None:
|
||||
def set_description(self, new_description: str | None) -> None:
|
||||
self.description = new_description
|
||||
|
||||
def toDict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"description": self.description or None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def fromDict(data: Dict[str, Any]) -> Objective:
|
||||
obj = Objective(data["name"], data["description"])
|
||||
obj = Objective(data["name"], data["description"] or None)
|
||||
obj.set_id(data["id"])
|
||||
return obj
|
||||
|
|
|
|||
|
|
@ -104,6 +104,11 @@ class Round:
|
|||
for bat in self.battles.values()
|
||||
)
|
||||
|
||||
def all_battles_finished(self) -> bool:
|
||||
return all(
|
||||
b.winner_id is not None or b.is_draw() for b in self.battles.values()
|
||||
)
|
||||
|
||||
def create_battle(self, sector_id: str) -> Battle:
|
||||
if sector_id not in self.battles:
|
||||
battle = Battle(sector_id=sector_id, player_1_id=None, player_2_id=None)
|
||||
|
|
|
|||
39
src/warchron/model/score_service.py
Normal file
39
src/warchron/model/score_service.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from warchron.model.war import War
|
||||
|
||||
|
||||
class ScoreService:
|
||||
|
||||
@staticmethod
|
||||
def compute_victory_points_for_participant(war: "War", participant_id: str) -> int:
|
||||
total = 0
|
||||
for campaign in war.campaigns:
|
||||
for round_ in campaign.rounds:
|
||||
for battle in round_.battles.values():
|
||||
if battle.winner_id == participant_id:
|
||||
sector = campaign.sectors[battle.sector_id]
|
||||
if sector.major_objective_id:
|
||||
total += war.major_value
|
||||
if sector.minor_objective_id:
|
||||
total += war.minor_value
|
||||
return total
|
||||
|
||||
@staticmethod
|
||||
def compute_narrative_points_for_participant(
|
||||
war: "War", participant_id: str
|
||||
) -> dict[str, int]:
|
||||
totals: dict[str, int] = {}
|
||||
for obj_id in war.objectives:
|
||||
totals[obj_id] = 0
|
||||
for campaign in war.campaigns:
|
||||
for round_ in campaign.rounds:
|
||||
for battle in round_.battles.values():
|
||||
if battle.winner_id == participant_id:
|
||||
sector = campaign.sectors[battle.sector_id]
|
||||
if sector.major_objective_id:
|
||||
totals[sector.major_objective_id] += war.major_value
|
||||
if sector.minor_objective_id:
|
||||
totals[sector.minor_objective_id] += war.minor_value
|
||||
return totals
|
||||
|
|
@ -14,7 +14,7 @@ class Sector:
|
|||
):
|
||||
self.id: str = str(uuid4())
|
||||
self.name: str = name
|
||||
self.round_id: str | None = round_id
|
||||
self.round_id: str | None = round_id # ref to Campaign.rounds
|
||||
self.major_objective_id: str | None = major_id # ref to War.objectives
|
||||
self.minor_objective_id: str | None = minor_id # ref to War.objectives
|
||||
self.influence_objective_id: str | None = influence_id # ref to War.objectives
|
||||
|
|
@ -27,16 +27,16 @@ class Sector:
|
|||
def set_name(self, new_name: str) -> None:
|
||||
self.name = new_name
|
||||
|
||||
def set_round(self, new_round_id: str) -> None:
|
||||
def set_round(self, new_round_id: str | None) -> None:
|
||||
self.round_id = new_round_id
|
||||
|
||||
def set_major(self, new_major_id: str) -> None:
|
||||
def set_major(self, new_major_id: str | None) -> None:
|
||||
self.major_objective_id = new_major_id
|
||||
|
||||
def set_minor(self, new_minor_id: str) -> None:
|
||||
def set_minor(self, new_minor_id: str | None) -> None:
|
||||
self.minor_objective_id = new_minor_id
|
||||
|
||||
def set_influence(self, new_influence_id: str) -> None:
|
||||
def set_influence(self, new_influence_id: str | None) -> None:
|
||||
self.influence_objective_id = new_influence_id
|
||||
|
||||
def toDict(self) -> Dict[str, Any]:
|
||||
|
|
@ -44,11 +44,11 @@ class Sector:
|
|||
"id": self.id,
|
||||
"name": self.name,
|
||||
"round_id": self.round_id,
|
||||
"major_objective_id": self.major_objective_id,
|
||||
"minor_objective_id": self.minor_objective_id,
|
||||
"influence_objective_id": self.influence_objective_id,
|
||||
"mission": self.mission,
|
||||
"description": self.description,
|
||||
"major_objective_id": self.major_objective_id or None,
|
||||
"minor_objective_id": self.minor_objective_id or None,
|
||||
"influence_objective_id": self.influence_objective_id or None,
|
||||
"mission": self.mission or None,
|
||||
"description": self.description or None,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -56,11 +56,11 @@ class Sector:
|
|||
sec = Sector(
|
||||
data["name"],
|
||||
data["round_id"],
|
||||
data.get("major_objective_id"),
|
||||
data.get("minor_objective_id"),
|
||||
data.get("influence_objective_id"),
|
||||
data.get("major_objective_id") or None,
|
||||
data.get("minor_objective_id") or None,
|
||||
data.get("influence_objective_id") or None,
|
||||
)
|
||||
sec.set_id(data["id"])
|
||||
sec.mission = data.get("mission")
|
||||
sec.description = data.get("description")
|
||||
sec.mission = data.get("mission") or None
|
||||
sec.description = data.get("description") or None
|
||||
return sec
|
||||
|
|
|
|||
46
src/warchron/model/tie_manager.py
Normal file
46
src/warchron/model/tie_manager.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
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
|
||||
|
||||
|
||||
class TieResolver:
|
||||
|
||||
@staticmethod
|
||||
def resolve(
|
||||
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,
|
||||
)
|
||||
)
|
||||
# determine winner
|
||||
max_bid = max(bids.values())
|
||||
winners = [pid for pid, b in bids.items() if b == max_bid]
|
||||
if len(winners) == 1:
|
||||
context.is_resolved = True
|
||||
return winners[0]
|
||||
# persisting tie → None
|
||||
return None
|
||||
|
|
@ -86,7 +86,7 @@ class War:
|
|||
|
||||
# Objective methods
|
||||
|
||||
def add_objective(self, name: str, description: str) -> Objective:
|
||||
def add_objective(self, name: str, description: str | None) -> Objective:
|
||||
obj = Objective(name, description)
|
||||
self.objectives[obj.id] = obj
|
||||
return obj
|
||||
|
|
@ -104,7 +104,7 @@ class War:
|
|||
return obj.name if obj else ""
|
||||
|
||||
def update_objective(
|
||||
self, objective_id: str, *, name: str, description: str
|
||||
self, objective_id: str, *, name: str, description: str | None
|
||||
) -> None:
|
||||
obj = self.get_objective(objective_id)
|
||||
obj.set_name(name)
|
||||
|
|
@ -173,6 +173,9 @@ class War:
|
|||
def get_default_campaign_values(self) -> Dict[str, Any]:
|
||||
return {"month": datetime.now().month}
|
||||
|
||||
def all_campaigns_finished(self) -> bool:
|
||||
return all(c.is_over for c in self.campaigns)
|
||||
|
||||
def add_campaign(self, name: str, month: int | None = None) -> Campaign:
|
||||
if month is None:
|
||||
month = self.get_default_campaign_values()["month"]
|
||||
|
|
@ -247,10 +250,10 @@ class War:
|
|||
sector_id: str,
|
||||
*,
|
||||
name: str,
|
||||
round_id: str,
|
||||
major_id: str,
|
||||
minor_id: str,
|
||||
influence_id: str,
|
||||
round_id: str | None,
|
||||
major_id: str | None,
|
||||
minor_id: str | None,
|
||||
influence_id: str | None,
|
||||
) -> None:
|
||||
camp = self.get_campaign_by_sector(sector_id)
|
||||
camp.update_sector(
|
||||
|
|
|
|||
30
src/warchron/model/war_event.py
Normal file
30
src/warchron/model/war_event.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
from datetime import datetime
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
class WarEvent:
|
||||
def __init__(self, participant_id: str):
|
||||
self.id: str = str(uuid4())
|
||||
self.participant_id: str = participant_id
|
||||
self.timestamp: datetime = datetime.now()
|
||||
|
||||
|
||||
class TieResolved(WarEvent):
|
||||
def __init__(self, participant_id: str, context_type: str, context_id: str):
|
||||
super().__init__(participant_id)
|
||||
self.context_type = context_type # battle, round, campaign, war
|
||||
self.context_id = context_id
|
||||
|
||||
|
||||
class InfluenceGained(WarEvent):
|
||||
def __init__(self, participant_id: str, amount: int, source: str):
|
||||
super().__init__(participant_id)
|
||||
self.amount = amount
|
||||
self.source = source # "battle", "tie_resolution", etc.
|
||||
|
||||
|
||||
class InfluenceSpent(WarEvent):
|
||||
def __init__(self, participant_id: str, amount: int, context: str):
|
||||
super().__init__(participant_id)
|
||||
self.amount = amount
|
||||
self.context = context # "battle_tie", "campaign_tie", etc.
|
||||
|
|
@ -1,6 +1,12 @@
|
|||
from __future__ import annotations
|
||||
from typing import Any, Dict
|
||||
from uuid import uuid4
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from warchron.model.war import War
|
||||
from warchron.model.war_event import WarEvent, InfluenceSpent, InfluenceGained
|
||||
from warchron.model.score_service import ScoreService
|
||||
|
||||
|
||||
class WarParticipant:
|
||||
|
|
@ -8,6 +14,7 @@ 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
|
||||
|
|
@ -33,3 +40,16 @@ class WarParticipant:
|
|||
)
|
||||
part.set_id(data["id"])
|
||||
return part
|
||||
|
||||
# Computed properties
|
||||
|
||||
def influence_tokens(self) -> int:
|
||||
gained = sum(e.amount for e in self.events if isinstance(e, InfluenceGained))
|
||||
spent = sum(e.amount for e in self.events if isinstance(e, InfluenceSpent))
|
||||
return gained - spent
|
||||
|
||||
def victory_points(self, war: "War") -> int:
|
||||
return ScoreService.compute_victory_points_for_participant(war, self.id)
|
||||
|
||||
def narrative_points(self, war: "War") -> Dict[str, int]:
|
||||
return ScoreService.compute_narrative_points_for_participant(war, self.id)
|
||||
|
|
|
|||
|
|
@ -49,20 +49,26 @@ class BattlesDialog(QDialog):
|
|||
def get_sector_id(self) -> str:
|
||||
return cast(str, self.ui.sectorComboBox.currentData())
|
||||
|
||||
def get_player_1_id(self) -> str:
|
||||
return cast(str, self.ui.player1ComboBox.currentData())
|
||||
def get_player_1_id(self) -> str | None:
|
||||
text = cast(str, self.ui.player1ComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_player_2_id(self) -> str:
|
||||
return cast(str, self.ui.player2ComboBox.currentData())
|
||||
def get_player_2_id(self) -> str | None:
|
||||
text = cast(str, self.ui.player2ComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_winner_id(self) -> str:
|
||||
return cast(str, self.ui.winnerComboBox.currentData())
|
||||
def get_winner_id(self) -> str | None:
|
||||
text = cast(str, self.ui.winnerComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_score(self) -> str:
|
||||
return self.ui.score.text().strip()
|
||||
def get_score(self) -> str | None:
|
||||
text = self.ui.score.text().strip()
|
||||
return text if text else None
|
||||
|
||||
def get_victory_condition(self) -> str:
|
||||
return self.ui.victoryCondition.text().strip()
|
||||
def get_victory_condition(self) -> str | None:
|
||||
text = self.ui.victoryCondition.text().strip()
|
||||
return text if text else None
|
||||
|
||||
def get_comment(self) -> str:
|
||||
return self.ui.battleComment.toPlainText().strip()
|
||||
def get_comment(self) -> str | None:
|
||||
text = self.ui.battleComment.toPlainText().strip()
|
||||
return text if text else None
|
||||
|
|
|
|||
|
|
@ -38,11 +38,14 @@ class ChoicesDialog(QDialog):
|
|||
def get_participant_id(self) -> str:
|
||||
return cast(str, self.ui.playerComboBox.currentData())
|
||||
|
||||
def get_priority_id(self) -> str:
|
||||
return cast(str, self.ui.priorityComboBox.currentData())
|
||||
def get_priority_id(self) -> str | None:
|
||||
text = cast(str, self.ui.priorityComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_secondary_id(self) -> str:
|
||||
return cast(str, self.ui.secondaryComboBox.currentData())
|
||||
def get_secondary_id(self) -> str | None:
|
||||
text = cast(str, self.ui.secondaryComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_comment(self) -> str:
|
||||
return self.ui.choiceComment.toPlainText().strip()
|
||||
def get_comment(self) -> str | None:
|
||||
text = self.ui.choiceComment.toPlainText().strip()
|
||||
return text if text else None
|
||||
|
|
|
|||
|
|
@ -20,5 +20,6 @@ class ObjectiveDialog(QDialog):
|
|||
def get_objective_name(self) -> str:
|
||||
return self.ui.objectiveName.text().strip()
|
||||
|
||||
def get_objective_description(self) -> str:
|
||||
return self.ui.objectiveDescription.toPlainText().strip()
|
||||
def get_objective_description(self) -> str | None:
|
||||
text = self.ui.objectiveDescription.toPlainText().strip()
|
||||
return text if text else None
|
||||
|
|
|
|||
|
|
@ -42,14 +42,21 @@ class SectorDialog(QDialog):
|
|||
def get_sector_name(self) -> str:
|
||||
return self.ui.sectorName.text().strip()
|
||||
|
||||
def get_round_id(self) -> str:
|
||||
return cast(str, self.ui.roundComboBox.currentData())
|
||||
def get_round_id(self) -> str | None:
|
||||
text = cast(str, self.ui.roundComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_major_id(self) -> str:
|
||||
return cast(str, self.ui.majorComboBox.currentData())
|
||||
def get_major_id(self) -> str | None:
|
||||
text = cast(str, self.ui.majorComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_minor_id(self) -> str:
|
||||
return cast(str, self.ui.minorComboBox.currentData())
|
||||
def get_minor_id(self) -> str | None:
|
||||
text = cast(str, self.ui.minorComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
def get_influence_id(self) -> str:
|
||||
return cast(str, self.ui.influenceComboBox.currentData())
|
||||
def get_influence_id(self) -> str | None:
|
||||
text = cast(str, self.ui.influenceComboBox.currentData())
|
||||
return text if text else None
|
||||
|
||||
|
||||
# TODO fix mission + description missing
|
||||
|
|
|
|||
|
|
@ -369,7 +369,7 @@ class Ui_MainWindow(object):
|
|||
spacerItem15 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
|
||||
self.horizontalLayout_13.addItem(spacerItem15)
|
||||
self.resolvePairingBtn = QtWidgets.QPushButton(parent=self.pageRound)
|
||||
self.resolvePairingBtn.setEnabled(True)
|
||||
self.resolvePairingBtn.setEnabled(False)
|
||||
self.resolvePairingBtn.setObjectName("resolvePairingBtn")
|
||||
self.horizontalLayout_13.addWidget(self.resolvePairingBtn)
|
||||
self.verticalLayout_8.addLayout(self.horizontalLayout_13)
|
||||
|
|
@ -486,7 +486,7 @@ class Ui_MainWindow(object):
|
|||
|
||||
self.retranslateUi(MainWindow)
|
||||
self.tabWidget.setCurrentIndex(1)
|
||||
self.selectedDetailsStack.setCurrentIndex(1)
|
||||
self.selectedDetailsStack.setCurrentIndex(3)
|
||||
QtCore.QMetaObject.connectSlotsByName(MainWindow)
|
||||
|
||||
def retranslateUi(self, MainWindow):
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@
|
|||
</widget>
|
||||
<widget class="QStackedWidget" name="selectedDetailsStack">
|
||||
<property name="currentIndex">
|
||||
<number>1</number>
|
||||
<number>3</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="pageEmpty">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
|
|
@ -873,7 +873,7 @@
|
|||
<item>
|
||||
<widget class="QPushButton" name="resolvePairingBtn">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Resolve pairing</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue