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.
|
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).
|
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.
|
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
|
## 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 __future__ import annotations
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Tuple
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from uuid import uuid4
|
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.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
|
||||||
|
|
||||||
|
ScoredBattle = Tuple[int, Battle]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
|
|
@ -115,7 +118,7 @@ class Pairing:
|
||||||
use_priority=False,
|
use_priority=False,
|
||||||
score_value=score_value,
|
score_value=score_value,
|
||||||
)
|
)
|
||||||
Pairing._assign_fallback(round, remaining)
|
Pairing._assign_fallback(war, round, remaining)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _run_phase(
|
def _run_phase(
|
||||||
|
|
@ -268,19 +271,52 @@ class Pairing:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _assign_fallback(
|
def _assign_fallback(
|
||||||
|
war: War,
|
||||||
round: Round,
|
round: Round,
|
||||||
remaining: List[str],
|
remaining: List[str],
|
||||||
) -> None:
|
) -> 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):
|
for pid in list(remaining):
|
||||||
available = round.get_battles_with_places()
|
available = round.get_battles_with_places()
|
||||||
if not available:
|
if not available:
|
||||||
raise DomainError("No available battle remaining")
|
raise DomainError("No available battle remaining")
|
||||||
if len(available) == 1:
|
scored: List[ScoredBattle] = [
|
||||||
available[0].assign_participant(pid)
|
(
|
||||||
|
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)
|
remaining.remove(pid)
|
||||||
continue
|
|
||||||
raise DomainError(f"Ambiguous fallback for participant {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
|
@staticmethod
|
||||||
def get_allocation_kind(
|
def get_allocation_kind(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue