order pairing draws with war ranking

This commit is contained in:
Maxime Réaux 2026-03-24 11:40:40 +01:00
parent 188f5256fb
commit 2505573250
3 changed files with 65 additions and 25 deletions

View file

@ -86,6 +86,7 @@ class RoundController:
rnd, rnd,
part.id, part.id,
) )
# TODO clarify status icons
if alloc.priority != ChoiceStatus.NONE: if alloc.priority != ChoiceStatus.NONE:
priority_icon = QIcon( priority_icon = QIcon(
Icons.get_pixmap(IconName[alloc.priority.name]) Icons.get_pixmap(IconName[alloc.priority.name])

View file

@ -1,20 +0,0 @@
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,6 @@
from __future__ import annotations from __future__ import annotations
from typing import Dict, List, Tuple from typing import Dict, List, Tuple, TYPE_CHECKING
from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from uuid import uuid4 from uuid import uuid4
@ -18,7 +19,9 @@ from warchron.model.scoring import ScoreComputer
from warchron.model.tiebreaking import TieBreaker, TieContext, ResolveTiesCallback from warchron.model.tiebreaking import TieBreaker, TieContext, ResolveTiesCallback
from warchron.model.war_event import TieResolved from warchron.model.war_event import TieResolved
from warchron.model.scoring import ParticipantScore from warchron.model.scoring import ParticipantScore
from warchron.model.history import CampaignHistory
if TYPE_CHECKING:
from warchron.model.campaign import Campaign
ScoredBattle = Tuple[int, Battle] ScoredBattle = Tuple[int, Battle]
@ -260,15 +263,58 @@ class Pairing:
context.participants, context.participants,
) )
ordered: List[str] = [] ordered: List[str] = []
for group in ranked_groups: refined_groups = Pairing._refine_with_war_ranking(
war,
ranked_groups,
)
for group in refined_groups:
shuffled_group = list(group) shuffled_group = list(group)
# TODO improve tie break with history parsing
random.shuffle(shuffled_group) random.shuffle(shuffled_group)
ordered.extend( ordered.extend(
campaign.war_to_campaign_part_id(pid) for pid in shuffled_group campaign.war_to_campaign_part_id(pid) for pid in shuffled_group
) )
return ordered[:places] return ordered[:places]
@staticmethod
def _refine_with_war_ranking(
war: War,
ranked_groups: List[List[str]],
) -> List[List[str]]:
from warchron.model.checking import ResultChecker
scores = ScoreComputer.compute_scores(
war,
ContextType.WAR,
war.id,
)
def value_getter(score: ParticipantScore) -> int:
return score.victory_points
war_ranking = ResultChecker.get_effective_ranking(
war,
ContextType.WAR,
war.id,
ScoreKind.VP,
scores,
value_getter,
)
rank_map: Dict[str, int] = {}
for rank, group, _ in war_ranking:
for pid in group:
rank_map[pid] = rank
refined: List[List[str]] = []
for group in ranked_groups:
if len(group) <= 1:
refined.append(group)
continue
buckets: Dict[int, List[str]] = defaultdict(list)
for pid in group:
buckets[rank_map.get(pid, 10**9)].append(pid)
for rank in sorted(buckets.keys()):
refined.append(buckets[rank])
return refined
@staticmethod @staticmethod
def _assign_fallback( def _assign_fallback(
war: War, war: War,
@ -278,7 +324,7 @@ class Pairing:
campaign = war.get_campaign_by_round(round.id) campaign = war.get_campaign_by_round(round.id)
if campaign is None: if campaign is None:
raise DomainError("Campaign not found") raise DomainError("Campaign not found")
match_counts = CampaignHistory.build_match_count(campaign) match_counts = Pairing.build_match_count(campaign)
random.shuffle(remaining) random.shuffle(remaining)
for pid in list(remaining): for pid in list(remaining):
available = round.get_battles_with_places() available = round.get_battles_with_places()
@ -316,6 +362,19 @@ class Pairing:
rematch_penalty += match_counts.get(key, 0) rematch_penalty += match_counts.get(key, 0)
return rematch_penalty * rematch_weight + occupancy_penalty * occupancy_weight return rematch_penalty * rematch_weight + occupancy_penalty * occupancy_weight
@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
@staticmethod @staticmethod
def get_allocation_kind( def get_allocation_kind(
war: War, war: War,