resolve pairing WIP
This commit is contained in:
parent
0c6014e946
commit
241d7f10f5
11 changed files with 302 additions and 11 deletions
118
src/warchron/model/pairing.py
Normal file
118
src/warchron/model/pairing.py
Normal 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"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue