feat(@projects/@magic-civilization): ✨ add visibility & render dependencies
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
3ef338725e
commit
f8affde13a
4 changed files with 370 additions and 2 deletions
|
|
@ -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<HexCoord>`.
|
||||
|
||||
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
|
||||
|
|
|
|||
122
.project/objectives/p2-70-mc-vision-visibility-producer.md
Normal file
122
.project/objectives/p2-70-mc-vision-visibility-producer.md
Normal file
|
|
@ -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<HexCoord>` 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<PlayerId, PlayerVision>,
|
||||
}
|
||||
|
||||
pub struct PlayerVision {
|
||||
/// Tiles currently visible (any source).
|
||||
pub visible: HashSet<HexCoord>,
|
||||
/// Tiles ever seen (visible ∪ last-seen).
|
||||
pub explored: HashSet<HexCoord>,
|
||||
/// Last-known state per explored-but-not-visible tile.
|
||||
pub last_seen: BTreeMap<HexCoord, LastSeenTile>,
|
||||
}
|
||||
|
||||
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<HexCoord>` on the wire).
|
||||
|
||||
### 4. GDExtension surface
|
||||
|
||||
```rust
|
||||
#[godot_api]
|
||||
impl GdVision {
|
||||
#[func]
|
||||
fn visible_tiles_for(&self, player: i64) -> Array<Vector2i>;
|
||||
#[func]
|
||||
fn explored_tiles_for(&self, player: i64) -> Array<Vector2i>;
|
||||
#[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).
|
||||
97
.project/objectives/p2-71-bench-projector-enrichment.md
Normal file
97
.project/objectives/p2-71-bench-projector-enrichment.md
Normal file
|
|
@ -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).
|
||||
117
.project/objectives/p2-72-gdplayerapi-render-bridge.md
Normal file
117
.project/objectives/p2-72-gdplayerapi-render-bridge.md
Normal file
|
|
@ -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<GdGameState>` 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<GdGameState>)` 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<GdGameState>`
|
||||
|
||||
```rust
|
||||
#[derive(GodotClass)]
|
||||
#[class(base = RefCounted)]
|
||||
struct GdPlayerApi {
|
||||
state: Gd<GdGameState>,
|
||||
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<GdGameState>` 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<GdGameState>`: ~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).
|
||||
Loading…
Add table
Reference in a new issue