Compare commits

..

No commits in common. "c144845376358a49c83c16cedac9e46a8ac0b5a5" and "188f5256fb84f00f4e5e7bbddffe7ba8a07bc93e" have entirely different histories.

6 changed files with 48 additions and 150 deletions

View file

@ -30,7 +30,6 @@ 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

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

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,9 +1,8 @@
from __future__ import annotations
from typing import Dict, List, Tuple, TYPE_CHECKING
from collections import defaultdict
from typing import Dict, List, Tuple
from dataclasses import dataclass
from uuid import uuid4
from copy import deepcopy
import random
from warchron.constants import ContextType, ScoreKind, ChoiceStatus, AllocationType
@ -19,9 +18,7 @@ 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
if TYPE_CHECKING:
from warchron.model.campaign import Campaign
from warchron.model.history import CampaignHistory
ScoredBattle = Tuple[int, Battle]
@ -33,14 +30,6 @@ 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
@ -271,58 +260,15 @@ class Pairing:
context.participants,
)
ordered: List[str] = []
refined_groups = Pairing._refine_with_war_ranking(
war,
ranked_groups,
)
for group in refined_groups:
for group in ranked_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,
@ -332,88 +278,37 @@ class Pairing:
campaign = war.get_campaign_by_round(round.id)
if campaign is None:
raise DomainError("Campaign not found")
match_counts = Pairing.build_match_count(campaign)
available_battles = round.get_battles_with_places()
if not available_battles:
raise DomainError("No available battle remaining")
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,
)
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)
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")
scored: List[ScoredBattle] = [
(
Pairing._fallback_score(pid, battle, match_counts),
battle,
)
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)
remaining.remove(pid)
@staticmethod
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(
def _fallback_score(
pid: str,
occupants: List[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:
@ -421,19 +316,6 @@ 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,

View file

@ -166,7 +166,6 @@ 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,7 +201,6 @@ 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))