golfgame/server/test_maya_bug.py
Aaron D. Lee 39b78a2ba6 Add final results modal, active rules display, and UI improvements
- Add big final results modal at game end with rankings and share button
- Add active rules bar showing enabled variants during gameplay
- Increase spacing between player cards and opponents row
- Add Wolfpack bonus rule (2 pairs of Jacks = -5 pts)
- Change joker options to radio buttons (None/Standard/Lucky Swing/Eagle-Eye)
- Update Eagle-Eye jokers: +2 pts unpaired, -4 pts paired
- Add card flip animation on discard pile
- Redesign waiting room layout with side-by-side columns
- Style card backs with red Bee-style diamond crosshatch pattern
- Compact standings panel to show top 4 per category
- Various CSS polish and responsive improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 23:53:07 -05:00

320 lines
12 KiB
Python

"""
Test for the original Maya bug:
Maya took a 10 from discard and had to discard an Ace.
Bug chain:
1. should_take_discard() incorrectly decided to take the 10
2. choose_swap_or_discard() correctly returned None (don't swap)
3. But drawing from discard FORCES a swap
4. The forced-swap fallback found the "worst" visible card
5. The Ace (value 1) was swapped out for the 10
This test verifies the fixes work.
"""
import pytest
from game import Card, Player, Game, GameOptions, Suit, Rank
from ai import (
GolfAI, CPUProfile, CPU_PROFILES,
get_ai_card_value, has_worse_visible_card
)
def get_maya_profile() -> CPUProfile:
"""Get Maya's profile."""
for p in CPU_PROFILES:
if p.name == "Maya":
return p
# Fallback - create Maya-like profile
return CPUProfile(
name="Maya",
style="Aggressive Closer",
swap_threshold=6,
pair_hope=0.4,
aggression=0.85,
unpredictability=0.1,
)
def create_test_game() -> Game:
"""Create a game in playing state."""
game = Game()
game.add_player(Player(id="maya", name="Maya"))
game.add_player(Player(id="other", name="Other"))
game.start_game(options=GameOptions(initial_flips=0))
return game
class TestMayaBugFix:
"""Test that the original Maya bug is fixed."""
def test_maya_does_not_take_10_with_good_hand(self):
"""
Original bug: Maya took a 10 from discard when she had good cards.
Setup: Maya has visible Ace, King, 2 (all good cards)
Discard: 10
Expected: Maya should NOT take the 10
"""
game = create_test_game()
maya = game.get_player("maya")
profile = get_maya_profile()
# Set up Maya's hand with good visible cards
maya.cards = [
Card(Suit.HEARTS, Rank.ACE, face_up=True), # Value 1
Card(Suit.HEARTS, Rank.KING, face_up=True), # Value 0
Card(Suit.HEARTS, Rank.TWO, face_up=True), # Value -2
Card(Suit.SPADES, Rank.FIVE, face_up=False),
Card(Suit.SPADES, Rank.SIX, face_up=False),
Card(Suit.SPADES, Rank.SEVEN, face_up=False),
]
# Put a 10 on discard
discard_10 = Card(Suit.CLUBS, Rank.TEN, face_up=True)
game.discard_pile = [discard_10]
# Maya should NOT take the 10
should_take = GolfAI.should_take_discard(discard_10, maya, profile, game)
assert should_take is False, (
"Maya should not take a 10 when her visible cards are Ace, King, 2"
)
def test_maya_does_not_take_10_even_with_unpredictability(self):
"""
The unpredictability trait should NOT cause taking bad cards.
Run multiple times to account for randomness.
"""
game = create_test_game()
maya = game.get_player("maya")
profile = get_maya_profile()
maya.cards = [
Card(Suit.HEARTS, Rank.ACE, face_up=True),
Card(Suit.HEARTS, Rank.KING, face_up=True),
Card(Suit.HEARTS, Rank.TWO, face_up=True),
Card(Suit.SPADES, Rank.FIVE, face_up=False),
Card(Suit.SPADES, Rank.SIX, face_up=False),
Card(Suit.SPADES, Rank.SEVEN, face_up=False),
]
discard_10 = Card(Suit.CLUBS, Rank.TEN, face_up=True)
game.discard_pile = [discard_10]
# Run 100 times - should NEVER take the 10
took_10_count = 0
for _ in range(100):
if GolfAI.should_take_discard(discard_10, maya, profile, game):
took_10_count += 1
assert took_10_count == 0, (
f"Maya took a 10 {took_10_count}/100 times despite having good cards. "
"Unpredictability should not override basic logic for bad cards."
)
def test_has_worse_visible_card_utility(self):
"""Test the utility function that guards against taking bad cards."""
game = create_test_game()
maya = game.get_player("maya")
options = game.options
# Hand with good visible cards (Ace=1, King=0, 2=-2)
maya.cards = [
Card(Suit.HEARTS, Rank.ACE, face_up=True), # 1
Card(Suit.HEARTS, Rank.KING, face_up=True), # 0
Card(Suit.HEARTS, Rank.TWO, face_up=True), # -2
Card(Suit.SPADES, Rank.FIVE, face_up=False),
Card(Suit.SPADES, Rank.SIX, face_up=False),
Card(Suit.SPADES, Rank.SEVEN, face_up=False),
]
# No visible card is worse than 10 (value 10)
assert has_worse_visible_card(maya, 10, options) is False
# No visible card is worse than 5
assert has_worse_visible_card(maya, 5, options) is False
# Ace (1) is worse than 0
assert has_worse_visible_card(maya, 0, options) is True
def test_forced_swap_uses_house_rules(self):
"""
When forced to swap (drew from discard), the AI should use
get_ai_card_value() to find the worst card, not raw value().
This matters for house rules like super_kings, ten_penny, etc.
"""
game = create_test_game()
game.options = GameOptions(super_kings=True) # Kings now worth -2
maya = game.get_player("maya")
# All face up - forced swap scenario
maya.cards = [
Card(Suit.HEARTS, Rank.KING, face_up=True), # -2 with super_kings
Card(Suit.HEARTS, Rank.ACE, face_up=True), # 1
Card(Suit.HEARTS, Rank.THREE, face_up=True), # 3 - worst!
Card(Suit.SPADES, Rank.KING, face_up=True), # -2 with super_kings
Card(Suit.SPADES, Rank.TWO, face_up=True), # -2
Card(Suit.SPADES, Rank.ACE, face_up=True), # 1
]
# Find worst card using house rules
worst_pos = 0
worst_val = -999
for i, c in enumerate(maya.cards):
card_val = get_ai_card_value(c, game.options)
if card_val > worst_val:
worst_val = card_val
worst_pos = i
# Position 2 (Three, value 3) should be worst
assert worst_pos == 2, (
f"With super_kings, the Three (value 3) should be worst, "
f"not position {worst_pos} (value {worst_val})"
)
def test_choose_swap_does_not_discard_excellent_cards(self):
"""
Unpredictability should NOT cause discarding excellent cards (2s, Jokers).
"""
game = create_test_game()
maya = game.get_player("maya")
profile = get_maya_profile()
maya.cards = [
Card(Suit.HEARTS, Rank.FIVE, face_up=True),
Card(Suit.HEARTS, Rank.SIX, face_up=True),
Card(Suit.HEARTS, Rank.SEVEN, face_up=False),
Card(Suit.SPADES, Rank.EIGHT, face_up=False),
Card(Suit.SPADES, Rank.NINE, face_up=False),
Card(Suit.SPADES, Rank.TEN, face_up=False),
]
# Drew a 2 (excellent card, value -2)
drawn_two = Card(Suit.CLUBS, Rank.TWO)
# Run 100 times - should ALWAYS swap (never discard a 2)
discarded_count = 0
for _ in range(100):
swap_pos = GolfAI.choose_swap_or_discard(drawn_two, maya, profile, game)
if swap_pos is None:
discarded_count += 1
assert discarded_count == 0, (
f"Maya discarded a 2 (excellent card) {discarded_count}/100 times. "
"Unpredictability should not cause discarding excellent cards."
)
def test_full_scenario_maya_10_ace(self):
"""
Full reproduction of the original bug scenario.
Maya has: [A, K, 2, ?, ?, ?] (good visible cards)
Discard: 10
Expected behavior:
1. Maya should NOT take the 10
2. If she somehow did, she should swap into face-down, not replace the Ace
"""
game = create_test_game()
maya = game.get_player("maya")
profile = get_maya_profile()
# Setup exactly like the bug report
maya.cards = [
Card(Suit.HEARTS, Rank.ACE, face_up=True), # Good - don't replace!
Card(Suit.HEARTS, Rank.KING, face_up=True), # Good
Card(Suit.HEARTS, Rank.TWO, face_up=True), # Excellent
Card(Suit.SPADES, Rank.JACK, face_up=False), # Unknown
Card(Suit.SPADES, Rank.QUEEN, face_up=False),# Unknown
Card(Suit.SPADES, Rank.TEN, face_up=False), # Unknown
]
discard_10 = Card(Suit.CLUBS, Rank.TEN, face_up=True)
game.discard_pile = [discard_10]
# Step 1: Maya should not take the 10
should_take = GolfAI.should_take_discard(discard_10, maya, profile, game)
assert should_take is False, "Maya should not take a 10 with this hand"
# Step 2: Even if she did take it (simulating old bug), verify swap logic
# The swap logic should prefer face-down positions
drawn_10 = Card(Suit.CLUBS, Rank.TEN)
swap_pos = GolfAI.choose_swap_or_discard(drawn_10, maya, profile, game)
# Should either discard (None) or swap into face-down (positions 3, 4, 5)
# Should NEVER swap into position 0 (Ace), 1 (King), or 2 (Two)
if swap_pos is not None:
assert swap_pos >= 3, (
f"Maya tried to swap 10 into position {swap_pos}, replacing a good card. "
"Should only swap into face-down positions (3, 4, 5)."
)
class TestEdgeCases:
"""Test edge cases related to the bug."""
def test_all_face_up_forced_swap_finds_actual_worst(self):
"""
When all cards are face up and forced to swap, find the ACTUAL worst card.
"""
game = create_test_game()
maya = game.get_player("maya")
# All face up, varying values
maya.cards = [
Card(Suit.HEARTS, Rank.ACE, face_up=True), # 1
Card(Suit.HEARTS, Rank.KING, face_up=True), # 0
Card(Suit.HEARTS, Rank.TWO, face_up=True), # -2
Card(Suit.SPADES, Rank.JACK, face_up=True), # 10 - WORST
Card(Suit.SPADES, Rank.THREE, face_up=True), # 3
Card(Suit.SPADES, Rank.FOUR, face_up=True), # 4
]
# Find worst
worst_pos = 0
worst_val = -999
for i, c in enumerate(maya.cards):
card_val = get_ai_card_value(c, game.options)
if card_val > worst_val:
worst_val = card_val
worst_pos = i
assert worst_pos == 3, f"Jack (position 3, value 10) should be worst, got position {worst_pos}"
assert worst_val == 10, f"Worst value should be 10, got {worst_val}"
def test_take_discard_respects_pair_potential(self):
"""
Taking a bad card to complete a pair IS valid strategy.
This should still work after the bug fix.
"""
game = create_test_game()
maya = game.get_player("maya")
profile = get_maya_profile()
# Maya has a visible 10 - taking another 10 to pair is GOOD
maya.cards = [
Card(Suit.HEARTS, Rank.TEN, face_up=True), # Visible 10
Card(Suit.HEARTS, Rank.KING, face_up=True),
Card(Suit.HEARTS, Rank.ACE, face_up=True),
Card(Suit.SPADES, Rank.FIVE, face_up=False), # Pair position for the 10
Card(Suit.SPADES, Rank.SIX, face_up=False),
Card(Suit.SPADES, Rank.SEVEN, face_up=False),
]
# 10 on discard - should take to pair!
discard_10 = Card(Suit.CLUBS, Rank.TEN, face_up=True)
game.discard_pile = [discard_10]
should_take = GolfAI.should_take_discard(discard_10, maya, profile, game)
assert should_take is True, (
"Maya SHOULD take a 10 when she has a visible 10 to pair with"
)
if __name__ == "__main__":
pytest.main([__file__, "-v"])