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