Files
Wizard/wizard_server/game_logic/tests/test_game.py
T
marvinandClaude Opus 5.5 fde8e2fb2e Always forbid bids that add up to the cards in hand
The sum of all bids may never equal the number of cards each player
holds in the round; the last bidder has to adjust their bid. This was an
optional "not equal" lobby setting and is now a fixed rule of the game
engine, so it applies to humans, AI players and training alike.

- Remove GameOptions.not_equal, the set_options field, the lobby toggle
  and the --not-equal training flag
- Explain the rule in the lobby and show the blocked bid while bidding
- Drop the option from the AI observation encoding
- Tests check that no round ends with bids summing to the cards in hand

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 10:15:25 +02:00

158 lines
5.3 KiB
Python

import json
import random
import pytest
from wizard_game import (
Bid,
ChooseTrump,
GameFinished,
GameOptions,
IllegalActionError,
Observation,
Phase,
PlayCard,
RoundCompleted,
Suit,
WizardGame,
action_from_dict,
card_by_id,
)
from wizard_game.cards import WIZARD_ID_OFFSET
def play_random_game(num_players: int, seed: int, options: GameOptions | None = None) -> WizardGame:
game = WizardGame(num_players, options, seed=seed)
rng = random.Random(seed)
rounds_completed = 0
while not game.is_finished:
player = game.current_player
actions = game.legal_actions()
assert actions, "current player must always have a legal action"
events = game.apply(player, rng.choice(actions))
rounds_completed += sum(isinstance(e, RoundCompleted) for e in events)
assert rounds_completed == game.total_rounds
return game
@pytest.mark.parametrize("num_players", [3, 4, 5, 6])
@pytest.mark.parametrize("seed", range(5))
def test_random_games_complete(num_players, seed):
options = GameOptions(hidden_bids=seed % 2 == 0)
game = play_random_game(num_players, seed, options)
assert game.phase is Phase.FINISHED
assert len(game.round_history) == game.total_rounds
assert game.scores == list(game.round_history[-1].scores)
for result in game.round_history:
assert sum(result.tricks) == result.round_number
assert sum(result.bids) != result.round_number
def test_invalid_player_count():
with pytest.raises(ValueError):
WizardGame(2)
with pytest.raises(ValueError):
WizardGame(7)
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
def test_wrong_player_rejected():
game = WizardGame(3, seed=1, first_dealer=0)
other = (game.current_player + 1) % 3
with pytest.raises(IllegalActionError):
game.apply(other, game.legal_actions()[0])
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
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
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
assert game.observation(2).has_bid[1] is True
game.apply(2, Bid(0))
game.apply(0, Bid(1)) # 0 is not allowed: the bids would add up to the 1 card in hand
assert game.observation(2).bids == (1, 1, 0)
def test_last_round_has_no_trump():
game = play_random_game(3, seed=7)
assert game.round.round_number == 20
assert game.round.trump_card is None
def test_observation_hides_other_hands_and_round_trips():
game = WizardGame(5, seed=9)
obs = game.observation(0)
assert obs.hand == tuple(sorted(game.hand(0), key=lambda c: c.id))
data = json.loads(json.dumps(obs.to_dict()))
assert Observation.from_dict(data) == obs
def test_action_round_trip():
for action in (Bid(3), PlayCard(card_by_id(WIZARD_ID_OFFSET)), ChooseTrump(Suit.BLUE)):
assert action_from_dict(action.to_dict()) == action
with pytest.raises(ValueError):
action_from_dict({"type": "bid", "value": True})
with pytest.raises(ValueError):
action_from_dict({"type": "play", "card": 99})
def test_game_finished_event_and_winners():
game = WizardGame(3, seed=2)
rng = random.Random(2)
last_events = []
while not game.is_finished:
last_events = game.apply(game.current_player, rng.choice(game.legal_actions()))
finished = [e for e in last_events if isinstance(e, GameFinished)]
assert finished and list(finished[0].winners) == game.winners()
assert game.observation(0).winners == tuple(game.winners())
assert game.legal_actions() == []
def test_clone_is_independent():
game = WizardGame(4, seed=4)
clone = game.clone()
clone.apply(clone.current_player, clone.legal_actions()[0])
assert game.observation(0) != clone.observation(0) or game.current_player != clone.current_player