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)