resolve pairing WIP

This commit is contained in:
Maxime Réaux 2026-03-11 11:44:57 +01:00
parent 0c6014e946
commit 241d7f10f5
11 changed files with 302 additions and 11 deletions

View file

@ -0,0 +1,118 @@
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"
)