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:
@@ -0,0 +1,89 @@
|
|||||||
|
"""Evaluacny harness: odohra N kol medzi 4 hracmi a spocita metriky.
|
||||||
|
|
||||||
|
Metriky per hrac (viz rl/DESIGN.md, sekcia 5): priemerne body na kolo
|
||||||
|
a presnost tipu (% kol s presne trafenym tipom). Sedadla sa medzi kolami
|
||||||
|
rotuju, aby ziadny hrac nebol systematicky zvyhodneny poradim tipovania.
|
||||||
|
|
||||||
|
Spustenie ako skript porovna baseline botov:
|
||||||
|
py -m rl.evaluate --rounds 500 --seed 7
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from random import Random
|
||||||
|
|
||||||
|
from bridzik import ROUNDS_PER_SERIES
|
||||||
|
from rl.env import PHASE_GUESS, RoundEnv
|
||||||
|
|
||||||
|
|
||||||
|
def play_round(players: list, env: RoundEnv, round_number: int = None,
|
||||||
|
first_player: int = None) -> list:
|
||||||
|
"""Odohra jedno kolo; `players[seat]` rozhoduje za sedadlo `seat`.
|
||||||
|
|
||||||
|
Vrati body 4 sedadiel (`Round.get_points_summary()`).
|
||||||
|
"""
|
||||||
|
decision = env.reset(round_number, first_player)
|
||||||
|
while True:
|
||||||
|
seat = decision.player
|
||||||
|
if decision.phase == PHASE_GUESS:
|
||||||
|
action = players[seat].guess(env.round, seat)
|
||||||
|
else:
|
||||||
|
action = players[seat].play(env.round, seat)
|
||||||
|
decision, rewards, done = env.step(action)
|
||||||
|
if done:
|
||||||
|
return rewards
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(players: list, n_rounds: int, rng: Random = None,
|
||||||
|
round_numbers: list = None) -> list:
|
||||||
|
"""Odohra `n_rounds` kol s rotaciou sedadiel; vrati stats per hrac.
|
||||||
|
|
||||||
|
Vystup: zoznam dictov v poradi `players` --
|
||||||
|
{'avg_points': float, 'hit_rate': float, 'rounds': int}.
|
||||||
|
"""
|
||||||
|
rng = rng if rng is not None else Random()
|
||||||
|
env = RoundEnv(rng)
|
||||||
|
points = [0] * 4
|
||||||
|
hits = [0] * 4
|
||||||
|
for i in range(n_rounds):
|
||||||
|
round_number = rng.choice(round_numbers) if round_numbers \
|
||||||
|
else rng.randrange(ROUNDS_PER_SERIES)
|
||||||
|
# rotacia: sedadlo s obsadzuje players[(s + i) % 4]
|
||||||
|
seating = [players[(s + i) % 4] for s in range(4)]
|
||||||
|
rewards = play_round(seating, env, round_number)
|
||||||
|
for seat in range(4):
|
||||||
|
player_index = (seat + i) % 4
|
||||||
|
points[player_index] += rewards[seat]
|
||||||
|
hits[player_index] += rewards[seat] > 0
|
||||||
|
return [{'avg_points': points[p] / n_rounds,
|
||||||
|
'hit_rate': hits[p] / n_rounds,
|
||||||
|
'rounds': n_rounds} for p in range(len(players))]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
from rl.players import HeuristicPlayer, RandomPlayer
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Evaluacia baseline botov')
|
||||||
|
parser.add_argument('--rounds', type=int, default=500)
|
||||||
|
parser.add_argument('--seed', type=int, default=7)
|
||||||
|
parser.add_argument('--mc-samples', type=int, default=100)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
rng = Random(args.seed)
|
||||||
|
lineups = [
|
||||||
|
('4x random', [RandomPlayer(rng) for _ in range(4)]),
|
||||||
|
('1x heuristika + 3x random',
|
||||||
|
[HeuristicPlayer(rng, n_samples=args.mc_samples)]
|
||||||
|
+ [RandomPlayer(rng) for _ in range(3)]),
|
||||||
|
('4x heuristika',
|
||||||
|
[HeuristicPlayer(rng, n_samples=args.mc_samples) for _ in range(4)]),
|
||||||
|
]
|
||||||
|
for label, players in lineups:
|
||||||
|
stats = evaluate(players, args.rounds, rng)
|
||||||
|
print(f'\n{label} ({args.rounds} kol):')
|
||||||
|
for i, s in enumerate(stats):
|
||||||
|
print(f' hrac {i}: {s["avg_points"]:6.2f} bodov/kolo, '
|
||||||
|
f'tip trafeny {100 * s["hit_rate"]:5.1f} %')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
+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
|
||||||
@@ -0,0 +1,316 @@
|
|||||||
|
import unittest
|
||||||
|
from random import Random
|
||||||
|
|
||||||
|
from bridzik import cards, Card, Card_colors, Card_values, Round
|
||||||
|
from rl.encoding import card_index, index_card, legal_cards
|
||||||
|
from rl.env import Decision, PHASE_GUESS, PHASE_PLAY, RoundEnv
|
||||||
|
from rl.evaluate import evaluate, play_round
|
||||||
|
from rl.players import (
|
||||||
|
HeuristicPlayer, McPlayer, RandomPlayer,
|
||||||
|
_beats, _current_best, deal_consistent, deduce_voids,
|
||||||
|
mc_guess_distribution, simulate_tricks,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoundEnvCase(unittest.TestCase):
|
||||||
|
def test_episode_structure(self):
|
||||||
|
env = RoundEnv(Random(42))
|
||||||
|
decision = env.reset(round_number=6, first_player=1)
|
||||||
|
rng = Random(0)
|
||||||
|
|
||||||
|
# prve 4 rozhodnutia su tipy, v poradi od first_player
|
||||||
|
expected_guessers = [1, 2, 3, 0]
|
||||||
|
for expected in expected_guessers:
|
||||||
|
self.assertIsInstance(decision, Decision)
|
||||||
|
self.assertEqual(decision.phase, PHASE_GUESS)
|
||||||
|
self.assertEqual(decision.player, expected)
|
||||||
|
action = rng.choice([g for g in range(9) if decision.mask[g]])
|
||||||
|
decision, rewards, done = env.step(action)
|
||||||
|
self.assertIsNone(rewards)
|
||||||
|
self.assertFalse(done)
|
||||||
|
|
||||||
|
# potom hracie rozhodnutia az po terminal: 2 karty x 4 hraci
|
||||||
|
steps = 0
|
||||||
|
while True:
|
||||||
|
self.assertEqual(decision.phase, PHASE_PLAY)
|
||||||
|
self.assertEqual(decision.player, env.round.get_active_player())
|
||||||
|
action = rng.choice([i for i in range(32) if decision.mask[i]])
|
||||||
|
decision, rewards, done = env.step(action)
|
||||||
|
steps += 1
|
||||||
|
if done:
|
||||||
|
break
|
||||||
|
self.assertEqual(steps, 8)
|
||||||
|
self.assertIsNone(decision)
|
||||||
|
self.assertEqual(rewards, env.round.get_points_summary())
|
||||||
|
self.assertEqual(len(rewards), 4)
|
||||||
|
|
||||||
|
# po done sa step neda volat, reset zacne novu epizodu
|
||||||
|
self.assertRaises(RuntimeError, env.step, 0)
|
||||||
|
self.assertIsInstance(env.reset(), Decision)
|
||||||
|
|
||||||
|
def test_reset_samples_round_and_seat(self):
|
||||||
|
env = RoundEnv(Random(7))
|
||||||
|
seen_rounds, seen_seats = set(), set()
|
||||||
|
for _ in range(100):
|
||||||
|
env.reset()
|
||||||
|
seen_rounds.add(env.round.round_number)
|
||||||
|
seen_seats.add(env.round.first_player)
|
||||||
|
self.assertEqual(seen_rounds, set(range(8)))
|
||||||
|
self.assertEqual(seen_seats, set(range(4)))
|
||||||
|
|
||||||
|
def test_deterministic_with_seed(self):
|
||||||
|
rewards = []
|
||||||
|
for _ in range(2):
|
||||||
|
env = RoundEnv(Random(123))
|
||||||
|
players = [RandomPlayer(Random(5)) for _ in range(4)]
|
||||||
|
rewards.append(play_round(players, env, round_number=0))
|
||||||
|
self.assertEqual(rewards[0], rewards[1])
|
||||||
|
|
||||||
|
|
||||||
|
class SimulationHelpersCase(unittest.TestCase):
|
||||||
|
def test_beats(self):
|
||||||
|
heart_7 = Card(Card_colors['HEARTS'], Card_values['C7'])
|
||||||
|
heart_8 = Card(Card_colors['HEARTS'], Card_values['C8'])
|
||||||
|
leaves_ace = Card(Card_colors['LEAVES'], Card_values['ACE'])
|
||||||
|
leaves_king = Card(Card_colors['LEAVES'], Card_values['KING'])
|
||||||
|
bells_ace = Card(Card_colors['BELLS'], Card_values['ACE'])
|
||||||
|
|
||||||
|
self.assertTrue(_beats(leaves_ace, leaves_king)) # vyssia vo farbe
|
||||||
|
self.assertFalse(_beats(leaves_king, leaves_ace))
|
||||||
|
self.assertTrue(_beats(heart_7, leaves_ace)) # tromf bije farbu
|
||||||
|
self.assertFalse(_beats(bells_ace, leaves_king)) # cudzia farba neberie
|
||||||
|
self.assertTrue(_beats(heart_8, heart_7)) # tromfy medzi sebou
|
||||||
|
self.assertFalse(_beats(leaves_ace, heart_7)) # farba nebije tromf
|
||||||
|
|
||||||
|
def test_current_best_tracks_stash(self):
|
||||||
|
from bridzik import Stash
|
||||||
|
leaves_7 = Card(Card_colors['LEAVES'], Card_values['C7'])
|
||||||
|
leaves_ace = Card(Card_colors['LEAVES'], Card_values['ACE'])
|
||||||
|
heart_7 = Card(Card_colors['HEARTS'], Card_values['C7'])
|
||||||
|
|
||||||
|
self.assertIsNone(_current_best(None))
|
||||||
|
s = Stash(0)
|
||||||
|
self.assertIsNone(_current_best(s))
|
||||||
|
s.add_card(0, leaves_7)
|
||||||
|
self.assertEqual(_current_best(s), leaves_7)
|
||||||
|
s.add_card(1, leaves_ace)
|
||||||
|
self.assertEqual(_current_best(s), leaves_ace)
|
||||||
|
s.add_card(2, heart_7)
|
||||||
|
self.assertEqual(_current_best(s), heart_7)
|
||||||
|
|
||||||
|
def test_simulate_tricks_consumes_hands(self):
|
||||||
|
rng = Random(3)
|
||||||
|
deck = list(cards)
|
||||||
|
rng.shuffle(deck)
|
||||||
|
hands = {seat: deck[seat * 8:(seat + 1) * 8] for seat in range(4)}
|
||||||
|
tricks = simulate_tricks(hands, leader=2, rng=rng)
|
||||||
|
self.assertEqual(sum(tricks), 8)
|
||||||
|
for seat in range(4):
|
||||||
|
self.assertEqual(hands[seat], [])
|
||||||
|
|
||||||
|
|
||||||
|
class HeuristicPlayerCase(unittest.TestCase):
|
||||||
|
@staticmethod
|
||||||
|
def _round_with_hand(player0_hand):
|
||||||
|
# deterministicke rozdanie: player0_hand ide hracovi 0, zvysok dalej;
|
||||||
|
# deal_starting_cards najprv zahodi 4*round_number kariet, preto
|
||||||
|
# treba ruku umiestnit az ZA odkladaciu kopu
|
||||||
|
round_number = 8 - len(player0_hand)
|
||||||
|
rest = [c for c in cards if c not in player0_hand]
|
||||||
|
skip = 4 * round_number
|
||||||
|
deck = rest[:skip] + list(player0_hand) + rest[skip:]
|
||||||
|
return Round(round_number, 0, deck, shuffler=lambda l: None)
|
||||||
|
|
||||||
|
def test_mc_guess_all_hearts_is_certain(self):
|
||||||
|
# 8 cerveni = tromfy beru kazdu kopku bez ohladu na rozdanie a hru
|
||||||
|
all_hearts = [Card(Card_colors['HEARTS'], v) for v in Card_values]
|
||||||
|
r = self._round_with_hand(all_hearts)
|
||||||
|
counts = mc_guess_distribution(r, 0, n_samples=30, rng=Random(1))
|
||||||
|
self.assertEqual(counts, {8: 30})
|
||||||
|
self.assertEqual(HeuristicPlayer(Random(1), n_samples=30).guess(r, 0), 8)
|
||||||
|
|
||||||
|
def test_mc_guess_weak_hand_low(self):
|
||||||
|
# dve najnizsie necervene karty -> tip 0 s prehladom
|
||||||
|
weak = [Card(Card_colors['LEAVES'], Card_values['C7']),
|
||||||
|
Card(Card_colors['BELLS'], Card_values['C7'])]
|
||||||
|
r = self._round_with_hand(weak)
|
||||||
|
self.assertEqual(HeuristicPlayer(Random(2), n_samples=60).guess(r, 0), 0)
|
||||||
|
|
||||||
|
def test_mc_guess_respects_mask(self):
|
||||||
|
# posledny tipujuci: zakazana hodnota nesmie byt vratena, ani ked
|
||||||
|
# je modom rozdelenia
|
||||||
|
all_hearts = [Card(Card_colors['HEARTS'], v) for v in Card_values]
|
||||||
|
r = self._round_with_hand(all_hearts)
|
||||||
|
r.add_player_guess(0, 0)
|
||||||
|
r.add_player_guess(1, 0)
|
||||||
|
r.add_player_guess(2, 0)
|
||||||
|
# zakazany tip pre hraca 3 je 8; jeho ruka je nahodna, ale nech by
|
||||||
|
# simulacia vratila cokolvek, vysledok musi byt legalny
|
||||||
|
guess = HeuristicPlayer(Random(3), n_samples=20).guess(r, 3)
|
||||||
|
self.assertNotEqual(guess, 8)
|
||||||
|
self.assertIn(guess, range(8))
|
||||||
|
|
||||||
|
def test_play_takes_trick_when_needed(self):
|
||||||
|
hand = [Card(Card_colors['LEAVES'], Card_values['ACE']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C7']),
|
||||||
|
Card(Card_colors['BELLS'], Card_values['C7'])]
|
||||||
|
r = self._round_with_hand(hand)
|
||||||
|
r.add_player_guess(0, 3) # najvyssi tip -> hrac 0 vynasa
|
||||||
|
r.add_player_guess(1, 0)
|
||||||
|
r.add_player_guess(2, 0)
|
||||||
|
r.add_player_guess(3, 1)
|
||||||
|
# hrac 0 potrebuje kopky -> vynasa najsilnejsiu kartu (LEAVES ACE)
|
||||||
|
action = HeuristicPlayer(Random(4)).play(r, 0)
|
||||||
|
self.assertEqual(index_card(action), hand[0])
|
||||||
|
|
||||||
|
def test_play_ducks_when_satisfied(self):
|
||||||
|
hand = [Card(Card_colors['LEAVES'], Card_values['ACE']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C7']),
|
||||||
|
Card(Card_colors['BELLS'], Card_values['C7'])]
|
||||||
|
r = self._round_with_hand(hand)
|
||||||
|
r.add_player_guess(0, 0) # hrac 0 nechce ziadnu kopku
|
||||||
|
r.add_player_guess(1, 2) # najvyssi tip -> vynasa hrac 1
|
||||||
|
r.add_player_guess(2, 0)
|
||||||
|
r.add_player_guess(3, 0)
|
||||||
|
first_card = legal_cards(r.player_cards[1], None)[0]
|
||||||
|
r.play_card(1, first_card)
|
||||||
|
action = HeuristicPlayer(Random(5)).play(r, 2)
|
||||||
|
# legalnost staci overit enginom; strategiu netestujeme natvrdo,
|
||||||
|
# lebo zavisi od nahodnej ruky hraca 2
|
||||||
|
r.play_card(2, index_card(action))
|
||||||
|
|
||||||
|
def test_play_duck_scenario_deterministic(self):
|
||||||
|
# hrac 0 tipol 0, ma na ruke LEAVES ACE aj C7; kopku vedie LEAVES C8
|
||||||
|
# -> musi priznat farbu a spravne je podliezt (C7), nie zobrat esom
|
||||||
|
hand0 = [Card(Card_colors['LEAVES'], Card_values['ACE']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C7'])]
|
||||||
|
hand1 = [Card(Card_colors['LEAVES'], Card_values['C8']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C9'])]
|
||||||
|
rest = [c for c in cards if c not in hand0 + hand1]
|
||||||
|
deck = rest[:24] + hand0 + hand1 + rest[24:] # 24 = odkladacia kopa
|
||||||
|
r = Round(6, 0, deck, shuffler=lambda l: None)
|
||||||
|
r.add_player_guess(0, 0)
|
||||||
|
r.add_player_guess(1, 2) # vynasa hrac 1
|
||||||
|
r.add_player_guess(2, 0)
|
||||||
|
r.add_player_guess(3, 1) # 0+2+0+0 by bol zakazany sucet (2 kopky)
|
||||||
|
r.play_card(1, hand1[0])
|
||||||
|
action = HeuristicPlayer(Random(6)).play(r, 0)
|
||||||
|
self.assertEqual(index_card(action), hand0[1])
|
||||||
|
|
||||||
|
|
||||||
|
class VoidDeductionCase(unittest.TestCase):
|
||||||
|
def test_deduce_voids_from_stash(self):
|
||||||
|
# hrac 0 vynasa zelen; 1 prizna farbu (nic), 2 tromfne cervenou
|
||||||
|
# (void zelen), 3 hodi gulu (void zelen AJ cerven)
|
||||||
|
hand0 = [Card(Card_colors['LEAVES'], Card_values['C7']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C8'])]
|
||||||
|
hand1 = [Card(Card_colors['LEAVES'], Card_values['C9']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C10'])]
|
||||||
|
hand2 = [Card(Card_colors['HEARTS'], Card_values['C7']),
|
||||||
|
Card(Card_colors['ACORNS'], Card_values['C7'])]
|
||||||
|
hand3 = [Card(Card_colors['BELLS'], Card_values['C7']),
|
||||||
|
Card(Card_colors['BELLS'], Card_values['C8'])]
|
||||||
|
rest = [c for c in cards if c not in hand0 + hand1 + hand2 + hand3]
|
||||||
|
deck = rest[:24] + hand0 + hand1 + hand2 + hand3
|
||||||
|
r = Round(6, 0, deck, shuffler=lambda l: None)
|
||||||
|
r.add_player_guess(0, 2) # najvyssi tip -> vynasa 0
|
||||||
|
r.add_player_guess(1, 0)
|
||||||
|
r.add_player_guess(2, 0)
|
||||||
|
r.add_player_guess(3, 1)
|
||||||
|
|
||||||
|
self.assertEqual(deduce_voids(r), {0: set(), 1: set(), 2: set(), 3: set()})
|
||||||
|
r.play_card(0, hand0[0])
|
||||||
|
r.play_card(1, hand1[0]) # priznal farbu -> nic
|
||||||
|
r.play_card(2, hand2[0]) # cerven -> void zelen
|
||||||
|
r.play_card(3, hand3[0]) # gula -> void zelen aj cerven
|
||||||
|
voids = deduce_voids(r)
|
||||||
|
self.assertEqual(voids[0], set()) # vynasajuci neprezradza nic
|
||||||
|
self.assertEqual(voids[1], set())
|
||||||
|
self.assertEqual(voids[2], {Card_colors['LEAVES']})
|
||||||
|
self.assertEqual(voids[3], {Card_colors['LEAVES'], Card_colors['HEARTS']})
|
||||||
|
|
||||||
|
def test_deduced_voids_never_contradict_hands(self):
|
||||||
|
# fuzz: dedukovany void NIKDY neprotireci realnej ruke hraca
|
||||||
|
rng = Random(21)
|
||||||
|
for _ in range(30):
|
||||||
|
r = Round(rng.randrange(4), rng.randrange(4))
|
||||||
|
players = [RandomPlayer(Random(rng.random())) for _ in range(4)]
|
||||||
|
for _ in range(4):
|
||||||
|
seat = r.get_active_player()
|
||||||
|
r.add_player_guess(seat, players[seat].guess(r, seat))
|
||||||
|
while not r.is_completed():
|
||||||
|
seat = r.get_active_player()
|
||||||
|
r.play_card(seat, index_card(players[seat].play(r, seat)))
|
||||||
|
for other, banned in deduce_voids(r).items():
|
||||||
|
held = {c.color for c in r.player_cards[other]}
|
||||||
|
self.assertFalse(held & banned,
|
||||||
|
f'void {banned} vs ruka {held}')
|
||||||
|
|
||||||
|
def test_deal_consistent_respects_voids(self):
|
||||||
|
rng = Random(22)
|
||||||
|
unknown = [c for c in cards][:20]
|
||||||
|
voids = {1: {Card_colors['HEARTS']}, 2: set(),
|
||||||
|
3: {Card_colors['LEAVES'], Card_colors['BELLS']}}
|
||||||
|
for _ in range(20):
|
||||||
|
hands = deal_consistent(unknown, {1: 4, 2: 4, 3: 4}, voids, rng)
|
||||||
|
self.assertTrue(all(len(h) == 4 for h in hands.values()))
|
||||||
|
for seat, banned in voids.items():
|
||||||
|
self.assertFalse({c.color for c in hands[seat]} & banned)
|
||||||
|
|
||||||
|
|
||||||
|
class McPlayerCase(unittest.TestCase):
|
||||||
|
def test_plays_legal_full_rounds(self):
|
||||||
|
rng = Random(23)
|
||||||
|
env = RoundEnv(rng)
|
||||||
|
players = [McPlayer(Random(24), n_samples=20, play_samples=8),
|
||||||
|
McPlayer(Random(25), n_samples=20, play_samples=8,
|
||||||
|
use_voids=False)] \
|
||||||
|
+ [RandomPlayer(Random(s)) for s in (26, 27)]
|
||||||
|
for round_number in range(8):
|
||||||
|
rewards = play_round(players, env, round_number)
|
||||||
|
self.assertEqual(len(rewards), 4)
|
||||||
|
|
||||||
|
def test_duck_scenario(self):
|
||||||
|
# tip 0, kopku vedie sused LEAVES C8 a ja som HNED na tahu (MC hrac
|
||||||
|
# stavia kopku poctivo, takze na rozdiel od pravidlovej heuristiky
|
||||||
|
# vyzaduje konzistentne poradie): mam ACE aj C7 -> podlezt sedmickou
|
||||||
|
hand0 = [Card(Card_colors['LEAVES'], Card_values['ACE']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C7'])]
|
||||||
|
hand3 = [Card(Card_colors['LEAVES'], Card_values['C8']),
|
||||||
|
Card(Card_colors['LEAVES'], Card_values['C9'])]
|
||||||
|
rest = [c for c in cards if c not in hand0 + hand3]
|
||||||
|
deck = rest[:24] + hand0 + rest[24:26] + rest[26:28] + hand3
|
||||||
|
r = Round(6, 0, deck, shuffler=lambda l: None)
|
||||||
|
r.add_player_guess(0, 0)
|
||||||
|
r.add_player_guess(1, 0)
|
||||||
|
r.add_player_guess(2, 0)
|
||||||
|
r.add_player_guess(3, 1) # najvyssi tip -> vynasa hrac 3, po nom ja
|
||||||
|
r.play_card(3, hand3[0])
|
||||||
|
self.assertEqual(r.get_active_player(), 0)
|
||||||
|
action = McPlayer(Random(28), play_samples=30).play(r, 0)
|
||||||
|
self.assertEqual(index_card(action), hand0[1])
|
||||||
|
|
||||||
|
|
||||||
|
class EvaluateCase(unittest.TestCase):
|
||||||
|
def test_full_random_matchup_runs(self):
|
||||||
|
rng = Random(11)
|
||||||
|
stats = evaluate([RandomPlayer(rng) for _ in range(4)], 40, rng)
|
||||||
|
for s in stats:
|
||||||
|
self.assertEqual(s['rounds'], 40)
|
||||||
|
self.assertGreaterEqual(s['avg_points'], 0)
|
||||||
|
self.assertLessEqual(s['hit_rate'], 1)
|
||||||
|
|
||||||
|
def test_heuristic_beats_random(self):
|
||||||
|
rng = Random(13)
|
||||||
|
players = [HeuristicPlayer(rng, n_samples=40)] \
|
||||||
|
+ [RandomPlayer(rng) for _ in range(3)]
|
||||||
|
stats = evaluate(players, 120, rng)
|
||||||
|
heuristic, randoms = stats[0], stats[1:]
|
||||||
|
best_random = max(s['avg_points'] for s in randoms)
|
||||||
|
self.assertGreater(heuristic['avg_points'], best_random)
|
||||||
|
self.assertGreater(heuristic['hit_rate'],
|
||||||
|
max(s['hit_rate'] for s in randoms))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main(verbosity=2)
|
||||||
Reference in New Issue
Block a user