avoid rematch on choice fallback

This commit is contained in:
Maxime Réaux 2026-03-20 20:48:52 +01:00
parent b7a35f6712
commit d3101bc9f6
3 changed files with 65 additions and 9 deletions

View file

@ -0,0 +1,20 @@
from __future__ import annotations
from typing import Dict, Tuple
from warchron.model.campaign import Campaign
class CampaignHistory:
@staticmethod
def build_match_count(campaign: Campaign) -> Dict[Tuple[str, str], int]:
counts: Dict[Tuple[str, str], int] = {}
for rnd in campaign.rounds:
for battle in rnd.battles.values():
p1 = battle.player_1_id
p2 = battle.player_2_id
if not p1 or not p2:
continue
key = (p1, p2) if p1 < p2 else (p2, p1)
counts[key] = counts.get(key, 0) + 1
return counts

View file

@ -1,5 +1,5 @@
from __future__ import annotations
from typing import Dict, List
from typing import Dict, List, Tuple
from dataclasses import dataclass
from uuid import uuid4
@ -18,6 +18,9 @@ from warchron.model.scoring import ScoreComputer
from warchron.model.tiebreaking import TieBreaker, TieContext, ResolveTiesCallback
from warchron.model.war_event import TieResolved
from warchron.model.scoring import ParticipantScore
from warchron.model.history import CampaignHistory
ScoredBattle = Tuple[int, Battle]
@dataclass(frozen=True, slots=True)
@ -115,7 +118,7 @@ class Pairing:
use_priority=False,
score_value=score_value,
)
Pairing._assign_fallback(round, remaining)
Pairing._assign_fallback(war, round, remaining)
@staticmethod
def _run_phase(
@ -268,19 +271,52 @@ class Pairing:
@staticmethod
def _assign_fallback(
war: War,
round: Round,
remaining: List[str],
) -> None:
# TODO avoid rematch
campaign = war.get_campaign_by_round(round.id)
if campaign is None:
raise DomainError("Campaign not found")
match_counts = CampaignHistory.build_match_count(campaign)
random.shuffle(remaining)
for pid in list(remaining):
available = round.get_battles_with_places()
if not available:
raise DomainError("No available battle remaining")
if len(available) == 1:
available[0].assign_participant(pid)
remaining.remove(pid)
continue
raise DomainError(f"Ambiguous fallback for participant {pid}")
scored: List[ScoredBattle] = [
(
Pairing._fallback_score(pid, battle, match_counts),
battle,
)
for battle in available
]
print(f"scored for pid {pid}: {scored}")
scored.sort(key=lambda x: x[0])
best_score = scored[0][0]
best_battles = [b for s, b in scored if s == best_score]
chosen = sorted(best_battles, key=lambda b: b.sector_id)[0]
chosen.assign_participant(pid)
remaining.remove(pid)
@staticmethod
def _fallback_score(
pid: str,
battle: Battle,
match_counts: Dict[Tuple[str, str], int],
) -> int:
rematch_weight = 1
occupancy_weight = 1
occupants = [
p for p in (battle.player_1_id, battle.player_2_id) if p is not None
]
occupancy_penalty = len(occupants)
rematch_penalty = 0
for opp in occupants:
key = (pid, opp) if pid < opp else (opp, pid)
print(f"key for occupant {opp}: {key}")
rematch_penalty += match_counts.get(key, 0)
return rematch_penalty * rematch_weight + occupancy_penalty * occupancy_weight
@staticmethod
def get_allocation_kind(