119 lines
4.3 KiB
Python
119 lines
4.3 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
from typing import Dict, List
|
||
|
|
import random
|
||
|
|
|
||
|
|
from warchron.constants import ContextType
|
||
|
|
from warchron.model.exception import (
|
||
|
|
DomainError,
|
||
|
|
ForbiddenOperation,
|
||
|
|
RequiresConfirmation,
|
||
|
|
)
|
||
|
|
from warchron.model.war import War
|
||
|
|
from warchron.model.round import Round
|
||
|
|
from warchron.model.battle import Battle
|
||
|
|
from warchron.model.score_service import ScoreService
|
||
|
|
|
||
|
|
|
||
|
|
class Pairing:
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def check_round_pairable(
|
||
|
|
round: Round,
|
||
|
|
) -> None:
|
||
|
|
if round.is_over:
|
||
|
|
raise ForbiddenOperation("Can not resolve pairing on finished round")
|
||
|
|
if round.has_finished_battle():
|
||
|
|
raise ForbiddenOperation("Can not resolve pairing with finished battle(s)")
|
||
|
|
if len(round.battles) * 2 < len(round.choices):
|
||
|
|
raise DomainError(
|
||
|
|
"There are not enough sectors for all participants to battle"
|
||
|
|
)
|
||
|
|
for pid, choice in round.choices.items():
|
||
|
|
if choice is not None and not choice.priority_sector_id:
|
||
|
|
raise DomainError(f"Missing priority choice for participant {pid}")
|
||
|
|
if choice is not None and not choice.secondary_sector_id:
|
||
|
|
raise DomainError(f"Missing secondary choice for participant {pid}")
|
||
|
|
|
||
|
|
def cleanup() -> None:
|
||
|
|
for bat in round.battles.values():
|
||
|
|
bat.cleanup_battle_players()
|
||
|
|
|
||
|
|
if any(
|
||
|
|
bat.player_1_id is not None or bat.player_2_id is not None
|
||
|
|
for bat in round.battles.values()
|
||
|
|
):
|
||
|
|
raise RequiresConfirmation(
|
||
|
|
"Battle(s) already have player(s) assigned for this round.\n"
|
||
|
|
"Battle players will be cleared.\n"
|
||
|
|
"Do you want to continue?",
|
||
|
|
action=cleanup,
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def assign_battles_to_participants(
|
||
|
|
war: War,
|
||
|
|
round: Round,
|
||
|
|
) -> None:
|
||
|
|
campaign = war.get_campaign_by_round(round.id)
|
||
|
|
if campaign is None:
|
||
|
|
raise DomainError(f"Campaign for round {round.id} doesn't exist")
|
||
|
|
scores = ScoreService.compute_scores(
|
||
|
|
war,
|
||
|
|
ContextType.CAMPAIGN,
|
||
|
|
campaign.id,
|
||
|
|
)
|
||
|
|
score_groups = ScoreService.group_participants_by_score(
|
||
|
|
scores, lambda score: score.victory_points
|
||
|
|
)
|
||
|
|
sector_to_battle: Dict[str, Battle] = {
|
||
|
|
b.sector_id: b for b in round.battles.values()
|
||
|
|
}
|
||
|
|
for group in score_groups:
|
||
|
|
# persistent equality → random order
|
||
|
|
ordered_group = list(group)
|
||
|
|
random.shuffle(ordered_group)
|
||
|
|
for participant_id in ordered_group:
|
||
|
|
camp_part = campaign.get_campaign_participant_by_war_participant_id(
|
||
|
|
participant_id
|
||
|
|
)
|
||
|
|
if camp_part:
|
||
|
|
Pairing._assign_single_participant(
|
||
|
|
round,
|
||
|
|
camp_part.id,
|
||
|
|
sector_to_battle,
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _assign_single_participant(
|
||
|
|
round: Round,
|
||
|
|
participant_id: str,
|
||
|
|
sector_to_battle: Dict[str, Battle],
|
||
|
|
) -> None:
|
||
|
|
choice = round.choices.get(participant_id)
|
||
|
|
preferred_sectors: List[str] = []
|
||
|
|
if choice:
|
||
|
|
if choice.priority_sector_id:
|
||
|
|
preferred_sectors.append(choice.priority_sector_id)
|
||
|
|
if choice.secondary_sector_id:
|
||
|
|
preferred_sectors.append(choice.secondary_sector_id)
|
||
|
|
# --- try preferred sectors ---
|
||
|
|
for sect_id in preferred_sectors:
|
||
|
|
battle = sector_to_battle.get(sect_id)
|
||
|
|
if not battle:
|
||
|
|
continue
|
||
|
|
if battle.get_available_places():
|
||
|
|
battle.assign_participant(participant_id)
|
||
|
|
return
|
||
|
|
# --- fallback rules ---
|
||
|
|
available_battles = round.get_battles_with_places()
|
||
|
|
if not available_battles:
|
||
|
|
raise RuntimeError("No available battle remaining")
|
||
|
|
if len(available_battles) == 1:
|
||
|
|
available_battles[0].assign_participant(participant_id)
|
||
|
|
return
|
||
|
|
# multiple remaining battles → warning
|
||
|
|
raise RuntimeError(
|
||
|
|
f"Ambiguous fallback for participant {participant_id}: "
|
||
|
|
"multiple battles still available"
|
||
|
|
)
|