feat(@projects): ✨ add claudio player api documentation
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
e2fdd69823
commit
e8d511a91a
1 changed files with 235 additions and 0 deletions
235
.project/objectives/p2-67-claude-player-api.md
Normal file
235
.project/objectives/p2-67-claude-player-api.md
Normal 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 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
|
||||
Loading…
Add table
Reference in a new issue