optimise global fallbcak score

This commit is contained in:
Maxime Réaux 2026-03-24 15:30:05 +01:00
parent 2505573250
commit c144845376
4 changed files with 85 additions and 23 deletions

View file

@ -30,6 +30,7 @@ class Presenter:
campaign: Campaign | None = None,
round: Round | None = None,
) -> TieDialogData:
# TODO display Nth place
if ctx.context_type == ContextType.WAR:
if ctx.objective_id:
obj = war.objectives[ctx.objective_id]

View file

@ -3,7 +3,7 @@ from typing import Dict, List, Tuple, TYPE_CHECKING
from collections import defaultdict
from dataclasses import dataclass
from uuid import uuid4
from copy import deepcopy
import random
from warchron.constants import ContextType, ScoreKind, ChoiceStatus, AllocationType
@ -33,6 +33,14 @@ class AllocationResult:
fallback: bool
@dataclass
class _FallbackState:
assignments: List[Tuple[str, str]] # (pid, battle_id)
remaining: List[str]
occupancy: Dict[str, List[str]]
score: int
class Pairing:
@staticmethod
@ -325,36 +333,87 @@ class Pairing:
if campaign is None:
raise DomainError("Campaign not found")
match_counts = Pairing.build_match_count(campaign)
random.shuffle(remaining)
for pid in list(remaining):
available = round.get_battles_with_places()
if not available:
available_battles = round.get_battles_with_places()
if not available_battles:
raise DomainError("No available battle remaining")
scored: List[ScoredBattle] = [
(
Pairing._fallback_score(pid, battle, match_counts),
battle,
occupancy = {
b.sector_id: [p for p in (b.player_1_id, b.player_2_id) if p is not None]
for b in round.battles.values()
}
shuffled_remaining = list(remaining)
random.shuffle(shuffled_remaining)
initial = _FallbackState(
assignments=[],
remaining=shuffled_remaining,
occupancy=occupancy,
score=0,
)
for battle in available
]
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)
best = Pairing._search_best_fallback(
initial,
match_counts,
beam_width=10,
)
battle_map = {b.sector_id: b for b in round.battles.values()}
for pid, bid in best.assignments:
battle_map[bid].assign_participant(pid)
remaining.remove(pid)
@staticmethod
def _fallback_score(
def _search_best_fallback(
initial: _FallbackState,
match_counts: Dict[Tuple[str, str], int],
beam_width: int,
) -> _FallbackState:
states = [initial]
while True:
new_states: List[_FallbackState] = []
progress = False
for state in states:
if not state.remaining:
new_states.append(state)
continue
progress = True
pid = state.remaining[0]
next_remaining = state.remaining[1:]
battles = Pairing._available_virtual_battles(state)
random.shuffle(battles)
for sector_id in battles:
occupants = state.occupancy[sector_id]
if len(occupants) >= 2:
continue
score = Pairing._fallback_score_virtual(
pid,
occupants,
match_counts,
)
new_occupancy = deepcopy(state.occupancy)
new_occupancy[sector_id] = occupants + [pid]
new_states.append(
_FallbackState(
assignments=state.assignments + [(pid, sector_id)],
remaining=next_remaining,
occupancy=new_occupancy,
score=state.score + score,
)
)
if not progress:
break
new_states.sort(key=lambda s: (s.score, random.random()))
states = new_states[:beam_width]
return min(states, key=lambda s: s.score)
@staticmethod
def _available_virtual_battles(state: _FallbackState) -> List[str]:
return [sector_id for sector_id, occ in state.occupancy.items() if len(occ) < 2]
@staticmethod
def _fallback_score_virtual(
pid: str,
battle: Battle,
occupants: List[str],
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:

View file

@ -166,6 +166,7 @@ class Round:
return any(b.is_finished() for b in self.battles.values())
def all_battles_finished(self) -> bool:
# TODO exception for participant alone
return all(
b.winner_id is not None or b.is_draw() for b in self.battles.values()
)

View file

@ -201,6 +201,7 @@ class View(QtWidgets.QMainWindow, Ui_MainWindow):
self.on_delete_item(ItemType.PLAYER, player_id)
def display_players(self, players: List[ParticipantOption]) -> None:
# TODO display stats (war, campaign battles...)
table = self.playersTable
table.setSortingEnabled(False)
table.setRowCount(len(players))