compute campaign points from rounds

This commit is contained in:
Maxime Réaux 2026-02-19 14:17:42 +01:00
parent 45c1d69a3f
commit 7c9c941864
9 changed files with 165 additions and 75 deletions

View file

@ -43,12 +43,13 @@ class Battle:
def is_draw(self) -> bool:
if self.winner_id is not None:
return False
# Case 1: score entered → interpreted as unresolved outcome
if self.score and self.score.strip():
return True
# Case 2: explicit draw mention
if self.victory_condition:
if "draw" in self.victory_condition.casefold():
if any(
keyword in (self.victory_condition or "").casefold()
for keyword in ["draw", "tie", "square"]
):
return True
return False

View file

@ -284,6 +284,22 @@ class Campaign:
return rnd
raise KeyError(f"Round {round_id} not found")
def get_round_index(self, round_id: str | None) -> int | None:
if round_id is None:
return None
for index, rnd in enumerate(self.rounds, start=1):
if rnd.id == round_id:
return index
raise KeyError("Round not found in campaign")
# TODO replace multiloops by internal has_* method
def get_round_by_battle(self, sector_id: str) -> Round:
for rnd in self.rounds:
for bat in rnd.battles.values():
if bat.sector_id == sector_id:
return rnd
raise KeyError(f"Battle {sector_id} not found in any Round")
def get_all_rounds(self) -> List[Round]:
return list(self.rounds)
@ -309,14 +325,6 @@ class Campaign:
if rnd:
self.rounds.remove(rnd)
def get_round_index(self, round_id: str | None) -> int | None:
if round_id is None:
return None
for index, rnd in enumerate(self.rounds, start=1):
if rnd.id == round_id:
return index
raise KeyError("Round not found in campaign")
# Choice methods
def create_choice(self, round_id: str, participant_id: str) -> Choice:

View file

@ -32,10 +32,9 @@ class ClosureService:
)
if already_granted:
return
base_winner = None
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,

View file

@ -1,45 +1,77 @@
from typing import Dict, TYPE_CHECKING
from typing import Dict, Iterator
from dataclasses import dataclass, field
if TYPE_CHECKING:
from warchron.model.war import War
from warchron.constants import ContextType
from warchron.model.tie_manager import TieResolver
from warchron.model.war import War
from warchron.model.battle import Battle
@dataclass(slots=True)
class ParticipantScore:
victory_points: int = 0
narrative_points: Dict[str, int] = field(default_factory=dict)
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
def _get_battles_for_context(
war: War, context_type: ContextType, context_id: str
) -> Iterator[Battle]:
if context_type == ContextType.WAR:
for camp in war.campaigns:
for rnd in camp.rounds:
if not rnd.is_over:
continue
yield from rnd.battles.values()
elif context_type == ContextType.CAMPAIGN:
campaign = war.get_campaign(context_id)
for rnd in campaign.rounds:
if not rnd.is_over:
continue
yield from rnd.battles.values()
elif context_type == ContextType.BATTLE:
battle = war.get_battle(context_id)
campaign = war.get_campaign_by_sector(battle.sector_id)
rnd = campaign.get_round_by_battle(context_id)
if rnd and rnd.is_over:
yield battle
@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
# def compute_round_results(round)
# def compute_campaign_winner(campaign)
# def compute_war_winner(war)
def compute_scores(
war: War, context_type: ContextType, context_id: str
) -> Dict[str, ParticipantScore]:
scores = {
pid: ParticipantScore(
narrative_points={obj_id: 0 for obj_id in war.objectives}
)
for pid in war.participants
}
battles = ScoreService._get_battles_for_context(war, context_type, context_id)
for battle in battles:
base_winner = None
if battle.winner_id is not None:
campaign = war.get_campaign_by_campaign_participant(battle.winner_id)
if campaign is not None:
base_winner = campaign.participants[
battle.winner_id
].war_participant_id
winner = TieResolver.get_effective_winner_id(
war, ContextType.BATTLE, battle.sector_id, base_winner
)
if winner is None:
continue
scores[winner].victory_points += 1
sector = war.get_sector(battle.sector_id)
if sector.major_objective_id:
scores[winner].narrative_points[
sector.major_objective_id
] += war.major_value
if sector.minor_objective_id:
scores[winner].narrative_points[
sector.minor_objective_id
] += war.minor_value
return scores

View file

@ -413,6 +413,15 @@ class War:
# Battle methods
# TODO replace multiloops by internal has_* method
def get_battle(self, battle_id: str) -> Battle:
for camp in self.campaigns:
for rnd in camp.rounds:
for bat in rnd.battles.values():
if bat.sector_id == battle_id:
return bat
raise KeyError("Round not found")
def create_battle(self, round_id: str, sector_id: str) -> Battle:
camp = self.get_campaign_by_round(round_id)
if camp is not None: