diff --git a/.project/objectives/p2-67-claude-player-api.md b/.project/objectives/p2-67-claude-player-api.md index eee4313f..05793ace 100644 --- a/.project/objectives/p2-67-claude-player-api.md +++ b/.project/objectives/p2-67-claude-player-api.md @@ -8,8 +8,8 @@ category: tooling owner: simulator-infra created: 2026-05-10 updated_at: 2026-05-10 -blocked_by: [p2-68] -follow_ups: [p2-68] +blocked_by: [p2-68, p2-70, p2-71, p2-72] +follow_ups: [p2-68, p2-69, p2-70, p2-71, p2-72] --- ## Context @@ -995,6 +995,38 @@ autoload dependencies. (recommended id: `p2-68`). Will edit `blocked_by` once the follow-up objective is filed. +## 2026-05-12 — Updated path to "Claude vs production AI" demo + +Phase 11 shipped (TurnProcessor::step ticking). Phases 12 + 13 each hit a structural gap that is NOT a quick wedge: + +### Phase 12 — needs `mc-vision` crate (filed as p2-70) + +`mc_observation::ObservationStore` was the wrong tool — it's per-player climate observation history (temperature/moisture/wind), not per-tile visibility. The actual per-player visibility producer currently lives only in GDScript (`Vision.gd`). Rust has no `fn visible_tiles(state: &GameState, player: PlayerId) -> HashSet`. + +Filed as **p2-70 mc-vision (per-player visibility producer)**. Estimate ~1 day. + +### Phase 13 — needs two pieces + +**1. Bench projector enrichment** (filed as **p2-71**): + +The AI is correctly wired through `project_tactical → run_ai_turn → apply_ai_action`, but `decide_tactical_actions` returns empty action chains past turn 0 because the bench projector emits a degenerate `TacticalState` — empty `unit_catalog`, zero per-tile yields, no move-cost data (all p2-68 Wave 1 documented limitations). Until the projector serves a representative tactical surface, MCTS bottoms out on nothing-to-do. + +Honest correction recorded: last night's "AI inertness = bench doesn't tick" hypothesis was wrong (Wave 2's TurnProcessor wire proved Claude's slot ticks; AI inertness is a separate projector-fidelity issue). + +Estimate ~1-1.5 days. + +**2. `GdPlayerApi` → render bridge** (filed as **p2-72**): + +Production proof scenes (e.g. `world_map.tscn`, `gameplay_arc_proof`) render from the `GameState` *autoload*. `GdPlayerApi` keeps its own state internally via `load_state_json`. There is currently no path that visualises the `GdPlayerApi`-held world. Phase 13's "screenshots every 5 turns" requires either pointing the autoload at `GdPlayerApi`'s state (one source of truth) or a thin render adapter that reads `view_json` and drives a TileMap. + +Estimate ~0.5-1 day. + +### Sequencing + +- p2-70 (mc-vision) ↔ p2-71 (projector enrichment) are independent — can run in parallel. +- p2-72 (render bridge) is independent of both; can run anytime. +- p2-67 final close happens when all three land + a Claude-vs-AI run produces screenshots and an action log. + ## References - `src/simulator/crates/mc-core/src/action.rs` — unit action enum diff --git a/.project/objectives/p2-70-mc-vision-visibility-producer.md b/.project/objectives/p2-70-mc-vision-visibility-producer.md new file mode 100644 index 00000000..52953cc5 --- /dev/null +++ b/.project/objectives/p2-70-mc-vision-visibility-producer.md @@ -0,0 +1,122 @@ +--- +id: p2-70 +title: "mc-vision — per-player tile visibility producer (Rust)" +priority: p2 +status: open +scope: game1 +category: simulation +owner: simulator-infra +created: 2026-05-12 +updated_at: 2026-05-12 +blocked_by: [] +follow_ups: [p2-67] +--- + +## Context + +`p2-67 Phase 12` (real per-tile fog of war in the Claude Player API projection) blocked here. The original spec named `mc_observation::ObservationStore` as the source of visibility; it isn't — that store is per-player climate observation history (temperature/moisture/wind), not tile visibility. The per-player visibility producer currently lives only in GDScript (`src/game/engine/src/modules/vision/Vision.gd`). + +Rust has no `fn visible_tiles(state: &GameState, player: PlayerId) -> HashSet` or equivalent. + +Until this exists: +- `mc-player-api/src/projection.rs` falls back to strict per-player redaction (all-or-nothing), which is wrong for any non-omniscient view. +- AI projection (`project_tactical`) cannot meaningfully reason about fog. +- Headless playtesting can't validate fog-driven behaviour. + +## Source-of-truth rails + +- **New crate**: `src/simulator/crates/mc-vision/`. Lives between `mc-turn` (depends on `mc-core`) and downstream consumers (`mc-player-api`, eventually `mc-ai`). +- **JSON path**: none — vision radii are unit/building stats already in `public/games/age-of-dwarves/data/units/*.json` and `buildings/*.json`. +- **GDScript**: `Vision.gd` becomes the *reader* of an exported `GdVision::visible_tiles_for(player)` once this lands, not the *computer*. Deletion of the GDScript implementation is part of acceptance. + +## Surface + +### 1. Core API + +```rust +pub struct VisionState { + pub per_player: BTreeMap, +} + +pub struct PlayerVision { + /// Tiles currently visible (any source). + pub visible: HashSet, + /// Tiles ever seen (visible ∪ last-seen). + pub explored: HashSet, + /// Last-known state per explored-but-not-visible tile. + pub last_seen: BTreeMap, +} + +pub fn compute_vision(state: &GameState) -> VisionState; +pub fn refresh_for_player(state: &GameState, player: PlayerId, prior: Option<&PlayerVision>) -> PlayerVision; +``` + +### 2. Vision sources + +- Each owned `MapUnit` contributes a hex disk of radius `unit_catalog[unit_id].vision_range` centred on `(unit.col, unit.row)`. +- Each owned city contributes radius `city_vision_radius` (default 2; widens with watchtower etc.). +- Each owned building with `vision_bonus: N` extends its parent city's radius. +- Terrain blocking (forests, mountains, hills) reduces vision through the blocking tile per the existing GDScript rule. Port the rule verbatim from `Vision.gd::can_see(...)` with cited line numbers. + +### 3. Determinism + +- Stable across runs given identical `GameState`. +- No RNG. +- BTreeMap ordering preserved across save/load (`HashSet` serialised as sorted `Vec` on the wire). + +### 4. GDExtension surface + +```rust +#[godot_api] +impl GdVision { + #[func] + fn visible_tiles_for(&self, player: i64) -> Array; + #[func] + fn explored_tiles_for(&self, player: i64) -> Array; + #[func] + fn is_visible(&self, player: i64, col: i64, row: i64) -> bool; +} +``` + +### 5. mc-player-api projector integration + +Replace the strict-redaction code path in `mc-player-api/src/projection.rs` with `mc_vision::compute_vision(state).per_player[player].visible` for visibility checks. Fog/last-seen tiles surface as `TileView::FogOfWar(LastSeenTile)` rather than absent. + +### 6. GDScript deletion + +`src/game/engine/src/modules/vision/Vision.gd` is removed (or thinned to call-through to `GdVision`). Any caller of the old `Vision.compute(player)` migrated. + +## Acceptance + +- ☐ `mc-vision` crate exists with `compute_vision` + `refresh_for_player`. +- ☐ Unit-radius + city-radius + building-bonus sources all contribute. +- ☐ Terrain blocking rule ported verbatim from `Vision.gd` with file:line citation. +- ☐ Determinism test: same `GameState` → byte-identical `VisionState` across two runs. +- ☐ `GdVision` GDExtension class exposes the three methods. +- ☐ `mc-player-api/src/projection.rs` no longer uses strict redaction; uses `mc_vision`. +- ☐ Property test: player A's `visible` does not include tiles only adjacent to player B's units (assuming no shared-map agreement). +- ☐ Old `Vision.gd` deleted or thinned to a call-through wrapper. +- ☐ `cargo check --workspace && cargo test --workspace` green. + +## Why this size + +- API + core compute: ~3 hr. +- Terrain blocking port: ~2 hr. +- GDExtension wrapper: ~1 hr. +- Projector integration: ~1 hr. +- GDScript caller migration + deletion: ~1 hr. + +**Total: ~1 day.** + +## Unblocks + +- p2-67 Phase 12 (fog from real visibility). +- AI projection can reason about fog correctly (downstream nice-to-have). + +## References + +- `src/game/engine/src/modules/vision/Vision.gd` — GDScript reference implementation to port. +- `src/simulator/crates/mc-player-api/src/projection.rs` — strict-redaction call site to replace. +- `public/games/age-of-dwarves/data/units/*.json` — `vision_range` field. +- `public/games/age-of-dwarves/data/buildings/*.json` — `vision_bonus` field. +- `.project/objectives/p2-67-claude-player-api.md` (Phase 12 STOP, 2026-05-12). diff --git a/.project/objectives/p2-71-bench-projector-enrichment.md b/.project/objectives/p2-71-bench-projector-enrichment.md new file mode 100644 index 00000000..ecf10c53 --- /dev/null +++ b/.project/objectives/p2-71-bench-projector-enrichment.md @@ -0,0 +1,97 @@ +--- +id: p2-71 +title: "Bench projector enrichment — make MCTS see a real tactical surface" +priority: p2 +status: open +scope: game1 +category: simulation +owner: simulator-infra +created: 2026-05-12 +updated_at: 2026-05-12 +blocked_by: [] +follow_ups: [p2-67] +--- + +## Context + +`p2-67 Phase 13` blocked partly here. The AI is correctly wired through `project_tactical → run_ai_turn → apply_ai_action` (p2-68 Waves 1+3+4 + p2-69), and Phase 11's `TurnProcessor::step` proves Claude's slot ticks per-turn — but the production AI returns *empty* action chains past turn 0. + +Root cause: `mc-player-api/src/projection.rs::project_tactical` was written as a v1 minimal projector and deliberately omitted several fields with `#[serde(default)]`-tolerant fallbacks. `decide_tactical_actions` bottoms out on: + +- `TacticalState.unit_catalog` empty → no legal unit-build choices. +- `TacticalState.building_catalog` empty → no legal building-queue choices. +- Per-tile yields zero → city placement scoring uniform. +- No move-cost data → unit moves have no cost signal. +- `strategic_axes` / personality scoring tables empty → MCTS prior is uniform. + +The MCTS isn't broken; it's correctly returning "no productive action" because the projection it sees has nothing productive to do. + +## Source-of-truth rails + +- **Rust crate**: edit `mc-player-api/src/projection.rs` and (if needed) thread catalog handles through `dispatch::apply_end_turn`. Catalogs already exist in `mc-units::UnitsCatalog` (p2-67 Phase 9) and need a sibling in `mc-buildings`. +- **JSON path**: none — projector reads existing `public/games/age-of-dwarves/data/{units,buildings}/*.json` via the loaders. +- **GDScript**: harness wiring only — pass catalog handles into `GdPlayerApi` at boot. + +## Surface + +### 1. Catalog plumbing + +- `UnitsCatalog` already loaded in `claude_player_main.gd` for `MapUnit::new(...)`. Pass it through `GdPlayerApi::new(...)` so `project_tactical` can read it. +- Add `BuildingsCatalog` (mirror `UnitsCatalog` pattern). Load once at harness boot. +- Optional: `TerrainCatalog` for per-tile yield lookups. + +### 2. Projector enrichment + +In `project_tactical`: + +- Populate `tactical.unit_catalog` from `UnitsCatalog` — convert each `UnitDef` to the `TacticalUnitDef` shape `mc-ai` expects (cost, moves, attack, defense, prerequisites). +- Populate `tactical.building_catalog` from `BuildingsCatalog`. +- Per-tile yields: for each `tactical.tiles[i]`, set `food/production/gold/science/culture` from the `TerrainCatalog` × current improvements/biome lookup. Mirror the formula used in `mc-city::tile_yield::compute_yield`. +- Populate `strategic_axes` from `ScoringWeights` (already set per-player via p2-67 Wave 1 `set_player_personality_json`). +- Populate `promotion_*_weight` / `difficulty_threshold_mult` from the personality table. + +### 3. Smoke verification + +After enrichment, re-run the 3-player 5-EndTurn smoke. Acceptance: AI slots emit `actions_applied > 0` on turn 1+ (not just turn 0), with action variants varying by personality (blackhammer aggressive, deepforge defensive, etc.). + +### 4. Test coverage + +- Unit test: `project_tactical` populates `unit_catalog.len() > 0`. +- Unit test: per-tile yields non-zero for at least one non-ocean tile. +- Integration test: 5-EndTurn driven game produces a non-empty AI action chain on each turn for each AI slot. + +## Acceptance + +- ☐ `mc-player-api::projection::project_tactical` populates `unit_catalog`, `building_catalog`, per-tile yields, `strategic_axes`, personality weights. +- ☐ `BuildingsCatalog` exists (or `UnitsCatalog`'s pattern is reused). +- ☐ `GdPlayerApi` accepts catalog handles at construction. +- ☐ 5-EndTurn smoke shows `actions_applied > 0` for AI slots on turns 1-5. +- ☐ AI action variants differ by personality (sample: blackhammer vs deepforge produce non-identical chains). +- ☐ Unit + integration tests prove the projector enrichment. +- ☐ `cargo test -p mc-player-api && cargo test -p mc-ai` green. +- ☐ p2-68 acceptance bullet "smoke-non-trivial-AI-chains" flips from ⚠ to ✓. + +## Why this size + +- BuildingsCatalog: ~2 hr (mirror UnitsCatalog). +- Catalog plumbing through GdPlayerApi: ~2 hr. +- Projector enrichment: ~3 hr (walk each field, port lookup). +- Tile yield port: ~2 hr (compute_yield mirror). +- Tests + smoke verification: ~2 hr. + +**Total: ~1-1.5 days.** + +## Unblocks + +- p2-67 Phase 13 (demo will have actual AI gameplay to screenshot). +- p2-68 smoke acceptance bullet flips ✓ → p2-68 status `done`. + +## References + +- `src/simulator/crates/mc-player-api/src/projection.rs::project_tactical` — current minimal projector. +- `src/simulator/crates/mc-ai/src/tactical/mod.rs::TacticalState` — target shape. +- `src/simulator/crates/mc-units/src/catalog.rs` — UnitsCatalog precedent (p2-67 Phase 9). +- `src/simulator/crates/mc-city/src/tile_yield.rs` — yield formula source of truth. +- `public/games/age-of-dwarves/data/ai_personalities.json` — personality scoring tables. +- `.project/objectives/p2-67-claude-player-api.md` (Phase 13 STOP, 2026-05-12). +- `.project/objectives/p2-68-mc-ai-headless-turn-driver.md` (Wave 1 projector limitations). diff --git a/.project/objectives/p2-72-gdplayerapi-render-bridge.md b/.project/objectives/p2-72-gdplayerapi-render-bridge.md new file mode 100644 index 00000000..34206956 --- /dev/null +++ b/.project/objectives/p2-72-gdplayerapi-render-bridge.md @@ -0,0 +1,117 @@ +--- +id: p2-72 +title: "GdPlayerApi → render bridge (visualise the API-held game world)" +priority: p2 +status: open +scope: game1 +category: tooling +owner: simulator-infra +created: 2026-05-12 +updated_at: 2026-05-12 +blocked_by: [] +follow_ups: [p2-67] +--- + +## Context + +`p2-67 Phase 13` (Claude-vs-AI demo with screenshots every 5 turns) blocked partly here. + +Production proof scenes (`world_map.tscn`, `src/game/engine/scenes/tests/gameplay_arc_proof.tscn`, etc.) render from the **`GameState` autoload** (`src/game/engine/src/autoloads/GameState.gd`). Their TileMap layers, unit sprites, city overlays, and fog mask all read directly from `GameState.grid`, `GameState.players[*].units`, etc. + +`GdPlayerApi` keeps its **own** state — internally constructed from `load_state_json(...)`, mutated by `apply_action_json(...)`. The headless harness (`claude_player_main.gd`) deliberately does NOT touch the `GameState` autoload after `_bootstrap_game` (which only initialises the autoload to keep theme/data loaders happy). + +Result: there is no path that visualises the world `GdPlayerApi` is actually running. Phase 13 needs screenshots of the same world Claude is acting in, not a parallel scripted-arc world. + +## Source-of-truth rails + +- **Rust crate**: edit `api-gdext` to add a one-way write-through path from `GdPlayerApi` state → `GdGameState` (the autoload's underlying Rust state). +- **JSON path**: none. +- **GDScript**: thin caller — the harness asks `GdPlayerApi` for its current state once per turn and calls `GameState.set_from_gdgamestate(handle)` to refresh the autoload before screenshot capture. + +## Design options + locked decision + +### Option A: One source of truth — `GameState` autoload IS `GdPlayerApi`'s state + +`GdPlayerApi::load_state_json` writes through to the `GdGameState` autoload (the same one the proof scenes render from). `apply_action_json` mutates the autoload's `GdGameState`. The proof scenes Just Work because there's only one state. + +Pros: maximally DRY, one source of truth, screenshots trivially correct. +Cons: requires `GdPlayerApi` to hold a `Gd` reference to the autoload — coupling. Tests that construct ephemeral `GdPlayerApi` instances need a per-test `GdGameState`. + +### Option B: Two states + refresh bridge + +`GdPlayerApi` keeps its own state; provide `GdPlayerApi::write_state_to(gd_game_state: Gd)` that copies into the autoload on demand. Harness calls this once per turn before screenshot capture. + +Pros: less coupling, ephemeral `GdPlayerApi` for tests stays simple. +Cons: dual state, drift risk (e.g. if any non-write_state_to caller mutates the autoload between turns). + +### Locked decision: **Option A** (SOLID/DRY) + +The Claude API path and the rendered path *are* the same game. Modeling them as two states violates the single-source-of-truth invariant. Tests that need ephemeral instances can construct a throw-away `GdGameState` per test. + +## Surface + +### 1. `GdPlayerApi` holds `Gd` + +```rust +#[derive(GodotClass)] +#[class(base = RefCounted)] +struct GdPlayerApi { + state: Gd, + omniscient: bool, +} +``` + +### 2. `load_state_json` writes to that handle + +Parses the JSON into a `GameState` and calls `self.state.bind_mut().set_state(parsed)`. + +### 3. `apply_action_json` and `view_json` route through the handle + +Same. No internal `GameState` field separate from the autoload. + +### 4. Harness wiring + +`claude_player_main.gd::_ready()` — instead of `ClassDB.instantiate("GdPlayerApi")` cold, hand it the `GameState` autoload's underlying `GdGameState` reference. + +### 5. Per-turn render refresh + +The harness already calls `view_json` after each EndTurn. Add a post-action hook that signals the render pipeline (or just relies on Option A: the autoload mutated, the scene re-renders next frame). + +### 6. Screenshot capture path + +`claude_player_main.gd` adds a `_capture_screenshot(turn: int)` helper invoked every 5 turns. Reuses `tools/screenshot.sh` pattern — render `world_map.tscn` against the now-fresh autoload state. + +## Acceptance + +- ☐ `GdPlayerApi` holds `Gd` instead of an internal `GameState`. +- ☐ `load_state_json` writes through to the held handle. +- ☐ `apply_action_json` mutates the held handle. +- ☐ Existing `mc-player-api` test crate-internal tests still pass. +- ☐ Existing GDScript callers of `GdPlayerApi` work unchanged. +- ☐ Harness boots with the autoload's `GdGameState` reference. +- ☐ A proof scene rendered against the autoload after `apply_action_json` shows the mutated state. +- ☐ 25-turn driven game produces 6 screenshots (turns 0, 5, 10, 15, 20, 25) showing real game state evolution. +- ☐ `cargo check --workspace && cargo test --workspace` green. + +## Why this size + +- `GdPlayerApi` refactor to hold `Gd`: ~2 hr. +- Test fixture migration (ephemeral `GdGameState` per test): ~1 hr. +- Harness wiring: ~1 hr. +- Screenshot pipeline integration: ~2 hr. +- Verification (rendered proof of mutation): ~2 hr. + +**Total: ~0.5-1 day.** + +## Unblocks + +- p2-67 Phase 13 (real screenshots of the Claude vs AI game). + +## References + +- `src/simulator/api-gdext/src/lib.rs::GdPlayerApi` — current internal-state shape. +- `src/simulator/api-gdext/src/lib.rs::GdGameState` — autoload's underlying Rust type. +- `src/game/engine/src/autoloads/GameState.gd` — autoload wrapper. +- `src/game/engine/scenes/headless/claude_player_main.gd` — harness wiring. +- `src/game/engine/scenes/tests/gameplay_arc_proof.tscn` — render-from-autoload precedent. +- `.project/objectives/p2-67-claude-player-api.md` (Phase 13 STOP, 2026-05-12).