""" Test suite for 6-Card Golf game rules. Verifies our implementation matches canonical 6-Card Golf rules: - Card values (A=1, 2=-2, 3-10=face, J/Q=10, K=0) - Column pairing (matching ranks in column = 0 points) - Draw/discard mechanics - Cannot re-discard card taken from discard pile - Round end conditions - Final turn logic Run with: pytest test_game.py -v """ import pytest from game import ( Card, Deck, Player, Game, GamePhase, GameOptions, Suit, Rank, RANK_VALUES ) # ============================================================================= # Card Value Tests # ============================================================================= class TestCardValues: """Verify card values match standard 6-Card Golf rules.""" def test_ace_worth_1(self): assert RANK_VALUES[Rank.ACE] == 1 def test_two_worth_negative_2(self): assert RANK_VALUES[Rank.TWO] == -2 def test_three_through_ten_face_value(self): assert RANK_VALUES[Rank.THREE] == 3 assert RANK_VALUES[Rank.FOUR] == 4 assert RANK_VALUES[Rank.FIVE] == 5 assert RANK_VALUES[Rank.SIX] == 6 assert RANK_VALUES[Rank.SEVEN] == 7 assert RANK_VALUES[Rank.EIGHT] == 8 assert RANK_VALUES[Rank.NINE] == 9 assert RANK_VALUES[Rank.TEN] == 10 def test_jack_worth_10(self): assert RANK_VALUES[Rank.JACK] == 10 def test_queen_worth_10(self): assert RANK_VALUES[Rank.QUEEN] == 10 def test_king_worth_0(self): assert RANK_VALUES[Rank.KING] == 0 def test_joker_worth_negative_2(self): assert RANK_VALUES[Rank.JOKER] == -2 def test_card_value_method(self): """Card.value() should return correct value.""" card = Card(Suit.HEARTS, Rank.KING) assert card.value() == 0 card = Card(Suit.SPADES, Rank.TWO) assert card.value() == -2 # ============================================================================= # Column Pairing Tests # ============================================================================= class TestColumnPairing: """Verify column pair scoring rules.""" def setup_method(self): """Create a player with controllable hand.""" self.player = Player(id="test", name="Test") def set_hand(self, ranks: list[Rank]): """Set player's hand to specific ranks (all hearts for simplicity).""" self.player.cards = [ Card(Suit.HEARTS, rank, face_up=True) for rank in ranks ] def test_matching_column_scores_zero(self): """Two cards of same rank in column = 0 points for that column.""" # Layout: [K, 5, 7] # [K, 3, 9] # Column 0 (K-K) = 0, Column 1 (5+3) = 8, Column 2 (7+9) = 16 self.set_hand([Rank.KING, Rank.FIVE, Rank.SEVEN, Rank.KING, Rank.THREE, Rank.NINE]) score = self.player.calculate_score() assert score == 24 # 0 + 8 + 16 def test_all_columns_matched(self): """All three columns matched = 0 total.""" self.set_hand([Rank.ACE, Rank.FIVE, Rank.KING, Rank.ACE, Rank.FIVE, Rank.KING]) score = self.player.calculate_score() assert score == 0 def test_no_columns_matched(self): """No matches = sum of all cards.""" # A(1) + 3 + 5 + 7 + 9 + K(0) = 25 self.set_hand([Rank.ACE, Rank.THREE, Rank.FIVE, Rank.SEVEN, Rank.NINE, Rank.KING]) score = self.player.calculate_score() assert score == 25 def test_twos_pair_still_zero(self): """Paired 2s score 0, not -4 (pair cancels, doesn't double).""" # [2, 5, 5] # [2, 5, 5] = all columns matched = 0 self.set_hand([Rank.TWO, Rank.FIVE, Rank.FIVE, Rank.TWO, Rank.FIVE, Rank.FIVE]) score = self.player.calculate_score() assert score == 0 def test_negative_cards_unpaired_keep_value(self): """Unpaired 2s and Jokers contribute their negative value.""" # [2, K, K] # [A, K, K] = -2 + 1 + 0 + 0 = -1 self.set_hand([Rank.TWO, Rank.KING, Rank.KING, Rank.ACE, Rank.KING, Rank.KING]) score = self.player.calculate_score() assert score == -1 # ============================================================================= # House Rules Scoring Tests # ============================================================================= class TestHouseRulesScoring: """Verify house rule scoring modifiers.""" def setup_method(self): self.player = Player(id="test", name="Test") def set_hand(self, ranks: list[Rank]): self.player.cards = [ Card(Suit.HEARTS, rank, face_up=True) for rank in ranks ] def test_super_kings_negative_2(self): """With super_kings, Kings worth -2.""" options = GameOptions(super_kings=True) self.set_hand([Rank.KING, Rank.ACE, Rank.ACE, Rank.THREE, Rank.ACE, Rank.ACE]) score = self.player.calculate_score(options) # K=-2, 3=3, columns 1&2 matched = 0 assert score == 1 def test_lucky_sevens_zero(self): """With lucky_sevens, 7s worth 0.""" options = GameOptions(lucky_sevens=True) self.set_hand([Rank.SEVEN, Rank.ACE, Rank.ACE, Rank.THREE, Rank.ACE, Rank.ACE]) score = self.player.calculate_score(options) # 7=0, 3=3, columns 1&2 matched = 0 assert score == 3 def test_ten_penny(self): """With ten_penny, 10s worth 1.""" options = GameOptions(ten_penny=True) self.set_hand([Rank.TEN, Rank.KING, Rank.KING, Rank.ACE, Rank.KING, Rank.KING]) score = self.player.calculate_score(options) # 10=1, A=1, columns 1&2 matched = 0 assert score == 2 def test_lucky_swing_joker(self): """With lucky_swing, single Joker worth -5.""" options = GameOptions(use_jokers=True, lucky_swing=True) self.player.cards = [ Card(Suit.HEARTS, Rank.JOKER, face_up=True), Card(Suit.HEARTS, Rank.KING, face_up=True), Card(Suit.HEARTS, Rank.KING, face_up=True), Card(Suit.HEARTS, Rank.ACE, face_up=True), Card(Suit.HEARTS, Rank.KING, face_up=True), Card(Suit.HEARTS, Rank.KING, face_up=True), ] score = self.player.calculate_score(options) # Joker=-5, A=1, columns 1&2 matched = 0 assert score == -4 def test_blackjack_21_becomes_0(self): """With blackjack option, score of exactly 21 becomes 0.""" # This is applied at round end, not in calculate_score directly # Testing the raw score first self.set_hand([Rank.JACK, Rank.ACE, Rank.THREE, Rank.FOUR, Rank.TWO, Rank.FIVE]) # J=10, A=1, 3=3, 4=4, 2=-2, 5=5 = 21 score = self.player.calculate_score() assert score == 21 # ============================================================================= # Draw and Discard Mechanics # ============================================================================= class TestDrawDiscardMechanics: """Verify draw/discard rules match standard Golf.""" def setup_method(self): self.game = Game() self.game.add_player(Player(id="p1", name="Player 1")) self.game.add_player(Player(id="p2", name="Player 2")) # Skip initial flip phase to test draw/discard mechanics directly self.game.start_game(options=GameOptions(initial_flips=0)) def test_can_draw_from_deck(self): """Player can draw from deck.""" card = self.game.draw_card("p1", "deck") assert card is not None assert self.game.drawn_card == card assert self.game.drawn_from_discard is False def test_can_draw_from_discard(self): """Player can draw from discard pile.""" discard_top = self.game.discard_top() card = self.game.draw_card("p1", "discard") assert card is not None assert card == discard_top assert self.game.drawn_card == card assert self.game.drawn_from_discard is True def test_can_discard_deck_draw(self): """Card drawn from deck CAN be discarded.""" self.game.draw_card("p1", "deck") assert self.game.can_discard_drawn() is True result = self.game.discard_drawn("p1") assert result is True def test_cannot_discard_discard_draw(self): """Card drawn from discard pile CANNOT be re-discarded.""" self.game.draw_card("p1", "discard") assert self.game.can_discard_drawn() is False result = self.game.discard_drawn("p1") assert result is False def test_must_swap_discard_draw(self): """When drawing from discard, must swap with a hand card.""" self.game.draw_card("p1", "discard") # Can't discard, must swap assert self.game.can_discard_drawn() is False # Swap works old_card = self.game.swap_card("p1", 0) assert old_card is not None assert self.game.drawn_card is None def test_swap_makes_card_face_up(self): """Swapped card is placed face up.""" player = self.game.get_player("p1") assert player.cards[0].face_up is False # Initially face down self.game.draw_card("p1", "deck") self.game.swap_card("p1", 0) assert player.cards[0].face_up is True def test_cannot_peek_before_swap(self): """Face-down cards stay hidden until swapped/revealed.""" player = self.game.get_player("p1") # Card is face down assert player.cards[0].face_up is False # to_dict doesn't reveal it card_dict = player.cards[0].to_dict(reveal=False) assert "rank" not in card_dict # ============================================================================= # Turn Flow Tests # ============================================================================= class TestTurnFlow: """Verify turn progression rules.""" def setup_method(self): self.game = Game() self.game.add_player(Player(id="p1", name="Player 1")) self.game.add_player(Player(id="p2", name="Player 2")) self.game.add_player(Player(id="p3", name="Player 3")) # Skip initial flip phase self.game.start_game(options=GameOptions(initial_flips=0)) def test_turn_advances_after_discard(self): """Turn advances to next player after discarding.""" assert self.game.current_player().id == "p1" self.game.draw_card("p1", "deck") self.game.discard_drawn("p1") assert self.game.current_player().id == "p2" def test_turn_advances_after_swap(self): """Turn advances to next player after swapping.""" assert self.game.current_player().id == "p1" self.game.draw_card("p1", "deck") self.game.swap_card("p1", 0) assert self.game.current_player().id == "p2" def test_turn_wraps_around(self): """Turn wraps from last player to first.""" # Complete turns for p1 and p2 self.game.draw_card("p1", "deck") self.game.discard_drawn("p1") self.game.draw_card("p2", "deck") self.game.discard_drawn("p2") assert self.game.current_player().id == "p3" self.game.draw_card("p3", "deck") self.game.discard_drawn("p3") assert self.game.current_player().id == "p1" # Wrapped def test_only_current_player_can_act(self): """Only current player can draw.""" assert self.game.current_player().id == "p1" card = self.game.draw_card("p2", "deck") # Wrong player assert card is None # ============================================================================= # Round End Tests # ============================================================================= class TestRoundEnd: """Verify round end conditions and final turn logic.""" def setup_method(self): self.game = Game() self.game.add_player(Player(id="p1", name="Player 1")) self.game.add_player(Player(id="p2", name="Player 2")) self.game.start_game(options=GameOptions(initial_flips=0)) def reveal_all_cards(self, player_id: str): """Helper to flip all cards for a player.""" player = self.game.get_player(player_id) for card in player.cards: card.face_up = True def test_revealing_all_triggers_final_turn(self): """When a player reveals all cards, final turn phase begins.""" # Reveal 5 cards for p1 player = self.game.get_player("p1") for i in range(5): player.cards[i].face_up = True assert self.game.phase == GamePhase.PLAYING # Draw and swap into last face-down position self.game.draw_card("p1", "deck") self.game.swap_card("p1", 5) # Last card assert self.game.phase == GamePhase.FINAL_TURN assert self.game.finisher_id == "p1" def test_other_players_get_final_turn(self): """After one player finishes, others each get one more turn.""" # P1 reveals all self.reveal_all_cards("p1") self.game.draw_card("p1", "deck") self.game.discard_drawn("p1") assert self.game.phase == GamePhase.FINAL_TURN assert self.game.current_player().id == "p2" # P2 takes final turn self.game.draw_card("p2", "deck") self.game.discard_drawn("p2") # Round should be over assert self.game.phase == GamePhase.ROUND_OVER def test_finisher_does_not_get_extra_turn(self): """The player who went out doesn't get another turn.""" # P1 reveals all and triggers final turn self.reveal_all_cards("p1") self.game.draw_card("p1", "deck") self.game.discard_drawn("p1") # P2's turn assert self.game.current_player().id == "p2" self.game.draw_card("p2", "deck") self.game.discard_drawn("p2") # Should be round over, not p1's turn again assert self.game.phase == GamePhase.ROUND_OVER def test_all_cards_revealed_at_round_end(self): """At round end, all cards are revealed.""" self.reveal_all_cards("p1") self.game.draw_card("p1", "deck") self.game.discard_drawn("p1") self.game.draw_card("p2", "deck") self.game.discard_drawn("p2") assert self.game.phase == GamePhase.ROUND_OVER # All cards should be face up now for player in self.game.players: assert all(card.face_up for card in player.cards) # ============================================================================= # Multi-Round Tests # ============================================================================= class TestMultiRound: """Verify multi-round game logic.""" def test_next_round_resets_hands(self): """Starting next round deals new hands.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(num_rounds=2, options=GameOptions(initial_flips=0)) # Force round end for player in game.players: for card in player.cards: card.face_up = True game._end_round() old_cards_p1 = [c.rank for c in game.players[0].cards] game.start_next_round() # Cards should be different (statistically) # and face down again assert game.phase in (GamePhase.PLAYING, GamePhase.INITIAL_FLIP) assert not all(game.players[0].cards[i].face_up for i in range(6)) def test_scores_accumulate_across_rounds(self): """Total scores persist across rounds.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(num_rounds=2, options=GameOptions(initial_flips=0)) # End round 1 for player in game.players: for card in player.cards: card.face_up = True game._end_round() round1_total = game.players[0].total_score game.start_next_round() # End round 2 for player in game.players: for card in player.cards: card.face_up = True game._end_round() # Total should have increased (or stayed same if score was 0) assert game.players[0].total_score >= round1_total or game.players[0].score < 0 # ============================================================================= # Initial Flip Tests # ============================================================================= class TestInitialFlip: """Verify initial flip phase mechanics.""" def test_initial_flip_two_cards(self): """With initial_flips=2, players must flip 2 cards.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(options=GameOptions(initial_flips=2)) assert game.phase == GamePhase.INITIAL_FLIP # Try to flip wrong number result = game.flip_initial_cards("p1", [0]) # Only 1 assert result is False # Flip correct number result = game.flip_initial_cards("p1", [0, 3]) assert result is True def test_initial_flip_zero_skips_phase(self): """With initial_flips=0, skip straight to playing.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(options=GameOptions(initial_flips=0)) assert game.phase == GamePhase.PLAYING def test_game_starts_after_all_flip(self): """Game starts when all players have flipped.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(options=GameOptions(initial_flips=2)) game.flip_initial_cards("p1", [0, 1]) assert game.phase == GamePhase.INITIAL_FLIP # Still waiting game.flip_initial_cards("p2", [2, 3]) assert game.phase == GamePhase.PLAYING # Now playing # ============================================================================= # Deck Management Tests # ============================================================================= class TestDeckManagement: """Verify deck initialization and reshuffling.""" def test_standard_deck_52_cards(self): """Standard deck has 52 cards.""" deck = Deck(num_decks=1, use_jokers=False) assert deck.cards_remaining() == 52 def test_joker_deck_54_cards(self): """Deck with jokers has 54 cards.""" deck = Deck(num_decks=1, use_jokers=True) assert deck.cards_remaining() == 54 def test_lucky_swing_single_joker(self): """Lucky swing adds only 1 joker total.""" deck = Deck(num_decks=1, use_jokers=True, lucky_swing=True) assert deck.cards_remaining() == 53 def test_multi_deck(self): """Multiple decks multiply cards.""" deck = Deck(num_decks=2, use_jokers=False) assert deck.cards_remaining() == 104 # ============================================================================= # Edge Cases # ============================================================================= class TestEdgeCases: """Test edge cases and boundary conditions.""" def test_cannot_draw_twice(self): """Cannot draw again before playing drawn card.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(options=GameOptions(initial_flips=0)) game.draw_card("p1", "deck") second_draw = game.draw_card("p1", "deck") assert second_draw is None def test_swap_position_bounds(self): """Swap position must be 0-5.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(options=GameOptions(initial_flips=0)) game.draw_card("p1", "deck") result = game.swap_card("p1", -1) assert result is None result = game.swap_card("p1", 6) assert result is None result = game.swap_card("p1", 3) # Valid assert result is not None def test_empty_discard_pile(self): """Cannot draw from empty discard pile.""" game = Game() game.add_player(Player(id="p1", name="Player 1")) game.add_player(Player(id="p2", name="Player 2")) game.start_game(options=GameOptions(initial_flips=0)) # Clear discard pile (normally has 1 card) game.discard_pile = [] card = game.draw_card("p1", "discard") assert card is None if __name__ == "__main__": pytest.main([__file__, "-v"])