feat(@projects): add claudio player api documentation

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-05-10 14:20:42 -07:00
parent e2fdd69823
commit e8d511a91a

View file

@ -0,0 +1,235 @@
---
id: p2-67
title: "Claude-driven player API — programmatic player + Agent-SDK adapter"
priority: p2
status: stub
scope: game1
category: tooling
owner: simulator-infra
created: 2026-05-10
updated_at: 2026-05-10
blocked_by: []
follow_ups: []
---
## Context
A Claude Agent SDK process should be able to **play a real game of
Magic Civilization vs. the production AI**, taking authentic
player-equivalent actions one at a time and reading game state from
data — not from screen scraping. Each turn is a sequence of discrete
actions ("open city, queue warrior, close city, move unit, end turn"),
the same flow the human UI exercises.
This unlocks:
- Authentic gameplay screenshots (this objective is the proper fix
for the gap p2-66 only papered over).
- Headless playtesting: Claude vs. AI tournaments, regression detection
via behavioural diffs, balance-tuning A/B runs.
- Live demos: stream Claude's reasoning + action choices alongside
the rendered game.
## Source-of-truth rails
- **Rust crate**: `mc-player-api` — single crate that owns the
`PlayerAction` enum, `PlayerView` snapshot type, `apply_action`,
`view`. All logic in Rust per Rail-1.
- **JSON path**: no new game-content files. The protocol is wire-only
JSON, not authored data.
- **GDScript**: presentation only. The Godot-side harness is a thin
GDExtension wrapper around `mc-player-api` plus a stdin/stdout pump.
- **Existing leverage**:
- `mc-core::action::ActionKind` — unit actions vocabulary.
- `mc-core::city_action::CityAction` — city actions vocabulary.
- `mc-core::building_action::BuildingAction` — building queue ops.
- `mc-mcts-service` — precedent for framing + JSON-RPC server.
- `auto_play.gd` — full headless game-flow harness with events.jsonl.
- `AiTurnBridge::run(player)` — proven action dispatch into mc-turn.
## Acceptance
- ❌ `mc-player-api` crate exposes `apply_action(state, player, action)`
and `view(state, player)` covering every action a UI button can
perform. Round-trip: serialise view → choose action → deserialise
action → apply.
- ❌ Headless Godot harness (`scripts/claude-player-server.sh`
`scenes/headless/claude_player_main.tscn`) runs a seeded game,
binds player slot 0 to stdin/stdout JSON-RPC, runs the production
AI for slots 1..N. Drains AI turns automatically; pauses on
player-0 turn until it receives an `EndTurn` action.
- ❌ Claude SDK adapter (`tooling/claude-player/`) — TypeScript
Agent SDK app — connects to the harness, reads view, picks action,
sends, repeats. Plays one full game vs. AI to victory or 100-turn
cap.
- ❌ Snapshot test: `mc-player-api::tests::seeded_game_replay` runs a
scripted action sequence and asserts the resulting events match a
golden file. Catches behavioural drift.
- ❌ Demo deliverable: a screen-recording (or 25-frame screenshot
series) of one Claude vs. AI game, with action log alongside.
- ❌ Phase-gate proof: Claude's first 10 turns logged + reviewed in
the conversation that closes this objective.
## Out of scope
- Magic / Archons / Ascension (Game-2/3 features).
- Multi-Claude games (Claude vs Claude). Adapter handles one player slot.
- Network IPC. Stdin/stdout local pipe is sufficient for v1; TCP comes later.
- UI parity — the harness drives state, not the world_map renderer.
Renders happen separately when wanted (replay viewer + p2-66 paths).
## Phase plan
### Phase 0 — Design + JSON schema (~3 hr)
- Enumerate every UI button in `world_map_hud.tscn`,
`city_screen.tscn`, `tech_tree.tscn`, `culture_tree.tscn`,
`diplomacy_panel.tscn`. Map each button to a `PlayerAction` variant.
- Write `docs/CLAUDE_PLAYER_API.md` with the JSON-RPC schema
(Request / Response / Notification envelopes), action variants,
view shape, error codes.
- Decide: stdin/stdout JSON-Lines vs. JSON-RPC 2.0. (Recommend
Lines — simpler, matches `mc-mcts-service::framing`.)
- Confirm view perspective: fog-of-war filtered, hidden tech / hidden
diplomacy redacted to player slot 0's knowledge.
### Phase 1 — `mc-player-api` crate (~1 day)
- New crate `src/simulator/crates/mc-player-api/`. Workspace member.
- Re-exports + outer enums:
```rust
pub enum PlayerAction {
Unit { unit_id: UnitId, kind: ActionKind, target: Option<HexCoord> },
City { city_id: CityId, op: CityAction },
Building { city_id: CityId, op: BuildingAction },
Tech { tech_id: String },
Culture { tradition_id: String },
Diplomacy { other: PlayerId, op: DiploOp },
EndTurn,
}
```
- `apply_action(state: &mut GameState, player: PlayerId, action: PlayerAction)
-> Result<Vec<Event>, ActionError>` — dispatches into the same
handlers `mc-turn::action_handlers/` already exposes.
- `view(state: &GameState, player: PlayerId) -> PlayerView` — fog-aware
snapshot. Includes `legal_actions: Vec<PlayerAction>` so Claude
doesn't have to compute legality itself.
- Unit tests: round-trip serialisation, every variant, fog-redaction
invariants.
### Phase 2 — GDExtension surface (~4 hr)
- `api-gdext::player_api` module exposes `GdPlayerApi` class:
- `view_json(player: int) -> String`
- `apply_action_json(player: int, action_json: String) -> String` (returns events JSON)
- Godot can call this from any scene; no wire protocol involved at this layer.
### Phase 3 — Headless harness (~half-day)
- `scenes/headless/claude_player_main.tscn` + `.gd`:
- Boots a seeded game (env: `CP_SEED`, `CP_PLAYERS`, `CP_MAP_SIZE`).
- Connects player slot 0 to **stdin** (read line) / **stdout** (write line).
- For other slots: runs `AiTurnBridge::run(player)` exactly as
`auto_play.gd` does today.
- On player-0's turn: blocks reading stdin. Each line is one
`PlayerAction` JSON. Emits the resulting `Vec<Event>` JSON +
updated `PlayerView` JSON to stdout. Loops until `EndTurn`.
- On all turns: emits a `Notification` line for each EventBus event.
- `scripts/claude-player-server.sh` — flatpak Godot launch wrapper
with the right env vars for headless + auto-quit on stdin EOF.
### Phase 4 — Claude Agent SDK adapter (~half-day)
- New TypeScript package `tooling/claude-player/`.
Uses `@anthropic-ai/sdk` Agent SDK.
- Tools exposed to Claude:
- `view()` — returns current `PlayerView` JSON.
- `act(action)` — sends one `PlayerAction`, returns events + new view.
- `end_turn()` — convenience wrapper for `act({EndTurn})`.
- Loop: spawn `claude-player-server.sh` as child process via
`spawn`, pipe stdin/stdout, run an Agent loop where Claude reads
the view, picks an action, applies, repeats until victory / 100
turns / blocker.
- Output an action log (`tooling/claude-player/.local/runs/<stamp>/log.jsonl`)
with reasoning + action + events per step.
### Phase 5 — End-to-end demo + screenshots (~2 hr)
- Run one Claude vs. 1-AI seeded game.
- Capture a screenshot every 5 turns via the existing
`gameplay_arc_proof` rendering path (now driven by real game state
instead of a scripted arc). Bundle 2025 frames into a demo zip.
- Append the action log to the conversation when closing this
objective so the phase-gate review is complete.
## Architecture sketch
```
┌─────────────────────────────────────────┐
│ Claude Agent SDK (TypeScript) │
│ ┌───────────┐ ┌─────────────────┐ │
│ │ view tool │ ←→ │ tooling/ │ │
│ │ act tool │ │ claude-player/ │ │
│ └───────────┘ └────────┬────────┘ │
└────────────────────────────│────────────┘
stdin/stdout JSON-Lines
┌────────────────────────────│────────────┐
│ Godot (flatpak, headless) │ │
│ ┌─────────────────────────▼─────────┐ │
│ │ claude_player_main.gd (harness) │ │
│ │ - reads stdin / writes stdout │ │
│ │ - drives AI for slots 1..N │ │
│ │ - emits notifications on events │ │
│ └────────┬──────────────────────────┘ │
│ ┌────────▼──────────┐ │
│ │ GdPlayerApi (gdext bridge) │
│ └────────┬──────────┘ │
└───────────│─────────────────────────────┘
┌───────────▼───────────────────────────┐
│ Rust simulator │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ mc-player- │→ │ mc-turn │ │
│ │ api │ │ handlers │ │
│ │ (apply/view)│ │ (existing) │ │
│ └──────────────┘ └──────────────┘ │
└───────────────────────────────────────┘
```
## Decisions resolved 2026-05-10
1. **Wire format**: **JSON-Lines** (one JSON value per line, `\n` framing).
Matches `mc-mcts-service::framing::LineCodec`; trivially debuggable
with `cat`. JSON-RPC 2.0 envelope is overkill for a single-client
local pipe.
2. **Fog-of-war**: **strict by default**. Claude only sees what player
slot 0 sees per the live `Player.observations` cache. Override via
`CP_OMNISCIENT=1` env (debug + golden-test mode only).
3. **Action timeout**: **60s default**, override via `CP_TIMEOUT_SEC`.
On expiry the harness emits `{"type":"turn_timeout"}` notification
and substitutes `AiTurnBridge::run` for that turn so the game keeps
advancing. Adapter logs the substitution for review.
4. **Tool surface**: **three discrete tools**`view()`, `act(action)`,
`end_turn()`. Cleaner Claude UX than one mega-tool with a
discriminator. `end_turn` is sugar for `act({"type":"end_turn"})`
so the wire protocol stays one-action-per-line.
## Total estimate
Phase 05 = **34 days** focused work. Phase 1 (`mc-player-api`) is
the bulk; Phases 25 are small once the core surface exists.
## References
- `src/simulator/crates/mc-core/src/action.rs` — unit action enum
- `src/simulator/crates/mc-core/src/city_action.rs` — city action enum
- `src/simulator/crates/mc-core/src/building_action.rs` — building queue
- `src/simulator/crates/mc-mcts-service/src/{framing,protocol,server}.rs`
— wire-protocol precedent
- `src/simulator/crates/mc-turn/src/action_handlers/` — existing
apply-action plumbing to delegate into
- `src/simulator/crates/mc-ai/src/lib.rs::AiTurnBridge` — AI driver
for non-Claude slots
- `src/game/engine/scenes/tests/auto_play.gd` — full headless harness
precedent
- p2-66 (`world-map-visual-proof.md`) — sister objective for visual
rendering of the resulting games