From 25055732502433cb23affd934487a4c6967cf578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maxime=20R=C3=A9aux?= Date: Tue, 24 Mar 2026 11:40:40 +0100 Subject: [PATCH] order pairing draws with war ranking --- src/warchron/controller/round_controller.py | 1 + src/warchron/model/history.py | 20 ------ src/warchron/model/pairing.py | 69 +++++++++++++++++++-- 3 files changed, 65 insertions(+), 25 deletions(-) delete mode 100644 src/warchron/model/history.py diff --git a/src/warchron/controller/round_controller.py b/src/warchron/controller/round_controller.py index 393a76b..fb11b7b 100644 --- a/src/warchron/controller/round_controller.py +++ b/src/warchron/controller/round_controller.py @@ -86,6 +86,7 @@ class RoundController: rnd, part.id, ) + # TODO clarify status icons if alloc.priority != ChoiceStatus.NONE: priority_icon = QIcon( Icons.get_pixmap(IconName[alloc.priority.name]) diff --git a/src/warchron/model/history.py b/src/warchron/model/history.py deleted file mode 100644 index d0a1751..0000000 --- a/src/warchron/model/history.py +++ /dev/null @@ -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 diff --git a/src/warchron/model/pairing.py b/src/warchron/model/pairing.py index 50384a0..5ec90b0 100644 --- a/src/warchron/model/pairing.py +++ b/src/warchron/model/pairing.py @@ -1,5 +1,6 @@ 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 uuid import uuid4 @@ -18,7 +19,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 + +if TYPE_CHECKING: + from warchron.model.campaign import Campaign ScoredBattle = Tuple[int, Battle] @@ -260,15 +263,58 @@ class Pairing: context.participants, ) 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) - # TODO improve tie break with history parsing random.shuffle(shuffled_group) ordered.extend( campaign.war_to_campaign_part_id(pid) for pid in shuffled_group ) 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 def _assign_fallback( war: War, @@ -278,7 +324,7 @@ class Pairing: campaign = war.get_campaign_by_round(round.id) if campaign is None: raise DomainError("Campaign not found") - match_counts = CampaignHistory.build_match_count(campaign) + match_counts = Pairing.build_match_count(campaign) random.shuffle(remaining) for pid in list(remaining): available = round.get_battles_with_places() @@ -316,6 +362,19 @@ class Pairing: rematch_penalty += match_counts.get(key, 0) 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 def get_allocation_kind( war: War,