avoid rematch on choice fallback
This commit is contained in:
parent
b7a35f6712
commit
d3101bc9f6
3 changed files with 65 additions and 9 deletions
|
|
@ -12,7 +12,7 @@ A campaign event presents customisable sectors to fight on during battle rounds.
|
|||
A round includes battles to combine all participants according to their choice. Rounds are successive and are used for participants pairing in different priority modes.
|
||||
Winning battle grants victory points, narrative points (optional) and influence token (optional).
|
||||
Round results determine campaign score, which determines the war score, in different counting modes.
|
||||
Victory points determine the winner, narrative points grant scenario award(s) and influence tokens decide tie-breaks.
|
||||
Victory points determine the winner, narrative points determine the scenario(s) achiever(s) and influence tokens decide tie-breaks.
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
|
|||
20
src/warchron/model/history.py
Normal file
20
src/warchron/model/history.py
Normal 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
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue