import copy import random import unittest from bridzik import cards, Card, Card_colors, Card_values, BridzikException, Round from rl.encoding import ( COLORS, VALUES, N_CARDS, N_GUESS_ACTIONS, N_PLAY_ACTIONS, OFF_HAND, OFF_SEEN, OFF_ROUND, OFF_PHASE, OFF_GUESSES, OFF_TRICKS, OFF_STASH, OFF_STASH_LEADER, OFF_VOIDS, OBS_DIM, card_index, deduce_voids, index_card, relative_seat, encode_observation, guess_mask, play_mask, ) class CardIndexCase(unittest.TestCase): def test_roundtrip_and_uniqueness(self): indexes = set() for card in cards: idx = card_index(card) self.assertIn(idx, range(N_CARDS)) self.assertEqual(index_card(idx), card) indexes.add(idx) self.assertEqual(len(indexes), N_CARDS) def test_layout(self): # farba = blok po 8, hodnota = pozicia v bloku self.assertEqual(card_index(Card(Card_colors['HEARTS'], Card_values['C7'])), 0) self.assertEqual(card_index(Card(Card_colors['HEARTS'], Card_values['ACE'])), 7) self.assertEqual(card_index(Card(COLORS[3], Card_values['ACE'])), 31) class RotationCase(unittest.TestCase): def test_relative_seat(self): for player in range(4): self.assertEqual(relative_seat(player, player), 0) # smer hry = rastuce cislo sedadla mod 4 self.assertEqual(relative_seat((player + 1) % 4, player), 1) self.assertEqual(relative_seat((player + 3) % 4, player), 3) def test_guesses_rotated_for_all_seats(self): r = Round(0, 2) guesses = {2: 5, 3: 0, 0: 1, 1: 1} for seat in [2, 3, 0, 1]: r.add_player_guess(seat, guesses[seat]) for player in range(4): obs = encode_observation(r, player) for seat in range(4): rel = relative_seat(seat, player) self.assertEqual(obs[OFF_GUESSES + 2 * rel], 1.0) self.assertEqual(obs[OFF_GUESSES + 2 * rel + 1], guesses[seat] / 8) def test_partial_guesses_flags(self): r = Round(3, 1) r.add_player_guess(1, 2) for player in range(4): obs = encode_observation(r, player) rel = relative_seat(1, player) self.assertEqual(obs[OFF_GUESSES + 2 * rel], 1.0) self.assertEqual(obs[OFF_GUESSES + 2 * rel + 1], 2 / 8) for seat in [0, 2, 3]: rel = relative_seat(seat, player) self.assertEqual(obs[OFF_GUESSES + 2 * rel], 0.0) self.assertEqual(obs[OFF_GUESSES + 2 * rel + 1], 0.0) class ObservationCase(unittest.TestCase): @staticmethod def _deterministic_round(): # rovnaka konstrukcia ako v test_engine.RoundCase.test_play_card shuffler = lambda l: None c0 = [Card(Card_colors['BELLS'], Card_values['UPPER']), Card(Card_colors['HEARTS'], Card_values['UPPER'])] c1 = [Card(Card_colors['BELLS'], Card_values['C7']), Card(Card_colors['HEARTS'], Card_values['C10'])] c2 = [Card(Card_colors['BELLS'], Card_values['ACE']), Card(Card_colors['BELLS'], Card_values['C8'])] c3 = [Card(Card_colors['LEAVES'], Card_values['C7']), Card(Card_colors['BELLS'], Card_values['LOWER'])] c = ['dummy'] * 24 + c0 + c1 + c2 + c3 r = Round(6, 1, c, shuffler) return r, [c0, c1, c2, c3] def test_hand_multi_hot(self): r, hands = self._deterministic_round() for player in range(4): obs = encode_observation(r, player) hand_indexes = {card_index(c) for c in hands[player]} for i in range(N_CARDS): self.assertEqual(obs[OFF_HAND + i], 1.0 if i in hand_indexes else 0.0) def test_round_number_and_phase(self): r, _ = self._deterministic_round() obs = encode_observation(r, 0) for i in range(8): self.assertEqual(obs[OFF_ROUND + i], 1.0 if i == 6 else 0.0) self.assertEqual(obs[OFF_PHASE], 1.0) # tipovacia faza for seat, guess in [(1, 0), (2, 0), (3, 1), (0, 2)]: r.add_player_guess(seat, guess) obs = encode_observation(r, 0) self.assertEqual(obs[OFF_PHASE], 0.0) # hracia faza def test_current_stash_slots_and_seen(self): r, hands = self._deterministic_round() for seat, guess in [(1, 0), (2, 0), (3, 1), (0, 2)]: r.add_player_guess(seat, guess) # rozohrana kopka: hraju 0 a 1 r.play_card(0, hands[0][0]) r.play_card(1, hands[1][0]) for player in range(4): obs = encode_observation(r, player) slot0 = relative_seat(0, player) slot1 = relative_seat(1, player) self.assertEqual(obs[OFF_STASH + slot0 * N_CARDS + card_index(hands[0][0])], 1.0) self.assertEqual(obs[OFF_STASH + slot1 * N_CARDS + card_index(hands[1][0])], 1.0) self.assertEqual(sum(obs[OFF_STASH:OFF_STASH + 4 * N_CARDS]), 2.0) # leader kopky je hrac 0 (najvyssi tip) self.assertEqual(obs[OFF_STASH_LEADER + relative_seat(0, player)], 1.0) # nic este nie je "videne" -- prva kopka nie je dokoncena self.assertEqual(sum(obs[OFF_SEEN:OFF_SEEN + N_CARDS]), 0.0) # dokoncena kopka -> karty sa presunu do SEEN, sloty sa vyprazdnia r.play_card(2, hands[2][0]) r.play_card(3, hands[3][1]) obs = encode_observation(r, 0) played = [hands[0][0], hands[1][0], hands[2][0], hands[3][1]] for card in played: self.assertEqual(obs[OFF_SEEN + card_index(card)], 1.0) self.assertEqual(sum(obs[OFF_SEEN:OFF_SEEN + N_CARDS]), 4.0) self.assertEqual(sum(obs[OFF_STASH:OFF_STASH + 4 * N_CARDS]), 0.0) # novu kopku vynasa vitaz (hrac 2, BELLS ACE) self.assertEqual(obs[OFF_STASH_LEADER + relative_seat(2, 0)], 1.0) # pocty vyhranych kopiek rotovane for player in range(4): obs = encode_observation(r, player) self.assertEqual(obs[OFF_TRICKS + relative_seat(2, player)], 1 / 8) def test_terminal_state_encodable(self): r = Round(7, 0) for seat, guess in [(0, 0), (1, 0), (2, 0), (3, 0)]: try: r.add_player_guess(seat, guess) except BridzikException: r.add_player_guess(seat, 1) while not r.is_completed(): player = r.get_active_player() mask = play_mask(r, player) r.play_card(player, index_card(mask.index(True))) obs = encode_observation(r, 0) self.assertEqual(len(obs), OBS_DIM) self.assertEqual(sum(obs[OFF_SEEN:OFF_SEEN + N_CARDS]), 4.0) class VoidsInObservationCase(unittest.TestCase): def test_voids_encoded_and_rotated(self): # hrac 0 vynasa zelen; 2 tromfne (void zelen), 3 hodi gulu (void # zelen aj cerven) -- viz deduce_voids 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) r.add_player_guess(1, 0) r.add_player_guess(2, 0) r.add_player_guess(3, 1) obs = encode_observation(r, 0) self.assertEqual(sum(obs[OFF_VOIDS:OFF_VOIDS + 16]), 0.0) for seat, card in [(0, hand0[0]), (1, hand1[0]), (2, hand2[0]), (3, hand3[0])]: r.play_card(seat, card) leaves_i = COLORS.index(Card_colors['LEAVES']) hearts_i = COLORS.index(Card_colors['HEARTS']) for player in range(4): obs = encode_observation(r, player) block = lambda seat: obs[OFF_VOIDS + relative_seat(seat, player) * 4: OFF_VOIDS + relative_seat(seat, player) * 4 + 4] self.assertEqual(sum(block(0)), 0.0) # vynasajuci neprezradza nic self.assertEqual(sum(block(1)), 0.0) # priznal farbu self.assertEqual(block(2)[leaves_i], 1.0) self.assertEqual(sum(block(2)), 1.0) self.assertEqual(block(3)[leaves_i], 1.0) self.assertEqual(block(3)[hearts_i], 1.0) self.assertEqual(sum(block(3)), 2.0) class GuessMaskCase(unittest.TestCase): def test_range_by_round_number(self): for round_number in range(8): r = Round(round_number, 0) mask = guess_mask(r) for g in range(N_GUESS_ACTIONS): self.assertEqual(mask[g], g <= 8 - round_number) def test_last_guesser_forbidden_value(self): r = Round(0, 0) r.add_player_guess(0, 2) r.add_player_guess(1, 1) r.add_player_guess(2, 3) mask = guess_mask(r) self.assertFalse(mask[2]) # 2+1+3+2 == 8 kopiek -> zakazane for g in [0, 1, 3, 4, 5, 6, 7, 8]: self.assertTrue(mask[g]) def test_forbidden_value_out_of_range(self): # sucet tipov > pocet kopiek -> zakazana hodnota by bola zaporna, # ziadne dodatocne maskovanie r = Round(0, 0) r.add_player_guess(0, 8) r.add_player_guess(1, 5) r.add_player_guess(2, 0) mask = guess_mask(r) self.assertEqual(mask, [True] * 9) class MaskEngineConsistencyCase(unittest.TestCase): """Fuzz: maska presne zrkadli engine -- povolena akcia NIKDY nezlyha, zakazana akcia VZDY vyhodi BridzikException.""" def _check_guess_mask(self, rnd, player, mask): for g in range(N_GUESS_ACTIONS): if mask[g]: copy.deepcopy(rnd).add_player_guess(player, g) else: with self.assertRaises(BridzikException): rnd.add_player_guess(player, g) def _check_play_mask(self, rnd, player, mask): self.assertIn(True, mask) # aktivny hrac ma vzdy legalny tah for i in range(N_PLAY_ACTIONS): if mask[i]: copy.deepcopy(rnd).play_card(player, index_card(i)) else: with self.assertRaises(BridzikException): rnd.play_card(player, index_card(i)) def _check_observation(self, rnd, player): obs = encode_observation(rnd, player) self.assertEqual(len(obs), OBS_DIM) for v in obs: self.assertGreaterEqual(v, 0.0) self.assertLessEqual(v, 1.0) hand = {card_index(c) for c in rnd.player_cards[player]} for i in range(N_CARDS): self.assertEqual(obs[OFF_HAND + i], 1.0 if i in hand else 0.0) if i in hand: # ruka a videne karty su disjunktne self.assertEqual(obs[OFF_SEEN + i], 0.0) # zakodovany void nikdy neprotireci realnej ruke hraca for seat in range(4): rel = relative_seat(seat, player) held = {c.color for c in rnd.player_cards[seat]} for ci, color in enumerate(COLORS): if obs[OFF_VOIDS + rel * 4 + ci] == 1.0: self.assertNotIn(color, held) def _fuzz_round(self, rng, round_number, first_player): rnd = Round(round_number, first_player) for _ in range(4): player = rnd.get_active_player() self._check_observation(rnd, player) mask = guess_mask(rnd) self._check_guess_mask(rnd, player, mask) rnd.add_player_guess( player, rng.choice([g for g in range(N_GUESS_ACTIONS) if mask[g]]) ) while not rnd.is_completed(): player = rnd.get_active_player() self._check_observation(rnd, player) mask = play_mask(rnd, player) self._check_play_mask(rnd, player, mask) rnd.play_card( player, index_card(rng.choice([i for i in range(N_PLAY_ACTIONS) if mask[i]])) ) # kolo dohrane do konca cisto cez masky -> bodovanie funguje self.assertEqual(len(rnd.get_points_summary()), 4) def test_fuzz_all_round_numbers_and_seats(self): rng = random.Random(1337) for round_number in range(8): for first_player in range(4): for _ in range(3): self._fuzz_round(rng, round_number, first_player) if __name__ == '__main__': unittest.main(verbosity=2)