RL: baseline boti a evaluacny harness
RandomPlayer, HeuristicPlayer (MC tipper nad rozdaniami neznamych kariet + tipom riadena hracia heuristika) a McPlayer (MC ohodnotenie kazdeho kandidatskeho tahu nad rozdaniami konzistentnymi s dedukovanymi voidmi). Evaluacia s rotaciou sedadiel: py -m rl.evaluate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+264
@@ -0,0 +1,264 @@
|
||||
"""Baseline hraci pre evaluaciu a neskorsi warm-start siete (viz rl/DESIGN.md).
|
||||
|
||||
Spolocne rozhranie: `guess(rnd, seat) -> int` (tip 0..8) a
|
||||
`play(rnd, seat) -> int` (index karty 0..31). Hrac vidi len to, co by videl
|
||||
pri stole -- vlastnu ruku, tipy, dokoncene kopky a rozohranu kopku; do cudzich
|
||||
ruk nesiaha.
|
||||
"""
|
||||
|
||||
from collections import Counter
|
||||
from random import Random
|
||||
|
||||
from bridzik import cards, Card_colors, Stash
|
||||
from rl.encoding import (
|
||||
N_GUESS_ACTIONS, N_PLAY_ACTIONS,
|
||||
card_index, deduce_voids, guess_mask, legal_cards, play_mask,
|
||||
)
|
||||
|
||||
|
||||
class RandomPlayer:
|
||||
"""Uniformne nahodny legalny tah -- najslabsi mozny baseline."""
|
||||
|
||||
def __init__(self, rng: Random = None):
|
||||
self.rng = rng if rng is not None else Random()
|
||||
|
||||
def guess(self, rnd, seat: int) -> int:
|
||||
mask = guess_mask(rnd)
|
||||
return self.rng.choice([g for g in range(N_GUESS_ACTIONS) if mask[g]])
|
||||
|
||||
def play(self, rnd, seat: int) -> int:
|
||||
mask = play_mask(rnd, seat)
|
||||
return self.rng.choice([i for i in range(N_PLAY_ACTIONS) if mask[i]])
|
||||
|
||||
|
||||
def _strength(card) -> tuple:
|
||||
"""Absolutna sila karty: kazda cervena (tromf) bije kazdu necervenu."""
|
||||
return (card.color == Card_colors['HEARTS'], card.value.value)
|
||||
|
||||
|
||||
def _current_best(stash):
|
||||
"""Zatial vitazna karta rozohranej kopky (None ak sa este nevynieslo)."""
|
||||
first = stash.get_first_card() if stash is not None else None
|
||||
if first is None:
|
||||
return None
|
||||
best = first
|
||||
for card in stash.get_cards().values():
|
||||
if _beats(card, best):
|
||||
best = card
|
||||
return best
|
||||
|
||||
|
||||
def _beats(card, best) -> bool:
|
||||
"""Ci `card` prebije `best` (karta drziaca kopku; jej farba je smerodajna)."""
|
||||
if card.color == best.color:
|
||||
return card.value > best.value
|
||||
return card.color == Card_colors['HEARTS']
|
||||
|
||||
|
||||
def simulate_tricks(hands: dict, leader: int, rng: Random) -> list:
|
||||
"""Dohra kopky s nahodnou legalnou strategiou; vrati pocty vyhier hracov.
|
||||
|
||||
`hands` je dict seat -> zoznam kariet (rovnako velke ruky); zoznamy sa
|
||||
spotrebuju. Vitaza kopky urcuje enginovy Stash.get_winner() -- pravidla
|
||||
sa tu neduplikuju.
|
||||
"""
|
||||
tricks = [0] * 4
|
||||
for _ in range(len(hands[leader])):
|
||||
stash = Stash(leader)
|
||||
for i in range(4):
|
||||
seat = (leader + i) % 4
|
||||
card = rng.choice(legal_cards(hands[seat], stash.get_first_card()))
|
||||
hands[seat].remove(card)
|
||||
stash.add_card(seat, card)
|
||||
leader = stash.get_winner()
|
||||
tricks[leader] += 1
|
||||
return tricks
|
||||
|
||||
|
||||
def deal_consistent(unknown: list, hand_sizes: dict, voids: dict,
|
||||
rng: Random, max_tries: int = 20) -> dict:
|
||||
"""Nahodne rozdanie neznamych kariet superom respektujuce voidy.
|
||||
|
||||
Greedy priradenie po zamiesani (najviac obmedzeni hraci prvi); ak sa
|
||||
konzistentne rozdanie nepodari za `max_tries`, padne na rozdanie bez
|
||||
voidov (zriedkave, radsej mierne skreslena vzorka nez ziadna).
|
||||
Zvysok kariet ostava v odlozenej kope mimo hry.
|
||||
"""
|
||||
seats = sorted(hand_sizes, key=lambda s: len(voids.get(s, ())), reverse=True)
|
||||
pool = list(unknown)
|
||||
for _ in range(max_tries):
|
||||
rng.shuffle(pool)
|
||||
remaining = list(pool)
|
||||
hands = {}
|
||||
for seat in seats:
|
||||
hand, rest, banned = [], [], voids.get(seat, set())
|
||||
for card in remaining:
|
||||
if len(hand) < hand_sizes[seat] and card.color not in banned:
|
||||
hand.append(card)
|
||||
else:
|
||||
rest.append(card)
|
||||
if len(hand) < hand_sizes[seat]:
|
||||
break
|
||||
hands[seat] = hand
|
||||
remaining = rest
|
||||
else:
|
||||
return hands
|
||||
rng.shuffle(pool)
|
||||
idx = 0
|
||||
hands = {}
|
||||
for seat in seats:
|
||||
hands[seat] = pool[idx:idx + hand_sizes[seat]]
|
||||
idx += hand_sizes[seat]
|
||||
return hands
|
||||
|
||||
|
||||
def mc_guess_distribution(rnd, seat: int, n_samples: int, rng: Random) -> Counter:
|
||||
"""Monte Carlo odhad rozdelenia poctu vlastnych kopiek v kole.
|
||||
|
||||
Nezname karty sa v kazdej vzorke nahodne rozdelia ostatnym trom hracom
|
||||
-- kazdemu len (8 - round_number) kariet, zvysok ostava v odlozenej kope
|
||||
mimo hry (pozri DESIGN.md, pasca "discard pile"). Leader prvej kopky je
|
||||
v case tipovania neznamy (najvyssi tip), sampluje sa uniformne.
|
||||
"""
|
||||
hand = rnd.player_cards[seat]
|
||||
hand_size = 8 - rnd.round_number
|
||||
unknown = [c for c in cards if c not in hand]
|
||||
counts = Counter()
|
||||
for _ in range(n_samples):
|
||||
rng.shuffle(unknown)
|
||||
sim_hands = {seat: list(hand)}
|
||||
others = [s for s in range(4) if s != seat]
|
||||
for i, other in enumerate(others):
|
||||
sim_hands[other] = unknown[i * hand_size:(i + 1) * hand_size]
|
||||
tricks = simulate_tricks(sim_hands, rng.randrange(4), rng)
|
||||
counts[tricks[seat]] += 1
|
||||
return counts
|
||||
|
||||
|
||||
def finish_round(hands: dict, current_cards: dict, first_player: int,
|
||||
me: int, my_card, tricks: list, rng: Random) -> list:
|
||||
"""Dohra kolo od mojho tahu: dokonci rozohranu kopku (ja hram `my_card`,
|
||||
dalsi nahodne legalne) a zvysne kopky dohra nahodnou legalnou strategiou.
|
||||
`tricks` su uz vyhrane kopky (mutuje sa kopia volajuceho); vrati final."""
|
||||
stash = Stash(first_player)
|
||||
for seat, card in current_cards.items():
|
||||
stash.add_card(seat, card)
|
||||
stash.add_card(me, my_card)
|
||||
while not stash.is_completed():
|
||||
seat = stash.get_active_player()
|
||||
card = rng.choice(legal_cards(hands[seat], stash.get_first_card()))
|
||||
hands[seat].remove(card)
|
||||
stash.add_card(seat, card)
|
||||
leader = stash.get_winner()
|
||||
tricks[leader] += 1
|
||||
while hands[leader]:
|
||||
stash = Stash(leader)
|
||||
for i in range(4):
|
||||
seat = (leader + i) % 4
|
||||
card = rng.choice(legal_cards(hands[seat], stash.get_first_card()))
|
||||
hands[seat].remove(card)
|
||||
stash.add_card(seat, card)
|
||||
leader = stash.get_winner()
|
||||
tricks[leader] += 1
|
||||
return tricks
|
||||
|
||||
|
||||
class HeuristicPlayer:
|
||||
"""MC tipper + jednoducha hracia heuristika riadena vlastnym tipom."""
|
||||
|
||||
def __init__(self, rng: Random = None, n_samples: int = 100):
|
||||
self.rng = rng if rng is not None else Random()
|
||||
self.n_samples = n_samples
|
||||
|
||||
def guess(self, rnd, seat: int) -> int:
|
||||
counts = mc_guess_distribution(rnd, seat, self.n_samples, self.rng)
|
||||
mask = guess_mask(rnd)
|
||||
# najcastejsi LEGALNY pocet kopiek (mod rozdelenia, nie priemer --
|
||||
# boduje sa len presna zhoda); pri nule vzoriek pre legalny tip
|
||||
# rozhodne blizkost k celkovemu modu
|
||||
mode = counts.most_common(1)[0][0]
|
||||
legal = [g for g in range(N_GUESS_ACTIONS) if mask[g]]
|
||||
return max(legal, key=lambda g: (counts[g], -abs(g - mode)))
|
||||
|
||||
def play(self, rnd, seat: int) -> int:
|
||||
hand = rnd.player_cards[seat]
|
||||
stash = rnd.get_last_stash()
|
||||
allowed = legal_cards(hand, stash.get_first_card() if stash else None)
|
||||
need = rnd.guesses[seat] - rnd.get_stashes_winner_summary()[seat]
|
||||
best = _current_best(stash)
|
||||
|
||||
if best is None:
|
||||
# vynasam: chcem kopku -> najsilnejsia karta; nechcem -> najslabsia
|
||||
chosen = max(allowed, key=_strength) if need > 0 else min(allowed, key=_strength)
|
||||
else:
|
||||
winning = [c for c in allowed if _beats(c, best)]
|
||||
if need > 0 and winning:
|
||||
# ber kopku co najlacnejsie
|
||||
chosen = min(winning, key=_strength)
|
||||
elif need <= 0 and len(winning) < len(allowed):
|
||||
# kopku nechcem: zbav sa najsilnejsej neberucej karty
|
||||
chosen = max((c for c in allowed if not _beats(c, best)), key=_strength)
|
||||
elif need <= 0:
|
||||
# vsetko berie -> ber co najlacnejsie (setri silne karty netreba,
|
||||
# ale nizka karta drzi sancu, ze ma este niekto prebije)
|
||||
chosen = min(allowed, key=_strength)
|
||||
else:
|
||||
# kopku chcem, ale nic neberie -> odhod najslabsiu
|
||||
chosen = min(allowed, key=_strength)
|
||||
return card_index(chosen)
|
||||
|
||||
|
||||
class McPlayer(HeuristicPlayer):
|
||||
"""Heuristika s MC hracou fazou: kazdy kandidatsky tah sa ohodnoti
|
||||
simulaciami zvysku kola nad rozdaniami neznamych kariet konzistentnymi
|
||||
s dedukovanymi voidmi (`use_voids=False` = ablacia bez dedukcie).
|
||||
Tipovanie ostava MC tipper z HeuristicPlayer (pred prvou kartou niet
|
||||
z coho voidy dedukovat)."""
|
||||
|
||||
def __init__(self, rng: Random = None, n_samples: int = 100,
|
||||
play_samples: int = 24, use_voids: bool = True):
|
||||
super().__init__(rng, n_samples)
|
||||
self.play_samples = play_samples
|
||||
self.use_voids = use_voids
|
||||
|
||||
def play(self, rnd, seat: int) -> int:
|
||||
hand = rnd.player_cards[seat]
|
||||
stash = rnd.get_last_stash()
|
||||
first_card = stash.get_first_card() if stash else None
|
||||
candidates = legal_cards(hand, first_card)
|
||||
if len(candidates) == 1:
|
||||
return card_index(candidates[0])
|
||||
|
||||
target = rnd.guesses[seat]
|
||||
base_tricks = rnd.get_stashes_winner_summary()
|
||||
voids = deduce_voids(rnd) if self.use_voids else {}
|
||||
seen = set()
|
||||
played_count = Counter()
|
||||
for st in rnd.stashes:
|
||||
for other, card in st.get_cards().items():
|
||||
seen.add(card)
|
||||
played_count[other] += 1
|
||||
unknown = [c for c in cards if c not in seen and c not in hand]
|
||||
hand_size0 = 8 - rnd.round_number
|
||||
hand_sizes = {s: hand_size0 - played_count[s] for s in range(4) if s != seat}
|
||||
current_cards = stash.get_cards()
|
||||
|
||||
# spolocne rozdanie pre vsetkych kandidatov (common random numbers --
|
||||
# porovnavame tahy na tych istych svetoch, mensia variancia)
|
||||
scores = {card_index(c): 0 for c in candidates}
|
||||
for _ in range(self.play_samples):
|
||||
world = deal_consistent(unknown, hand_sizes, voids, self.rng)
|
||||
for candidate in candidates:
|
||||
sim_hands = {s: list(h) for s, h in world.items()}
|
||||
sim_hands[seat] = [c for c in hand if c != candidate]
|
||||
tricks = finish_round(sim_hands, dict(current_cards),
|
||||
stash.first_player, seat, candidate,
|
||||
list(base_tricks), self.rng)
|
||||
if tricks[seat] == target:
|
||||
scores[card_index(candidate)] += 1
|
||||
|
||||
best_index = max(scores, key=scores.get)
|
||||
if scores[best_index] == 0:
|
||||
# tip uz je (takmer) nedosiahnutelny -> aspon rozumny pravidlovy tah
|
||||
return super().play(rnd, seat)
|
||||
return best_index
|
||||
Reference in New Issue
Block a user