2026-01-28 16:25:40 +01:00
|
|
|
from __future__ import annotations
|
2026-02-02 14:33:31 +01:00
|
|
|
from uuid import uuid4
|
2026-02-04 16:10:53 +01:00
|
|
|
from typing import Any, Dict, List
|
2026-01-19 18:55:07 +01:00
|
|
|
|
2026-02-05 16:17:18 +01:00
|
|
|
from warchron.model.exception import (
|
|
|
|
|
DeletionRequiresConfirmation,
|
|
|
|
|
UpdateRequiresConfirmation,
|
|
|
|
|
)
|
2026-02-05 08:42:38 +01:00
|
|
|
from warchron.model.campaign_participant import CampaignParticipant
|
|
|
|
|
from warchron.model.sector import Sector
|
|
|
|
|
from warchron.model.round import Round
|
|
|
|
|
from warchron.model.choice import Choice
|
|
|
|
|
from warchron.model.battle import Battle
|
2026-01-19 18:55:07 +01:00
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-01-19 18:55:07 +01:00
|
|
|
class Campaign:
|
2026-02-04 16:10:53 +01:00
|
|
|
def __init__(self, name: str, month: int) -> None:
|
2026-01-28 16:25:40 +01:00
|
|
|
self.id: str = str(uuid4())
|
|
|
|
|
self.name: str = name
|
|
|
|
|
self.month: int = month
|
2026-02-04 16:10:53 +01:00
|
|
|
self.participants: Dict[str, CampaignParticipant] = {}
|
|
|
|
|
self.sectors: Dict[str, Sector] = {}
|
|
|
|
|
self.rounds: List[Round] = []
|
2026-01-19 18:55:07 +01:00
|
|
|
self.is_over = False
|
|
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def set_id(self, new_id: str) -> None:
|
2026-01-19 18:55:07 +01:00
|
|
|
self.id = new_id
|
|
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def set_name(self, new_name: str) -> None:
|
2026-01-19 18:55:07 +01:00
|
|
|
self.name = new_name
|
|
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def set_month(self, new_month: int) -> None:
|
2026-01-19 18:55:07 +01:00
|
|
|
self.month = new_month
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def set_state(self, new_state: bool) -> None:
|
2026-01-19 18:55:07 +01:00
|
|
|
self.is_over = new_state
|
|
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def toDict(self) -> Dict[str, Any]:
|
2026-01-19 18:55:07 +01:00
|
|
|
return {
|
2026-02-02 14:33:31 +01:00
|
|
|
"id": self.id,
|
|
|
|
|
"name": self.name,
|
|
|
|
|
"month": self.month,
|
2026-02-06 09:59:54 +01:00
|
|
|
"participants": [p.toDict() for p in self.participants.values()],
|
|
|
|
|
"sectors": [s.toDict() for s in self.sectors.values()],
|
2026-01-21 08:31:48 +01:00
|
|
|
"rounds": [rnd.toDict() for rnd in self.rounds],
|
2026-02-02 14:33:31 +01:00
|
|
|
"is_over": self.is_over,
|
2026-01-19 18:55:07 +01:00
|
|
|
}
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-01-19 18:55:07 +01:00
|
|
|
@staticmethod
|
2026-02-04 16:10:53 +01:00
|
|
|
def fromDict(data: Dict[str, Any]) -> Campaign:
|
2026-01-22 23:42:47 +01:00
|
|
|
camp = Campaign(name=data["name"], month=data["month"])
|
2026-01-21 08:31:48 +01:00
|
|
|
camp.set_id(data["id"])
|
2026-02-06 09:59:54 +01:00
|
|
|
for p in data.get("participants", []):
|
|
|
|
|
part = CampaignParticipant.fromDict(p)
|
|
|
|
|
camp.participants[part.id] = part
|
|
|
|
|
for s in data.get("sectors", []):
|
|
|
|
|
sec = Sector.fromDict(s)
|
|
|
|
|
camp.sectors[sec.id] = sec
|
2026-01-21 08:31:48 +01:00
|
|
|
for rnd_data in data.get("rounds", []):
|
|
|
|
|
camp.rounds.append(Round.fromDict(rnd_data))
|
|
|
|
|
camp.set_state(data.get("is_over", False))
|
|
|
|
|
return camp
|
2026-01-28 16:25:40 +01:00
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
# Campaign participant methods
|
2026-01-30 00:34:22 +01:00
|
|
|
|
|
|
|
|
def get_all_campaign_participants_ids(self) -> set[str]:
|
|
|
|
|
return set(self.participants.keys())
|
|
|
|
|
|
2026-02-03 08:25:25 +01:00
|
|
|
def has_participant(self, participant_id: str) -> bool:
|
|
|
|
|
return participant_id in self.participants
|
2026-01-30 00:34:22 +01:00
|
|
|
|
2026-02-03 09:01:17 +01:00
|
|
|
def has_war_participant(self, war_participant_id: str) -> bool:
|
|
|
|
|
return any(
|
|
|
|
|
part.war_participant_id == war_participant_id
|
|
|
|
|
for part in self.participants.values()
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
def add_campaign_participant(
|
|
|
|
|
self, war_participant_id: str, leader: str, theme: str
|
|
|
|
|
) -> CampaignParticipant:
|
2026-02-03 09:01:17 +01:00
|
|
|
if self.has_war_participant(war_participant_id):
|
2026-01-30 00:34:22 +01:00
|
|
|
raise ValueError("Player already registered in this campaign")
|
2026-02-02 14:33:31 +01:00
|
|
|
participant = CampaignParticipant(
|
|
|
|
|
war_participant_id=war_participant_id, leader=leader, theme=theme
|
|
|
|
|
)
|
2026-01-30 00:34:22 +01:00
|
|
|
self.participants[participant.id] = participant
|
|
|
|
|
return participant
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
def get_campaign_participant(self, participant_id: str) -> CampaignParticipant:
|
|
|
|
|
try:
|
|
|
|
|
return self.participants[participant_id]
|
|
|
|
|
except KeyError:
|
|
|
|
|
raise KeyError(f"Participant {participant_id} not in campaign {self.id}")
|
2026-01-30 00:34:22 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def get_all_campaign_participants(self) -> List[CampaignParticipant]:
|
2026-01-30 00:34:22 +01:00
|
|
|
return list(self.participants.values())
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
def update_campaign_participant(
|
|
|
|
|
self, participant_id: str, *, leader: str, theme: str
|
2026-02-04 16:10:53 +01:00
|
|
|
) -> None:
|
2026-02-02 14:33:31 +01:00
|
|
|
part = self.get_campaign_participant(participant_id)
|
|
|
|
|
# Can't change referred War.participant
|
2026-01-30 00:34:22 +01:00
|
|
|
part.set_leader(leader)
|
|
|
|
|
part.set_theme(theme)
|
|
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def remove_campaign_participant(self, participant_id: str) -> None:
|
2026-02-05 16:17:18 +01:00
|
|
|
rounds_blocking: list[Round] = []
|
|
|
|
|
for rnd in self.rounds:
|
|
|
|
|
if rnd.has_choice_with_participant(
|
|
|
|
|
participant_id
|
|
|
|
|
) or rnd.has_battle_with_participant(participant_id):
|
|
|
|
|
rounds_blocking.append(rnd)
|
|
|
|
|
if not rounds_blocking:
|
|
|
|
|
del self.participants[participant_id]
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def cleanup() -> None:
|
|
|
|
|
for rnd in rounds_blocking:
|
|
|
|
|
rnd.clear_participant_references(participant_id)
|
|
|
|
|
rnd.remove_choice(participant_id)
|
|
|
|
|
del self.participants[participant_id]
|
|
|
|
|
|
|
|
|
|
rounds_str = ", ".join(
|
|
|
|
|
str(self.get_round_index(rnd.id)) for rnd in rounds_blocking
|
|
|
|
|
)
|
|
|
|
|
raise DeletionRequiresConfirmation(
|
|
|
|
|
message=(
|
|
|
|
|
f"This participant is used in round(s): {rounds_str}.\n"
|
|
|
|
|
"Related choices and battles will be cleared.\n"
|
|
|
|
|
"Do you want to continue?"
|
|
|
|
|
),
|
|
|
|
|
cleanup_action=cleanup,
|
|
|
|
|
)
|
2026-01-30 00:34:22 +01:00
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
# Sector methods
|
2026-01-30 00:34:22 +01:00
|
|
|
|
2026-02-05 16:17:18 +01:00
|
|
|
def has_sector(self, sector_id: str) -> bool:
|
|
|
|
|
return sector_id in self.sectors
|
|
|
|
|
|
|
|
|
|
def has_sector_with_objective(self, objective_id: str) -> bool:
|
|
|
|
|
return any(
|
|
|
|
|
sect.major_objective_id == objective_id
|
|
|
|
|
or sect.minor_objective_id == objective_id
|
|
|
|
|
or sect.influence_objective_id == objective_id
|
|
|
|
|
for sect in self.sectors.values()
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
def add_sector(
|
2026-02-11 19:22:43 +01:00
|
|
|
self,
|
|
|
|
|
name: str,
|
|
|
|
|
round_id: str | None,
|
|
|
|
|
major_id: str | None,
|
|
|
|
|
minor_id: str | None,
|
|
|
|
|
influence_id: str | None,
|
2026-02-12 09:10:03 +01:00
|
|
|
mission: str | None,
|
|
|
|
|
description: str | None,
|
2026-02-02 14:33:31 +01:00
|
|
|
) -> Sector:
|
2026-02-12 09:10:03 +01:00
|
|
|
sect = Sector(
|
|
|
|
|
name, round_id, major_id, minor_id, influence_id, mission, description
|
|
|
|
|
)
|
2026-01-30 00:34:22 +01:00
|
|
|
self.sectors[sect.id] = sect
|
|
|
|
|
return sect
|
|
|
|
|
|
2026-01-30 15:32:44 +01:00
|
|
|
def get_sector(self, sector_id: str) -> Sector:
|
|
|
|
|
return self.sectors[sector_id]
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-01-30 15:32:44 +01:00
|
|
|
def get_sector_name(self, sector_id: str) -> str:
|
|
|
|
|
if sector_id is None:
|
|
|
|
|
return ""
|
|
|
|
|
return self.sectors[sector_id].name
|
2026-01-30 00:34:22 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def get_all_sectors(self) -> List[Sector]:
|
2026-01-30 00:34:22 +01:00
|
|
|
return list(self.sectors.values())
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
def update_sector(
|
|
|
|
|
self,
|
|
|
|
|
sector_id: str,
|
|
|
|
|
*,
|
|
|
|
|
name: str,
|
2026-02-11 19:22:43 +01:00
|
|
|
round_id: str | None,
|
|
|
|
|
major_id: str | None,
|
|
|
|
|
minor_id: str | None,
|
|
|
|
|
influence_id: str | None,
|
2026-02-12 09:10:03 +01:00
|
|
|
mission: str | None,
|
|
|
|
|
description: str | None,
|
2026-02-04 16:10:53 +01:00
|
|
|
) -> None:
|
2026-01-30 00:34:22 +01:00
|
|
|
sect = self.get_sector(sector_id)
|
2026-02-05 16:17:18 +01:00
|
|
|
old_round_id = sect.round_id
|
|
|
|
|
|
|
|
|
|
def apply_update() -> None:
|
|
|
|
|
sect.set_name(name)
|
|
|
|
|
sect.set_round(round_id)
|
|
|
|
|
sect.set_major(major_id)
|
|
|
|
|
sect.set_minor(minor_id)
|
|
|
|
|
sect.set_influence(influence_id)
|
2026-02-12 09:10:03 +01:00
|
|
|
sect.set_mission(mission)
|
|
|
|
|
sect.set_description(description)
|
2026-02-05 16:17:18 +01:00
|
|
|
|
|
|
|
|
if old_round_id == round_id:
|
|
|
|
|
apply_update()
|
|
|
|
|
return
|
|
|
|
|
affected_rounds: list[Round] = []
|
|
|
|
|
for rnd in self.rounds:
|
|
|
|
|
if rnd.id == old_round_id and (
|
|
|
|
|
rnd.has_choice_with_sector(sector_id)
|
|
|
|
|
or rnd.has_battle_with_sector(sector_id)
|
|
|
|
|
):
|
|
|
|
|
affected_rounds.append(rnd)
|
|
|
|
|
if not affected_rounds:
|
|
|
|
|
apply_update()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def cleanup_and_update() -> None:
|
|
|
|
|
for rnd in affected_rounds:
|
|
|
|
|
rnd.clear_sector_references(sector_id)
|
|
|
|
|
rnd.remove_battle(sector_id)
|
|
|
|
|
apply_update()
|
|
|
|
|
|
|
|
|
|
rounds_str = ", ".join(
|
|
|
|
|
str(self.get_round_index(rnd.id)) for rnd in affected_rounds
|
|
|
|
|
)
|
|
|
|
|
raise UpdateRequiresConfirmation(
|
|
|
|
|
message=(
|
|
|
|
|
f"Changing the round of this sector will affect round(s): {rounds_str}."
|
|
|
|
|
"\nRelated battles and choices will be cleared.\n"
|
|
|
|
|
"Do you want to continue?"
|
|
|
|
|
),
|
|
|
|
|
apply_update=cleanup_and_update,
|
|
|
|
|
)
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def remove_sector(self, sector_id: str) -> None:
|
2026-02-05 16:17:18 +01:00
|
|
|
rounds_blocking: list[Round] = []
|
|
|
|
|
for rnd in self.rounds:
|
|
|
|
|
if rnd.has_battle_with_sector(sector_id) or rnd.has_choice_with_sector(
|
|
|
|
|
sector_id
|
|
|
|
|
):
|
|
|
|
|
rounds_blocking.append(rnd)
|
|
|
|
|
if not rounds_blocking:
|
|
|
|
|
del self.sectors[sector_id]
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
def cleanup() -> None:
|
|
|
|
|
for rnd in rounds_blocking:
|
|
|
|
|
rnd.clear_sector_references(sector_id)
|
|
|
|
|
rnd.remove_battle(sector_id)
|
|
|
|
|
del self.sectors[sector_id]
|
|
|
|
|
|
|
|
|
|
rounds_str = ", ".join(
|
|
|
|
|
str(self.get_round_index(rnd.id)) for rnd in rounds_blocking
|
|
|
|
|
)
|
|
|
|
|
raise DeletionRequiresConfirmation(
|
|
|
|
|
message=(
|
|
|
|
|
f"This sector is used in round(s): {rounds_str}.\n"
|
|
|
|
|
"Battles and choices using this sector will be cleared.\n"
|
|
|
|
|
"Do you want to continue?"
|
|
|
|
|
),
|
|
|
|
|
cleanup_action=cleanup,
|
|
|
|
|
)
|
2026-01-30 00:34:22 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def get_sectors_in_round(self, round_id: str) -> List[Sector]:
|
2026-02-02 14:33:31 +01:00
|
|
|
sectors = [s for s in self.sectors.values() if s.round_id == round_id]
|
2026-01-30 18:55:39 +01:00
|
|
|
return sectors
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
# Round methods
|
2026-01-30 00:34:22 +01:00
|
|
|
|
2026-01-28 16:25:40 +01:00
|
|
|
def has_round(self, round_id: str) -> bool:
|
|
|
|
|
return any(r.id == round_id for r in self.rounds)
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-01-28 16:25:40 +01:00
|
|
|
def get_round(self, round_id: str) -> Round:
|
2026-01-30 15:32:44 +01:00
|
|
|
for rnd in self.rounds:
|
|
|
|
|
if rnd.id == round_id:
|
|
|
|
|
return rnd
|
|
|
|
|
raise KeyError(f"Round {round_id} not found")
|
2026-01-21 07:43:04 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def get_all_rounds(self) -> List[Round]:
|
2026-01-21 07:43:04 +01:00
|
|
|
return list(self.rounds)
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-01-21 07:43:04 +01:00
|
|
|
def add_round(self) -> Round:
|
|
|
|
|
round = Round()
|
|
|
|
|
self.rounds.append(round)
|
2026-01-22 23:42:47 +01:00
|
|
|
return round
|
2026-02-02 14:33:31 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def remove_round(self, round_id: str) -> None:
|
2026-01-22 23:42:47 +01:00
|
|
|
rnd = next((r for r in self.rounds if r.id == round_id), None)
|
2026-02-06 11:13:29 +01:00
|
|
|
for sect in self.sectors.values():
|
|
|
|
|
if sect.round_id == round_id:
|
|
|
|
|
sect.round_id = None
|
2026-01-22 23:42:47 +01:00
|
|
|
if rnd:
|
|
|
|
|
self.rounds.remove(rnd)
|
2026-01-27 11:49:37 +01:00
|
|
|
|
2026-02-06 11:13:29 +01:00
|
|
|
def get_round_index(self, round_id: str | None) -> int | None:
|
2026-01-30 00:34:22 +01:00
|
|
|
if round_id is None:
|
|
|
|
|
return None
|
2026-01-27 11:49:37 +01:00
|
|
|
for index, rnd in enumerate(self.rounds, start=1):
|
|
|
|
|
if rnd.id == round_id:
|
|
|
|
|
return index
|
|
|
|
|
raise KeyError("Round not found in campaign")
|
2026-01-28 16:25:40 +01:00
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
# Choice methods
|
2026-01-30 15:32:44 +01:00
|
|
|
|
|
|
|
|
def create_choice(self, round_id: str, participant_id: str) -> Choice:
|
|
|
|
|
rnd = self.get_round(round_id)
|
|
|
|
|
return rnd.create_choice(participant_id)
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
def update_choice(
|
|
|
|
|
self,
|
|
|
|
|
round_id: str,
|
|
|
|
|
participant_id: str,
|
|
|
|
|
priority_sector_id: str | None,
|
|
|
|
|
secondary_sector_id: str | None,
|
|
|
|
|
comment: str | None,
|
2026-02-04 16:10:53 +01:00
|
|
|
) -> None:
|
2026-02-02 10:41:16 +01:00
|
|
|
rnd = self.get_round(round_id)
|
2026-02-02 14:33:31 +01:00
|
|
|
rnd.update_choice(
|
|
|
|
|
participant_id, priority_sector_id, secondary_sector_id, comment
|
|
|
|
|
)
|
2026-02-02 10:41:16 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def remove_choice(self, round_id: str, participant_id: str) -> None:
|
2026-01-30 15:32:44 +01:00
|
|
|
rnd = self.get_round(round_id)
|
|
|
|
|
rnd.remove_choice(participant_id)
|
|
|
|
|
|
2026-02-02 14:33:31 +01:00
|
|
|
# Battle methods
|
2026-01-30 18:55:39 +01:00
|
|
|
|
2026-02-11 19:22:43 +01:00
|
|
|
def all_rounds_finished(self) -> bool:
|
|
|
|
|
return all(r.is_over for r in self.rounds)
|
|
|
|
|
|
2026-01-30 18:55:39 +01:00
|
|
|
def create_battle(self, round_id: str, sector_id: str) -> Battle:
|
|
|
|
|
rnd = self.get_round(round_id)
|
|
|
|
|
return rnd.create_battle(sector_id)
|
2026-02-02 14:33:31 +01:00
|
|
|
|
|
|
|
|
def update_battle(
|
|
|
|
|
self,
|
|
|
|
|
round_id: str,
|
|
|
|
|
sector_id: str,
|
|
|
|
|
player_1_id: str | None,
|
|
|
|
|
player_2_id: str | None,
|
|
|
|
|
winner_id: str | None,
|
|
|
|
|
score: str | None,
|
|
|
|
|
victory_condition: str | None,
|
|
|
|
|
comment: str | None,
|
2026-02-04 16:10:53 +01:00
|
|
|
) -> None:
|
2026-02-02 10:41:16 +01:00
|
|
|
rnd = self.get_round(round_id)
|
2026-02-02 14:33:31 +01:00
|
|
|
rnd.update_battle(
|
|
|
|
|
sector_id,
|
|
|
|
|
player_1_id,
|
|
|
|
|
player_2_id,
|
|
|
|
|
winner_id,
|
|
|
|
|
score,
|
|
|
|
|
victory_condition,
|
|
|
|
|
comment,
|
|
|
|
|
)
|
2026-01-30 18:55:39 +01:00
|
|
|
|
2026-02-04 16:10:53 +01:00
|
|
|
def remove_battle(self, round_id: str, sector_id: str) -> None:
|
2026-01-30 18:55:39 +01:00
|
|
|
rnd = self.get_round(round_id)
|
|
|
|
|
rnd.remove_battle(sector_id)
|