Metadata-Version: 2.4
Name: wingspan-rl
Version: 1.0.0
Summary: Wingspan board-game simulator and RL training pipeline, with a bundled trained model
License: MIT
Project-URL: Homepage, https://github.com/bmboucher/wingspan
Project-URL: Repository, https://github.com/bmboucher/wingspan
Classifier: Programming Language :: Python :: 3.12
Classifier: License :: OSI Approved :: MIT License
Classifier: Topic :: Games/Entertainment :: Board Games
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Requires-Dist: torch>=2.0
Requires-Dist: pydantic>=2.0
Requires-Dist: pyyaml
Requires-Dist: rich>=13.7
Requires-Dist: psutil>=5.9
Requires-Dist: boto3>=1.34
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: pytest-xdist>=3.5; extra == "dev"
Requires-Dist: pyright>=1.1.400; extra == "dev"
Requires-Dist: black>=24.0; extra == "dev"
Requires-Dist: isort>=9.0; extra == "dev"
Requires-Dist: boto3-stubs[s3]; extra == "dev"
Requires-Dist: types-PyYAML; extra == "dev"
Dynamic: license-file

# Wingspan

A simulator and reinforcement-learning training pipeline for the **core-set**
board game [Wingspan](https://stonemaiergames.com/games/wingspan/), supporting
**2-4 players from the CLI** (the engine/config support up to 5; see
[docs/PROJECT.md](docs/PROJECT.md)). A given trained network plays at exactly
the seat count it was trained at (`num_players`, a per-run config field);
`wingspan play` seats any mix of human, random, and AI players up to that
count. You can play a full
game from the terminal, run quick automated games for logs or debugging, watch
a trained network play (against itself, a frozen past self, or a random
agent), and train a neural-network agent by self-play while watching it
improve on a live dashboard.

## Setup

Requires **Python 3.12+**. Install the released package from PyPI:

```
pip install wingspan-rl
```

and play a game with the bundled trained model right away:

```
wingspan play --html game.html
```

PyTorch is the heavy transitive dependency. For a much smaller install, put the
CPU-only wheel in first (see the
[PyTorch install selector](https://pytorch.org/get-started/locally/); e.g.
`pip install torch --index-url https://download.pytorch.org/whl/cpu`) and then
`pip install wingspan-rl`.

### Bundled model

The package ships a trained model (artifact era 1.5, trained by self-play,
**2-player only**). When `--p0` / `--p1` are `last` / `best` / `opponent` and
no such checkpoint exists in the checkpoint directory, `wingspan play` falls
back to the bundled model and says so; `--p0 release` selects it explicitly.
`--p2` / `--p3` (3-4 player tables) require a model you trained yourself.
`wingspan --version` shows the package version and the bundled model's run.

### Development install

To work on the code, install this checkout (editable) instead:

```
pip install -e .
```

This pulls in everything needed to play and to train (PyTorch, NumPy, Pydantic,
rich). For the test suite and developer tooling (pyright, black, isort, pytest):

```
pip install -e ".[dev]"
```

Training runs on **CPU**: self-play collection is CPU-bound and fans out across
worker processes, and the gradient update is small, so the whole pipeline is
designed for CPU. (A CUDA PyTorch build still works for one-off experiments, but
CPU is the supported path.)

## Play games

One command runs every matchup — interactive play, quick automated games, and
trained-AI matches. Set each seat with `--p0` / `--p1` / `--p2` / `--p3`; the
value is `human` (interactive play in the terminal), `random` (the
uniform-random agent), a named checkpoint (`last`, `best`, or `opponent`,
resolved against `--checkpoint-dir`), a path to a `.pt` file, or a run
directory (its `last.pt` is seated). `--p0` / `--p1` are always active
(2-seat default); `--p2` / `--p3` add a 3rd / 4th seat and must be filled
contiguously (`--p3` without `--p2` is rejected). A trained checkpoint can
only be seated at the table size it was trained at (`num_players`) — mixing,
say, a 2-player checkpoint into a 3-seat table is refused up front. The
default matchup is `last` vs `last` — the most recent trained model playing
itself — so the bare command needs a trained model (see **Train an agent**);
use `--p0 human --p1 random` to play without one:

```
wingspan play                                 # latest trained model vs itself
wingspan play --p0 human --p1 random          # play interactively vs a random opponent
wingspan play --p0 human --p1 human           # two humans, hotseat on one keyboard
wingspan play --p0 human --p1 best --seed 42  # take on the strongest checkpoint, reproducible deal
wingspan play --p0 random --p1 random --games 5 --log games.log  # quick automated games -> games.log.0 ..
wingspan play --p0 best --p1 opponent --games 10        # best vs the frozen ladder rung
wingspan play --p0 best --p1 best --greedy --log ai.log # best vs itself, argmax play
wingspan play --p0 runs/exp/last.pt --p1 random         # a checkpoint by path
wingspan play --p0 best --p1 best --html game.html      # navigable HTML game-log viewer
wingspan play --p0 random --p1 random --p2 random --html game.html  # 3-seat random game, HTML log
wingspan play --p0 best --p1 best --jsonl game.jsonl    # flat structured log, for analysis
```

A `human` seat gets a numbered menu for every choice the rules require — pick a
bird, choose a habitat, pay food, lay eggs, and so on. When a seat is
AI-driven, every genuine decision is annotated in the game log with the
policy's ranked probability distribution over the legal options, turning the
log into a move-by-move readout of what the network was "thinking", and
`--greedy` makes AI seats take the argmax option instead of sampling. `--games
N` plays a series (game *i* deals with `--seed + i`), `--log` writes a
structured plaintext game log per game (sourced from the event tree — the
same log the `--jsonl` and `--html` outputs are built from), and `--quiet`
suppresses the per-game summary. For the older raw `engine.log` dump, use
`--debug-log FILE` instead; by default it splits into `FILE_p0.log` /
`FILE_p1.log` (one file per seat), or pass `--collate` for a single
interleaved file. `--device` selects the torch device AI seats run
inference on (`cpu` by default). `--html FILE` additionally writes a
self-contained HTML viewer that replays the
game one phase/turn at a time — prev/next arrows, a per-seat view toggle
(`P0 / Both / P1` at 2 seats, `All / P0 / P1 / ...` at 3+ seats), 3x5 board
grids pinned at the top, and a collapsible decision log beneath (for a
`--games` series the game index is inserted before the extension, `game.html ->
game.0.html`).  The **Timeline** modal (opened via the toolbar button) shows
per-seat score lines and per-seat critic/target return lines; it includes a
**⬇ CSV** download link that yields one row per in-game decision with columns
for timestamp, player, per-seat scores, and per-seat critic/target values
(each seat's own future-return margin vs its best other seat, in VP — not
projected onto any other seat's axis).

### The flat structured log

`--jsonl FILE` writes the same game as data: one JSON object per line — a header
row per match, then one row per event and sub-event. Every state change the
engine made is there with its own typed columns (`card`, `food`, `amount`,
`bird`, `habitat`), each event row carries the same at-a-glance header the HTML
viewer shows, and the tree's nesting survives as `event_id` / `parent_id` /
`depth` columns. Unlike `--log`, a `--games` series appends to the *one* file,
keyed by `game_id`.

```python
import pandas

rows = pandas.read_json("game.jsonl", lines=True)
rows[rows.kind == "lay_egg"].groupby("player_id")["count"].sum()
```

`wingspan tournament --jsonl games.jsonl` does the same for every game of a
round-robin. See [`docs/GAMELOG.md`](docs/GAMELOG.md) for the full row schema.

## Train an agent

The training app is **FLIGHT PLAN** — a full-screen TUI that opens on a
configuration screen first, then transitions to a live `top`-style dashboard
once a run is started or resumed:

```
wingspan dashboard                    # open FLIGHT PLAN (always starts in config)
wingspan dashboard --collect-device cpu --train-device cuda   # CPU pool feeding a GPU learner
wingspan dashboard --train-device cpu                         # CPU-only
wingspan dashboard --games-per-iter 256 --eval-every 5 --eval-games 128
wingspan dashboard --config runs/my_run.json                 # open the config screen seeded from FILE
wingspan dashboard --config runs/my_run.json --start          # skip the screen and launch immediately
```

`--config FILE` supplies the whole run config from a file instead of building
it from flags — a dated `run_config_<stamp>.json` artifact, a
`configurator_defaults.json`-style envelope, a cloud run-file's `train:`
block, or a bare `RunConfig` JSON/YAML dump (see
`wingspan.training.config_file`). Only the five run-identity flags
(`--checkpoint-dir`, `--run-name`, `--collect-device`, `--train-device`,
`--resume`/`--no-resume`) may be combined with `--config` to override the
file; any other flag is a usage error, since the file already speaks for
every other field. Without `--start` the config screen still opens, seeded
from the file, so you can review or tweak it before pressing Start; with
`--start` the run launches immediately and headlessly — it never archives or
overwrites an existing run on its own, so an incompatible or resume-disabled
checkpoint directory is refused with a message pointing you at the config
screen's archive action instead.

The config screen lets you tune every hyperparameter (learning rate,
games/iteration, evaluation cadence, target-iteration milestone, network
width, …) and manage the runs already in the checkpoint directory. Arrow keys
move between fields, ←/→ nudge a value (or cycle a choice), and Enter edits
one directly; every value is validated as you type. The screen shows whether
the directory holds a resumable run and flags which edits would force a fresh
start (changing the network width can't reuse old weights). From there:

- **Start** resumes a compatible run, or starts a fresh one in an empty
  directory.
- **New run** (or starting after an incompatible edit) prompts you to
  **archive** the existing run to `checkpoints/archive/<label>/` — preserving
  its checkpoints, metrics, and log — before the new run begins, so a long
  training run is never silently overwritten.

Once started, the dashboard runs until you press **Ctrl+C**, which asks it to
finish the current game, save a final checkpoint, and print a summary. As
strength improves the evaluation opponent advances from the random agent to
frozen past selves (a self-play ladder). Checkpoints (`last.pt`, `best.pt`, the
frozen `opponent.pt`), a per-iteration `metrics.jsonl`, and a per-game
`games.jsonl` are written to the checkpoint directory (`checkpoints/` by
default; change with `--checkpoint-dir`), and runs are resumable. Pass
`--iterations N` to stop automatically after N rounds instead.

You can also set a **target-iteration** milestone (in the config screen):
the run pauses there, runs a large fixed-model self-play evaluation, and waits
for you to **[C]ontinue** — optionally entering a new target — or **[E]nd** the
run and return to the config screen.

See [docs/TRAINING.md](docs/TRAINING.md) for the training program and
[docs/DECISIONS.md](docs/DECISIONS.md) for the per-decision modelling direction.

## Run a tournament

`wingspan tournament` runs a round-robin between any mix of trained AIs and the
random agent, tracks Elo ratings, and writes a JSON report.

```
wingspan tournament                                        # interactive picker (default)
wingspan tournament --base-dir checkpoints                 # picker scans a different dir
wingspan tournament --no-picker --ai runs/exp1 --ai runs/exp2        # skip picker; explicit dirs
wingspan tournament --no-picker --ai runs/exp1 --include-random      # add the random agent
wingspan tournament --no-picker --ai runs/exp1 --ai runs/exp2 \
    --games-per-pair 64 --out results.json                 # 64 mirrored games per pair
wingspan tournament --quiet                                # no live UI; periodic progress only
```

By default an interactive picker scans `checkpoints/` and lets you choose
competitors with arrow keys. Pass `--no-picker` to specify them via `--ai
<checkpoint-dir>` flags (repeatable) and optionally `--include-random`. Each
pair plays `--games-per-pair` games (default 32) as mirrored deals. The live
dashboard (skipped with `--quiet`) shows Elo ratings and W-L-T updating as
games finish; **q** / Ctrl+C stops after the current games complete. The final
standings and the full JSON report are written to `--out`
(`tournament_report.json` by default).

The tournament is **2-seat only for now**: N-seat scheduling and Elo are
deferred scope, so a checkpoint trained at `num_players != 2` is refused with
a clear error rather than being silently seated. 3-4 player checkpoints still
play fine through `wingspan play`; only the round-robin tournament format is
restricted.

## Get live advice for a physical game

`wingspan aid` is a live assistant for a 2-player game played at a physical
table, not on the computer. It runs a terminal session that starts at game
setup — you enter your dealt hand, bonus cards, round goals, the initial
tray, and the feeder roll — and a trained model then ranks your options for
every decision you make for the rest of the game, showing its top picks and
expected VP margin before you confirm what you actually played:

```
wingspan aid                                  # advise with the latest trained model
wingspan aid best --seed 42                   # advise with the strongest checkpoint
wingspan aid --log session.log --jsonl session.jsonl  # write session logs
wingspan aid --trust-me                       # auto-play the model's picks for your seat
```

In a real terminal, card prompts are type-to-filter pickers (type a few
letters to narrow the list, arrows + Enter to select; Enter on an empty
query marks a revealed card as face-down where that's allowed) and dice are
entered as per-face counts (arrows to move between faces and adjust, Enter
to accept once the total matches). When stdio is scripted or redirected the
same prompts fall back to plain text: comma-separated card names and
space-separated die faces.

The opponent's moves are relayed by you as they happen — the assistant asks
"what did they do?" for each of their decisions and tracks their hand and
bonus cards as hidden counts (never guessing their identities), matching
what the encoder itself is allowed to see. At the end of the final round it
offers to enter the opponent's bonus card(s) for exact scoring; skipping
that leaves their bonus VP as a placeholder to count manually. Only
checkpoints trained at `num_players=2` can drive the advisor seat, and a
session runs setup-to-end with no mid-game entry or undo.

Pass `--trust-me` to skip confirming your own moves: the model's top pick is
committed automatically for your seat, so you only sit through setup entry,
oracle reveals/dice, and relaying the opponent's moves — the recommendation
display still prints first, since under `--trust-me` it's what tells you
which move to actually execute at the table.

## Run a research study

`wingspan research` hosts the offline, fixed-model studies from
[docs/RESEARCH.md](docs/RESEARCH.md). The first is the setup keep-rate
**experience study**: it deals random setups, asks the run's setup model which
birds it keeps, and writes one CSV row per dealt bird (an *exposure*) with a
0/1 `kept` flag (the *observation*), so a card's keep rate is
`sum(kept) / count(rows)` and pivots by any other column.

```
wingspan research setup-keep                               # 1000 setups from checkpoints/ -> setup_keep.csv
wingspan research setup-keep --setups 20000 --workers 8    # fan the deals across 8 processes
wingspan research setup-keep --checkpoint-dir runs/exp1 --out exp1_keep.csv
wingspan research setup-keep --temperature 1.0             # sample the keep instead of taking the argmax
```

Each setup is one seat of a fresh seeded game dealt exactly as the engine deals
it (tray, feeder roll, round goals, five birds, two bonus cards); games use
consecutive seeds from `--seed`, so a study can be extended by continuing the
range, and the CSV is identical whether it ran in-process or under `--workers`.
Every row carries the exposure key (`setup_id`, `game_seed`, `seat`,
`card_slot`), the observation (`kept`, the policy's marginal `keep_prob`,
`n_kept`, the critic's `deal_value`, `bonus_kept`), the card's printed
attributes (points, egg limit, wingspan, nest, power color, habitats, per-food
cost, flocking/predator, effect kinds), and the rest of the setup (`hand`,
`tray_1..3`, the six `feeder_*` die counts, `goal_1..4` with categories,
`bonus_1`/`bonus_2`). The seat count defaults to the run's trained
`num_players`; the run must have trained a setup model (`setup.pt`).

The second is the **game-stats study**: greedy self-play of one run's model
against itself (its setup net resolves the opening keep), capturing one CSV row
per seat per game — the score after every turn (0–26, in both a projected and a
locked-in round-goal convention), the main action chosen on every turn, the
winner/tie, and the six-way final score breakdown. Alongside the raw table it
writes chart-ready aggregates (per-turn mean/CI/percentile score curves and
action frequencies for all vs. won games, the category breakdown as CSV and
markdown, paired final scores per game) plus a self-contained `preview.html`.
With `--blog-snippets` it also writes `blog_snippets/` — one ready-to-paste
Plotly embed block per figure for a markdown blog post (the scattergram
subsampled to keep the page light), plus the pinned CDN `<script>` tag.

```
wingspan research game-stats --out stats/                          # 10k games from checkpoints/last.pt
wingspan research game-stats --games 100000 --workers 10 --out stats/
wingspan research game-stats --aggregate-only stats/games_raw.csv --out stats/ --blog-snippets
```

## Probe a model's architecture

`wingspan analysis probe` measures how much each part of a trained network's
architecture actually matters: it self-plays a sample of on-distribution
decisions, then reports board-attention behavior and ablation sensitivity
(zero / uniform / per-head knockout, scored by policy KL and value shift),
trunk and choice-encoder layer capacity (effective rank, linear
predictability, dead units), and which trunk input groups the network reads
from.

```
wingspan analysis probe last --games 50 --reference best   # probe the latest checkpoint, compare against best.pt
```

See `docs/TRAINING.md` "Representation diagnostics" for how to read the
numbers and `src/wingspan/analysis/INDEX.md` for the package's caveats.

## Review a finished run

`training_summary.html` is a standalone, interactive HTML page charting a
run's `metrics.jsonl` progression end to end: strength vs. the reference
opponent, score composition, loss curves, throughput, and — when the run used
them — the setup-model and architecture-probe readouts. Every chart is a
`Plotly.js` chart (pan/zoom/hover), unlike the live dashboard's fixed-size
terminal charts.

The page is written automatically at the target milestone, alongside
`final_eval_<n>.json`, and can be regenerated on demand for any run directory
with `wingspan summary [RUN_DIR] [--out FILE]` (`RUN_DIR` defaults to
`checkpoints`). Because the charts load Plotly from its CDN
(`cdn.plot.ly`), viewing the page needs internet access.

## Installed commands

After `pip install -e .` all tools are available through a single `wingspan`
command with subcommands:

| Command                 | What it does                                            |
| ----------------------- | ------------------------------------------------------- |
| `wingspan play`         | Games between any mix of human, random, and AI seats    |
| `wingspan aid`          | Live assistant for a physical 2-player game             |
| `wingspan dashboard`    | FLIGHT PLAN: config screen → live training dashboard    |
| `wingspan tournament`   | Round-robin tournament between trained AIs              |
| `wingspan inspect`      | Model introspection report (vectors, architecture, params) |
| `wingspan summary`      | Training-progression HTML report for a run directory   |
| `wingspan cloud`        | Headless S3-persisted training (container use)          |
| `wingspan monitor`      | FLOCK WATCH: live roster of cloud runs                  |
| `wingspan research`     | Offline research studies (setup keep-rate, game-stats)  |
| `wingspan analysis`     | Architecture-importance probe over a trained checkpoint |

Run `wingspan --help` for the full list, or `wingspan <command> --help` for
per-command usage.

## Tests

Run the full quality gate (pyright + isort + black + pytest):

```
bash scripts/quality_gate.sh
```

See `CLAUDE.md` for the gate's section flags and pass-through argument reference.

## Further reading

| File | What it covers |
| ---- | -------------- |
| [docs/PROJECT.md](docs/PROJECT.md) | Full package layout and what's modelled |
| [docs/DECISIONS.md](docs/DECISIONS.md) | Decision/choice taxonomy, `ALL_DECISION_CLASSES` ordering, featurization |
| [docs/BIRDS.md](docs/BIRDS.md) | Every bird power: `EffectKind` patterns, handler mappings, implementation gaps |
| [docs/TRAINING.md](docs/TRAINING.md) | Training program, hyperparameter guidance, eval ladder |
| [docs/RESEARCH.md](docs/RESEARCH.md) | Research agenda, per-project feasibility analysis |
