Trump card: draw from the stack, no trump for wizards and jesters

- Deal the hands from the shuffled stack and draw the trump card from it,
  so the trump card is in nobody's hand and cannot be played
- A wizard or jester as trump card means no trump suit (previously a
  wizard let the dealer choose); remove the trump selection phase, the
  ChooseTrump action, the trump picker UI and the AI trump actions
- Completed tricks carry their round's trump card, so the table keeps
  showing the matching trump card while the last trick of a round is
  displayed
- Card corners are no longer rotated, so 6 and 9 cannot be confused
- Trump label explains why there is no trump (wizard, jester, last round)
- Tests: trump card never dealt, special cards give no trump, tricks keep
  their trump card

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:28:53 +02:00
co-authored by Claude Opus 5.5
parent fde8e2fb2e
commit 21a2b044d9
16 changed files with 135 additions and 190 deletions
+61 -22
View File
@@ -4,8 +4,8 @@ import random
import pytest
from wizard_game import (
DECK_SIZE,
Bid,
ChooseTrump,
GameFinished,
GameOptions,
IllegalActionError,
@@ -13,7 +13,8 @@ from wizard_game import (
Phase,
PlayCard,
RoundCompleted,
Suit,
TrickCompleted,
TrickResult,
WizardGame,
action_from_dict,
card_by_id,
@@ -59,8 +60,8 @@ def test_deal_sizes_and_starter():
game = WizardGame(4, seed=3, first_dealer=2)
assert game.round_number == 1
assert all(len(game.hand(p)) == 1 for p in range(4))
if game.phase is Phase.BIDDING:
assert game.current_player == 3
assert game.phase is Phase.BIDDING
assert game.current_player == 3
def test_wrong_player_rejected():
@@ -72,31 +73,69 @@ def test_wrong_player_rejected():
def test_illegal_bid_rejected():
game = WizardGame(3, seed=1, first_dealer=0)
if game.phase is Phase.TRUMP_SELECTION:
game.apply(game.current_player, ChooseTrump(Suit.RED))
with pytest.raises(IllegalActionError):
game.apply(game.current_player, Bid(5))
def test_wizard_trump_card_lets_dealer_choose():
for seed in range(200):
game = WizardGame(4, seed=seed, first_dealer=1)
if game.round.trump_card and game.round.trump_card.is_wizard:
break
else:
pytest.fail("no seed produced a wizard as trump card")
assert game.phase is Phase.TRUMP_SELECTION
assert game.current_player == 1
game.apply(1, ChooseTrump(Suit.GREEN))
assert game.round.trump_suit is Suit.GREEN
@pytest.mark.parametrize("num_players", [3, 4, 5, 6])
def test_trump_card_is_taken_from_the_stack(num_players):
"""The trump card is dealt to nobody and every card exists only once."""
game = WizardGame(num_players, seed=num_players)
rng = random.Random(num_players)
rounds_seen = 0
while not game.is_finished:
if game.phase is Phase.BIDDING and all(bid is None for bid in game.round.bids):
rounds_seen += 1
hands = [card for player in range(num_players) for card in game.hand(player)]
assert len(hands) == len(set(hands)) == game.round_number * num_players
trump = game.round.trump_card
if game.round_number * num_players < DECK_SIZE:
assert trump is not None and trump not in hands
else:
assert trump is None # last round: every card was dealt
game.apply(game.current_player, rng.choice(game.legal_actions()))
assert rounds_seen == game.total_rounds
def find_game(predicate, num_players=4):
for seed in range(500):
game = WizardGame(num_players, seed=seed, first_dealer=1)
if predicate(game.round.trump_card):
return game
pytest.fail("no seed produced the wanted trump card")
def test_number_trump_card_defines_trump_suit():
game = find_game(lambda card: card is not None and card.is_number)
assert game.round.trump_suit is game.round.trump_card.suit
assert game.observation(0).trump_suit is game.round.trump_card.suit
def test_completed_trick_keeps_its_rounds_trump_card():
game = WizardGame(3, seed=4)
rng = random.Random(4)
for _ in range(200):
trump = game.round.trump_card
events = game.apply(game.current_player, rng.choice(game.legal_actions()))
tricks = [e.trick for e in events if isinstance(e, TrickCompleted)]
for trick in tricks:
assert trick.trump_card == trump
assert TrickResult.from_dict(trick.to_dict()) == trick
@pytest.mark.parametrize("kind", ["wizard", "jester"])
def test_special_trump_card_means_no_trump(kind):
game = find_game(lambda card: card is not None and card.kind.value == kind)
obs = game.observation(0)
assert obs.trump_card == game.round.trump_card
assert obs.trump_suit is None
# Bidding starts right away with the player left of the dealer.
assert game.phase is Phase.BIDDING
assert game.current_player == 2
def test_sum_of_bids_never_equals_cards_in_hand():
game = WizardGame(3, seed=11, first_dealer=0)
if game.phase is Phase.TRUMP_SELECTION:
game.apply(game.current_player, ChooseTrump(Suit.RED))
game.apply(1, Bid(0))
game.apply(2, Bid(0))
assert game.legal_actions(0) == [Bid(0)] # bidding 1 would make the sum equal 1
@@ -104,8 +143,6 @@ def test_sum_of_bids_never_equals_cards_in_hand():
def test_hidden_bids():
game = WizardGame(3, GameOptions(hidden_bids=True), seed=5, first_dealer=0)
if game.phase is Phase.TRUMP_SELECTION:
game.apply(game.current_player, ChooseTrump(Suit.RED))
game.apply(1, Bid(1))
assert game.observation(1).bids[1] == 1
assert game.observation(2).bids[1] is None
@@ -130,8 +167,10 @@ def test_observation_hides_other_hands_and_round_trips():
def test_action_round_trip():
for action in (Bid(3), PlayCard(card_by_id(WIZARD_ID_OFFSET)), ChooseTrump(Suit.BLUE)):
for action in (Bid(3), PlayCard(card_by_id(WIZARD_ID_OFFSET))):
assert action_from_dict(action.to_dict()) == action
with pytest.raises(ValueError):
action_from_dict({"type": "trump", "suit": "red"})
with pytest.raises(ValueError):
action_from_dict({"type": "bid", "value": True})
with pytest.raises(ValueError):