Reinforcement learning agents for the board game Wingspan. The goal is to implement a simplified version of the game, then train agents to discover optimal play strategies through self-play — inspired by AlphaGo and Leela Chess Zero.
- Game engine — Implement a playable simplified Wingspan (done)
- MCTS agent — Monte Carlo Tree Search with UCT to select actions (done)
- Self-play training — Agents play against each other to iteratively improve their policies (done)
- Policy interpretation — Understand what agents learn about the game (done)
# Install uv if you don't have it
# https://docs.astral.sh/uv/getting-started/installation/
# Sync dependencies (creates .venv automatically)
uv sync
# Play against a random bot
uv run python -m src.game --num_players 2 --num_human 1
# Play against an MCTS bot (smarter, ~0.2s per decision)
uv run python -m src.game --num_players 2 --num_human 1 --policy mcts
# MCTS with more simulations (stronger, slower)
uv run python -m src.game --num_players 2 --num_human 1 --policy mcts --num_simulations 500
# MCTS with learned playout policy (uses trained policy instead of random rollouts)
uv run python -m src.game --num_players 2 --num_human 1 --policy mcts --playout_policy models/policy_latest.npz
# Watch two bots play each other
uv run python -m src.game
# Train a policy via self-play (REINFORCE with linear function approximation)
uv run python -m src.train train --num_iterations 50 --games_per_iteration 100
# Evaluate a trained policy against random
uv run python -m src.train evaluate --policy_path models/policy_latest.npz
# Evaluate against MCTS
uv run python -m src.train evaluate --policy_path models/policy_latest.npz --opponent mcts
# Play against a trained policy
uv run python -m src.game --num_players 2 --num_human 1 --policy learned --policy_path models/policy_latest.npz
# Interpret a trained policy
uv run python -m src.train interpret --policy_path models/policy_latest.npz --mode summary
uv run python -m src.train interpret --policy_path models/policy_latest.npz --mode weights
uv run python -m src.train interpret --policy_path models/policy_latest.npz --mode trace
uv run python -m src.train interpret --policy_path models/policy_latest.npz --mode evolution --save
# Run tests
uv run python -m pytestThe game engine implements a subset of Wingspan's rules:
| Feature | Real Wingspan | This Implementation |
|---|---|---|
| Bird cards | 170+ unique birds with powers | 180 birds, VP + food cost only (no powers) |
| Food types | 5 distinct types | Single generic food token |
| Actions | 4 (play bird, gain food, lay eggs, draw bird) | 3 (no lay eggs) |
| Habitats | 3 rows with increasing capacity | Single flat board (5 slots) |
| Bonus cards | End-of-round and end-of-game goals | Not implemented |
| Egg mechanic | Eggs as resources and VP | Not implemented |
These simplifications reduce the state space to make MCTS tractable while preserving the core decision-making loop: which birds to play, when to gather resources, and when to invest in drawing new options.
wingspan/
├── src/
│ ├── constants.py # Game phase constants
│ ├── entities/ # Core game objects
│ │ ├── bird.py # Bird card (name, VP, food cost)
│ │ ├── birdfeeder.py # Shared food supply (5 tokens, auto-reroll)
│ │ ├── deck.py # Draw pile with shuffle
│ │ ├── food_supply.py # Per-player food tokens
│ │ ├── gameboard.py # Per-player board (5 bird slots)
│ │ ├── game_state.py # GameState + MCTSGameState (serialization)
│ │ ├── hand.py # Player hand management
│ │ ├── player.py # Player, HumanPlayer (CLI), BotPlayer (policy-driven)
│ │ └── tray.py # Face-up bird display (3 slots)
│ ├── rl/
│ │ ├── policy.py # Policy ABC, RandomPolicy, MCTSPolicy
│ │ ├── mcts.py # Node and Edge classes for game tree
│ │ ├── linear_policy.py # LinearPolicy (trainable, numpy-based)
│ │ ├── featurizer.py # GameState → feature vector for learning
│ │ ├── self_play.py # Bot-vs-bot game runner + experience collection
│ │ ├── trainer.py # REINFORCE policy gradient training
│ │ ├── evaluator.py # Win rate evaluation against baselines
│ │ └── interpreter.py # Policy interpretation and analysis
│ ├── utilities/
│ │ ├── utils.py # Terminal rendering helpers
│ │ └── player_factory.py # Player creation (avoids circular imports)
│ ├── game.py # Game setup, turn loop, CLI entry point
│ └── train.py # CLI for training and evaluation
├── tests/ # Unit tests (unittest + pytest)
├── data/
│ ├── bird_data.csv # Source bird data
│ ├── bird_list.py # Generated: 180 Bird objects
│ └── generate_bird_list.py
- GameState consolidates all game objects: bird deck, discard pile, tray, bird feeder, players, and turn/phase tracking
- Players inherit from
Player.HumanPlayertakes CLI input;BotPlayerdelegates decisions to aPolicyobject - Game loop rotates through players, validates legal actions, executes the chosen action, and updates state until turns are exhausted
- Scoring sums victory points of birds on each player's board
- Policy maps
(state, actions) → chosen action.RandomPolicypicks uniformly;MCTSPolicyuses tree search - MCTSPolicy implements the full select → expand → playout → backpropagate loop:
- Select: traverse the tree via UCB1 to find a leaf node
- Expand: generate child nodes for each legal action (using
copy.deepcopy) - Playout: simulate the game to completion using
RandomPolicyor a trainedLinearPolicy(cloned viaMCTSGameState.from_representationfor imperfect-information determinization) - Backpropagate: walk parent pointers updating visit counts and rewards
- MCTSGameState extends GameState with hashable
to_representation()/from_representation()for state serialization, handling hidden information (opponent hands stored as counts)
- LinearPolicy uses two learned linear models, both interpretable (#80):
- Action choice:
state_features @ weights → softmaxpicks between play/gain/draw. 26 state features (game progress, food, score, hand/tray quality, strategic calculations like max achievable VP and greedy rollout, deck composition stats, opponent state, interaction/polynomial features like food gap, endgame flag, and urgency) × 3 action columns - Sub-decisions:
[state_features; option_features] @ sub_weights → softmaxpicks which bird to play/draw. 8 per-option features (VP, cost, ratio, affordability, time to play, deck flag) let the model learn contextual bird selection
- Action choice:
- REINFORCE policy gradient with baseline updates both weight vectors from self-play outcomes
- Self-play runner plays bot-vs-bot games, collects experience for both action and sub-decision levels
- Evaluator measures win rate against random or MCTS baselines, alternating positions to remove first-player bias
- Training CLI (
src/train.py): train with--fresh/--resume, evaluate against random or MCTS, plot training progress - Interpretation (
interpreter.py): pure analysis functions for weight inspection, feature importance, strategy rule generation, decision traces with diverse game states, and weight evolution across checkpoints. CLI viatrain.py interpretwith 5 modes:weights,importance,summary,trace,evolution. Visualizations use seaborn - Analysis report (
report.py):train.py reportgenerates a Jupyter notebook combining all interpretation modes with interactive exploration cells (what-if feature sweeps, counterintuitive decision finder). Run with:uv run python -m src.train report --policy_path models/policy_latest.npz
See GitHub Issues for the full backlog. Next milestones:
- #75 — Add a value network to evaluate positions without full playout
- Expectimax MCTS (arXiv:0909.0801) — Two node types for stochastic games
- AlphaGo (Silver et al., 2016) — MCTS + neural network self-play
- Wingspan (Stonemaier Games)