From e8d511a91aaee25282808001f4ddaead6a018cab Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 10 May 2026 14:20:42 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects):=20=E2=9C=A8=20add=20claudio=20?= =?UTF-8?q?player=20api=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../objectives/p2-67-claude-player-api.md | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 .project/objectives/p2-67-claude-player-api.md diff --git a/.project/objectives/p2-67-claude-player-api.md b/.project/objectives/p2-67-claude-player-api.md new file mode 100644 index 00000000..d34fdf9e --- /dev/null +++ b/.project/objectives/p2-67-claude-player-api.md @@ -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 }, + 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, 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` 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` 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//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 20–25 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 0–5 = **3–4 days** focused work. Phase 1 (`mc-player-api`) is +the bulk; Phases 2–5 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