Files
Wizard/wizard_ai/tests/test_ai.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

81 lines
3.2 KiB
Python

import json
import random
import pytest
from wizard_game import GameOptions, WizardGame
from wizard_ai.agents import HeuristicAgent, ModelAgent, RandomAgent, make_agent
from wizard_ai.model import FEATURE_SIZE, ActionSpace, UniformPolicy, encode_observation, load_policy
from wizard_ai.training import SelfPlayTrainer, TrainingConfig, WizardEnv, evaluate
@pytest.mark.parametrize("num_players", [3, 4, 5, 6])
def test_encoding_has_fixed_size_and_masks_match(num_players):
game = WizardGame(num_players, GameOptions(hidden_bids=True), seed=num_players)
rng = random.Random(0)
while not game.is_finished:
obs = game.observation(game.current_player)
assert len(encode_observation(obs)) == FEATURE_SIZE
mask = ActionSpace.mask(obs)
legal = {ActionSpace.index(a) for a in obs.legal_actions}
assert {i for i, allowed in enumerate(mask) if allowed} == legal
game.apply(obs.player, rng.choice(obs.legal_actions))
def test_action_space_round_trip():
for index in range(ActionSpace.SIZE):
assert ActionSpace.index(ActionSpace.action(index)) == index
@pytest.mark.parametrize("agent", [HeuristicAgent(), RandomAgent(1), ModelAgent(UniformPolicy(1))])
def test_agents_play_legal_games(agent):
game = WizardGame(4, seed=3)
while not game.is_finished:
obs = game.observation(game.current_player)
action = agent.act(obs)
assert action in obs.legal_actions
game.apply(obs.player, action)
def test_env_rewards_add_up_to_scores():
env = WizardEnv(3, seed=5)
obs = env.reset()
totals = [0.0] * env.num_players
rng = random.Random(5)
while obs is not None:
result = env.step(rng.choice(obs.legal_actions))
totals = [t + r for t, r in zip(totals, result.rewards)]
obs = result.observation
assert [round(t * 100) for t in totals] == env.game.scores
def test_trainer_runs_and_checkpoints(tmp_path):
config = TrainingConfig(episodes=4, player_counts=(3, 4), episodes_per_update=2,
checkpoint_dir=tmp_path / "ckpt", seed=1)
trainer = SelfPlayTrainer(UniformPolicy(1), config)
trajectories, scores = trainer.play_episode()
assert len(trajectories) == len(scores)
assert all(t[-1].done for t in trajectories)
assert sum(sum(tr.reward for tr in t) for t in trajectories) == pytest.approx(sum(scores) / 100)
trainer.train()
assert json.loads((tmp_path / "ckpt" / "policy.json").read_text())["type"] == "uniform"
assert isinstance(load_policy(tmp_path / "ckpt"), UniformPolicy)
def test_trainer_against_heuristic_opponents():
config = TrainingConfig(episodes=2, player_counts=(5,), opponents="heuristic", seed=2)
trajectories, _ = SelfPlayTrainer(UniformPolicy(2), config).play_episode()
assert len(trajectories) == 1
def test_make_agent_falls_back_without_checkpoint(tmp_path):
assert isinstance(make_agent("model", tmp_path / "missing"), HeuristicAgent)
UniformPolicy().save(tmp_path / "ckpt")
assert isinstance(make_agent("model", tmp_path / "ckpt"), ModelAgent)
def test_heuristic_beats_random():
result = evaluate([HeuristicAgent(), RandomAgent(1), RandomAgent(2), RandomAgent(3)], games=40, seed=0)
assert result.mean_scores[0] > max(result.mean_scores[1:])