first prototype

This commit is contained in:
2026-09-27 09:52:00 +02:00
parent 4635199a41
commit 6d011d474f
109 changed files with 6354 additions and 507 deletions
+158
View File
@@ -0,0 +1,158 @@
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(not_equal=seed % 2 == 0, hidden_bids=seed % 3 == 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
if options.not_equal:
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_not_equal_restricts_last_bidder():
game = WizardGame(3, GameOptions(not_equal=True), 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(0))
assert game.observation(2).bids == (0, 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
@@ -0,0 +1,79 @@
from wizard_game import Suit, card_by_id
from wizard_game.cards import JESTER_ID_OFFSET, WIZARD_ID_OFFSET
from wizard_game.rules import lead_suit, legal_cards, round_score, total_rounds, trick_winner
def num(suit: Suit, rank: int):
return card_by_id(suit.index * 13 + rank - 1)
WIZ1 = card_by_id(WIZARD_ID_OFFSET)
WIZ2 = card_by_id(WIZARD_ID_OFFSET + 1)
JES1 = card_by_id(JESTER_ID_OFFSET)
JES2 = card_by_id(JESTER_ID_OFFSET + 1)
def test_total_rounds():
assert [total_rounds(n) for n in (3, 4, 5, 6)] == [20, 15, 12, 10]
def test_card_ids_are_consistent():
card = num(Suit.GREEN, 7)
assert card.suit is Suit.GREEN and card.rank == 7
assert WIZ1.is_wizard and JES1.is_jester
def test_lead_suit():
assert lead_suit([]) is None
assert lead_suit([num(Suit.RED, 3), num(Suit.BLUE, 5)]) is Suit.RED
assert lead_suit([JES1, num(Suit.BLUE, 5)]) is Suit.BLUE
assert lead_suit([JES1, JES2]) is None
assert lead_suit([WIZ1, num(Suit.BLUE, 5)]) is None
assert lead_suit([JES1, WIZ1, num(Suit.BLUE, 5)]) is None
def test_must_follow_suit():
hand = [num(Suit.RED, 1), num(Suit.BLUE, 2), WIZ1, JES1]
assert set(legal_cards(hand, [num(Suit.RED, 9)])) == {num(Suit.RED, 1), WIZ1, JES1}
def test_free_play_without_lead_suit_cards():
hand = [num(Suit.GREEN, 1), num(Suit.BLUE, 2), JES1]
assert set(legal_cards(hand, [num(Suit.RED, 9)])) == set(hand)
def test_free_play_after_wizard_lead():
hand = [num(Suit.RED, 1), num(Suit.BLUE, 2)]
assert set(legal_cards(hand, [WIZ1])) == set(hand)
def test_first_wizard_wins():
trick = [num(Suit.RED, 13), WIZ1, WIZ2]
assert trick_winner(trick, Suit.RED) == 1
def test_trump_beats_lead_suit():
trick = [num(Suit.RED, 13), num(Suit.BLUE, 2), num(Suit.BLUE, 5)]
assert trick_winner(trick, Suit.BLUE) == 2
def test_highest_lead_suit_wins_without_trump():
trick = [num(Suit.RED, 3), num(Suit.GREEN, 13), num(Suit.RED, 10)]
assert trick_winner(trick, None) == 2
assert trick_winner(trick, Suit.YELLOW) == 2
def test_jester_lead_then_suit():
trick = [JES1, num(Suit.GREEN, 2), num(Suit.RED, 13)]
assert trick_winner(trick, None) == 1
def test_only_jesters_first_wins():
assert trick_winner([JES1, JES2], Suit.RED) == 0
def test_round_score():
assert round_score(0, 0) == 20
assert round_score(3, 3) == 50
assert round_score(2, 0) == -20
assert round_score(1, 4) == -30