From b4c402e76690ada6b8300b176206f8c814f7fd04 Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 27 Jun 2026 09:44:11 -0400 Subject: [PATCH] =?UTF-8?q?docs(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=85=20p3-26=20Gap=203=20DONE=20(equipment/crafting=20veri?= =?UTF-8?q?fied=20headless)=20+=20Gap=204=20scope=20assessment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gap 3 — Equipment/crafting: verified the full craft→equip→combat path runs headless and Rust-authoritative (orig bullet was stale at [ ]): - PlayerAction::CraftEquipment → craft_equipment dispatch (materials gate + consume strategic_ledger + equip), 2 tests - recipe_phase ("recipe_refine") in END_OF_TURN_PHASES — passive crafting economy refines raw→quality-tiered product every self-play turn, 1 test - equip_combat_bonus reads boot-loaded item_combat at every combat site, 2 tests - boot path: set_item_combat_json FFI ← headless harness _apply_item_combat - MCTS AI not electing to craft = deliberate 9-kind GPU-rollout constraint, not a missing system Verified green: mc-turn + mc-player-api 557/0. Gap 4 — Per-building queues: recorded verified assessment. Bench single-slot + per-turn AI reselection is functionally equivalent to a FIFO build queue for the self-play SIMULATION outcome; the multi-item queue is a live-game UI affordance belonging to the p3-25/p3-29 projection arc. Owner scope call pending: does p3-26 require simulating a multi-item queue, or reclassify Gap 4 out of the headless bar. Co-Authored-By: Claude Opus 4.8 --- .mcp.json | 10 ++- .project/objectives/DASHBOARD_CATEGORIES.md | 2 + .project/objectives/README.md | 5 +- .project/objectives/objectives.json | 42 +++++++++- .../p3-26-complete-headless-simulator.md | 42 +++++++++- .../p3-28-modular-turn-architecture.md | 43 +++++++++- .../objectives/p3-31-replay-live-recording.md | 83 +++++++++++++++++++ .../p3-32-replay-visual-map-rendering.md | 78 +++++++++++++++++ .../games/age-of-dwarves/data/objectives.json | 36 ++++++-- 9 files changed, 317 insertions(+), 24 deletions(-) create mode 100644 .project/objectives/p3-31-replay-live-recording.md create mode 100644 .project/objectives/p3-32-replay-visual-map-rendering.md diff --git a/.mcp.json b/.mcp.json index 0d0967a0..c5784d9e 100644 --- a/.mcp.json +++ b/.mcp.json @@ -2,11 +2,17 @@ "mcpServers": { "objectives": { "command": "/opt/homebrew/bin/mcp-objectives", - "args": [] + "args": [], + "env": { + "PROJECT_ROOT": "/Users/natalie/Code/@projects/@magic-civilization" + } }, "experts": { "command": "/opt/homebrew/bin/mcp-experts", - "args": [] + "args": [], + "env": { + "PROJECT_ROOT": "/Users/natalie/Code/@projects/@magic-civilization" + } }, "magic-civ": { "command": "node", diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 505ccf47..545bf3d4 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -536,4 +536,6 @@ | [p3-28](p3-28-modular-turn-architecture.md) | 🟡 partial | P3 | Modular turn architecture — break dep cycle, phase registry, boot-config DRY | [warcouncil](../team-leads/warcouncil.md) | 🟢 | | [p3-29](p3-29-rail1-turn-unification.md) | 🟡 partial | P3 | Rail-1 turn unification — live game calls the Rust turn, delete GDScript orchestration | [warcouncil](../team-leads/warcouncil.md) | 🟢 | | [p3-30](p3-30-wild-creature-ai-rust-port.md) | 🟡 partial | P3 | Port wild-creature AI from GDScript to Rust (Rail-1 compliance) | [warcouncil](../team-leads/warcouncil.md) | 🟢 | +| [p3-31](p3-31-replay-live-recording.md) | ❌ missing | P3 | Replay recording — live games archive a GameHistory (per-turn TurnSnapshot + events) on game-over | [shipwright](../team-leads/shipwright.md) | 🟢 | +| [p3-32](p3-32-replay-visual-map-rendering.md) | ❌ missing | P3 | Replay rendering — visual map playback (terrain + city/unit markers) from the archive | [shipwright](../team-leads/shipwright.md) | 🔒 p3-31 | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index b14efb98..fb40fa5b 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -17,8 +17,8 @@ | **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 | | **P1** | 0 | 0 | 0 | 0 | 1 | 88 | 89 | | **P2** | 0 | 0 | 0 | 0 | 1 | 132 | 133 | -| **P3 (oos)** | 0 | 7 | 0 | 0 | 29 | 34 | 70 | -| **total** | **0** | **7** | **0** | **0** | **31** | **298** | **336** | +| **P3 (oos)** | 0 | 7 | 0 | 2 | 29 | 34 | 72 | +| **total** | **0** | **7** | **0** | **2** | **31** | **298** | **338** | @@ -27,6 +27,7 @@ | Team Lead | Remaining | |---|---| | [warcouncil](../team-leads/warcouncil.md) | 7 | +| [shipwright](../team-leads/shipwright.md) | 2 | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index 7b0c77ca..fde12248 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,13 +1,13 @@ { - "generated_at": "2026-06-27T11:35:58Z", + "generated_at": "2026-06-27T13:22:12Z", "totals": { "done": 298, "in_progress": 0, "partial": 7, "stub": 0, - "missing": 0, + "missing": 2, "oos": 31, - "total": 336 + "total": 338 }, "objectives": [ { @@ -3707,7 +3707,7 @@ "status": "partial", "scope": "game1", "owner": "warcouncil", - "updated_at": "2026-06-26", + "updated_at": "2026-06-27", "blocked_by": [], "summary": "The per-subsystem sprawl noticed while porting climate/events/happiness/healing/ecology\nrevealed three SOLID/DRY/DIP debts. \"Foundation first\" tackled the layering + phase pieces." }, @@ -3732,6 +3732,30 @@ "updated_at": "2026-06-27", "blocked_by": [], "summary": "**Rail-1 gap surfaced during the p3-29 logic sweep (2026-06-27).** The live turn runs\nwild-creature AI **decision logic in GDScript**: `turn_processor.gd::_process_wild_creatures`\n(line 459) calls `wild_ai.process_wild_turn(game_map)` →\n`src/game/engine/src/modules/ai/wild_creature_ai.gd` (302 LOC — a guard / attack / roam state\nmachine over `owner == -1` units).\n\nThis is sim logic, not presentation, so it violates Rail-1 (\"GDScript is presentation only\";\n\"AI decision-making lives in Rust\"). It is **distinct from [[p0-26-ai-tactical-rust-port]]**,\nwhich ported *player* tactical AI (`simple_heuristic_ai.gd` / `ai_tactical.gd` / `ai_military.gd`)\nand explicitly did not touch wild-creature behaviour. It is also distinct from the fauna\n*population/rendering/stats* objectives (`g2-08`, `p3-12`, `p1-49`, `p2-58a`) — those model\necology; this is the per-creature **combat behaviour AI**.\n\nThe combat *resolution* for wilds already lives in Rust (`mc-combat::wilds`); only the\n*decision* layer (who to attack, when to roam, leash enforcement) is still GDScript." + }, + { + "id": "p3-31", + "title": "Replay recording — live games archive a GameHistory (per-turn TurnSnapshot + events) on game-over", + "priority": "p3", + "status": "missing", + "scope": "game1-stretch", + "owner": "shipwright", + "updated_at": "2026-06-27", + "blocked_by": [], + "summary": "" + }, + { + "id": "p3-32", + "title": "Replay rendering — visual map playback (terrain + city/unit markers) from the archive", + "priority": "p3", + "status": "missing", + "scope": "game1-stretch", + "owner": "shipwright", + "updated_at": "2026-06-27", + "blocked_by": [ + "p3-31" + ], + "summary": "" } ], "blocked": [ @@ -3976,12 +4000,22 @@ "blockedBy": [ "p0-36" ] + }, + { + "id": "p3-32", + "blockedBy": [ + "p3-31" + ] } ], "remaining_by_lead": [ { "owner": "warcouncil", "remaining": 7 + }, + { + "owner": "shipwright", + "remaining": 2 } ] } diff --git a/.project/objectives/p3-26-complete-headless-simulator.md b/.project/objectives/p3-26-complete-headless-simulator.md index b6be3b7b..78ad6537 100644 --- a/.project/objectives/p3-26-complete-headless-simulator.md +++ b/.project/objectives/p3-26-complete-headless-simulator.md @@ -48,14 +48,48 @@ expansion, tech/science, fauna encounters, combat/siege, diplomacy. Verified liv terrain effects headless; `process_events` returns the fired list but `step()` discards it (`let _fired =`, processor.rs:1117). The SYSTEM runs in self-play; only replay/observation visibility is missing. Low priority (parallels the p3-29 §A event surface, render-gated payoff). -- [ ] **Gap 3 — Equipment / crafting.** `mc-city/recipes.rs` + `enqueue_item` exist but - there is no headless `Craft`/`Equip` `PlayerAction` and crafting isn't in the bench - turn. Add the action(s) + dispatch + wire recipe resolution (gating resources, quality) - into the headless production/turn so equipment is craftable in self-play. +- [x] **Gap 3 — Equipment / crafting — DONE** ✅ (verified 2026-06-27; orig bullet stale). + The full craft→equip→combat path runs headless and Rust-authoritative: + - **Action + dispatch:** `PlayerAction::CraftEquipment { unit_id, item_id }` + ([action.rs:195](../../src/simulator/crates/mc-player-api/src/action.rs)) → `craft_equipment` + ([dispatch.rs:1521](../../src/simulator/crates/mc-player-api/src/dispatch.rs)) checks all + materials against `strategic_ledger`, consumes them (saturating), pushes `mc_items::EquippedItem` + onto the unit. 2 tests: `craft_equipment_consumes_materials_and_equips`, + `craft_equipment_rejects_insufficient_materials` (dispatch.rs:2446/2462). + - **Recipe resolution in the turn:** `recipe_phase::process_recipe_phase` ("recipe_refine") sits in + `sim_phases::END_OF_TURN_PHASES` ([sim_phases.rs:31](../../src/simulator/crates/mc-turn/src/sim_phases.rs)) — + every self-play turn refines raw→product (quality-tiered) via `mc_city::recipes::tick_recipes` + against each player's stockpile. Test `recipe_phase_refines_resources` (recipe_phase.rs:70). + - **Equipment affects combat:** `equip_combat_bonus` sums equipped-item atk/def from the boot-loaded + `item_combat` table at every combat site ([processor.rs:56/2934/3771](../../src/simulator/crates/mc-turn/src/processor.rs)); + 2 tests (processor.rs:5496/5506). + - **Boot path:** `item_combat` loaded into headless via `set_item_combat_json` FFI + ([player_api.rs:208](../../src/simulator/api-gdext/src/player_api.rs)) from the headless harness + `_apply_item_combat` ([player_api_main.gd:255](../../src/game/engine/scenes/headless/player_api_main.gd)). + - **NON-gap (deliberate):** the MCTS rollout policy exposes 9 GPU-rollout-legal `ActionKind`s + (policy.rs:79-87) — `CraftEquipment` is a deterministic player action, not a strategic rollout + choice, so the self-play AI not *electing* to craft is the same intentional constraint as the + GPU policy surface, not a missing system. Recipe refinement (the passive crafting economy) DOES + run autonomously every self-play turn. Verified green: mc-turn + mc-player-api 557/0. - [ ] **Gap 4 — Per-building build queues.** Bench `CityState` has a single `queue`; per-building queues live in the full `mc_city::City` (live game). This is the dual city-model split ([[p3-25 ...]] step 6 / city_slot.rs). Either give `CityState` per-building queues or unify the models so the headless turn simulates them. + - **Assessment (2026-06-27) — likely PARITY-not-gap for the self-play criterion; owner scope call pending.** + Verified: bench `CityState.queue` is `Option` (single in-progress slot, + [mc-city/src/lib.rs:138](../../src/simulator/crates/mc-city/src/lib.rs)); `process_city_production` + completes it then clears to `None` (processor.rs:1604/1615); the bench AI sets exactly one item via + `apply_queue_production`. The live `construction_queue` is a FIFO **list** + ([city.gd:74](../../src/game/engine/src/entities/city.gd)) — but a FIFO of buildings is functionally + *select-the-next-item-when-the-current-completes*, which single-slot + per-turn AI reselection already + achieves; the **simulation outcome** (what gets built, in what order) is identical. The multi-item + queue is a player-UX affordance (batch decisions ahead of time), not a self-play simulation behavior. + So for p3-26's bar ("a full self-play game with ALL **systems**") this is arguably already met. The + genuine multi-queue/per-building-item surface belongs to the live-game **projection** arc + ([[p3-25-unify-dual-city-model]] / [[p3-29-rail1-turn-unification]]), where the UI reads `getState()`. + **Owner decision needed:** does p3-26 require the bench to *simulate* a multi-item queue, or is + single-slot+reselection the accepted headless model (Gap 4 reclassified out of p3-26 into the p3-25/29 + projection arc)? ## Notes diff --git a/.project/objectives/p3-28-modular-turn-architecture.md b/.project/objectives/p3-28-modular-turn-architecture.md index dc15bca3..b2cf0c3d 100644 --- a/.project/objectives/p3-28-modular-turn-architecture.md +++ b/.project/objectives/p3-28-modular-turn-architecture.md @@ -5,7 +5,7 @@ priority: p3 scope: game1 owner: warcouncil status: partial -updated_at: 2026-06-26 +updated_at: 2026-06-27 --- ## Summary @@ -24,9 +24,20 @@ revealed three SOLID/DRY/DIP debts. "Foundation first" tackled the layering + ph living world = one registry line + a phase module, no step() edit. - [ ] **Boot-config DRY (Opportunity A)** — collapse the 6 `set_*_json` FFI setters + 9 `_apply_*` GDScript harness loaders + N `#[serde(skip)]` GameState fields into one - Rust-native `boot_from_resources(path)` reading `public/resources/*` directly (Rust already - does this in tests). Removes GDScript-shuttling-data (Rail-3 smell); single source of truth - = the JSON files. NOT YET DONE (was the separate option 2). + Rust-native content loader. Removes GDScript-shuttling-data (Rail-3 smell); single source of + truth = the JSON files. NOT YET DONE (was the separate option 2). + **⚠ WASM constraint (verified 2026-06-27) — a file-reading `boot_from_resources(path)` is + wrong:** shared simulation code compiles to WASM too, and the web guide has **no filesystem**. + The loader must be a `ContentRegistry` populated by the *host* at engine init — Godot passes + `res://` bytes across FFI, the web guide fetches packs → bytes across bindgen — with + `include_str!` surviving **only** as the headless/test fallback. So the target is a registry + fed by injected bytes, not a path-reading function. +- [ ] **Dedup the ad-hoc `include_str!` content sites (Opportunity A, same arc)** — verified + 2026-06-27: **8 JSON-config `include_str!` sites** across simulator crates, each rolling its + own `OnceLock` + a fragile relative path (6 at `../../../../../`). `treaty_rules.json` is + embedded **3 separate times** (no dedup). These fold into the `ContentRegistry` above — + `registry.get::("promotions")` replaces per-crate path+parse. (WGSL shader `include_str!`s + are correct to embed and stay.) - [ ] **Widen the registry** — optionally fold climate (convert the method to a free fn) + happiness into the registry / a positioned-phase model so the whole turn sequence is data. @@ -36,3 +47,27 @@ Created 2026-06-26. The registry currently covers the contiguous end-of-turn wor (ecology, healing); climate stays a TurnProcessor method (shares climate helpers) and happiness stays positioned post-economy — both deliberately, to avoid risky reordering. The big remaining win is the boot-config DRY (3 layers → 1 Rust loader). + +### Content-loading audit (2026-06-27) — the two-path divergence the registry must kill + +Why Opportunity A matters beyond DRY: content currently reaches the simulation **two different +ways**, and they silently drift apart. +- **In-game (Godot drives):** GDScript `DataLoader` reads packs at runtime and passes overrides + across FFI (`mc-player-api/src/projection.rs:41` — *"overrides from DataLoader for the in-game + path"*). +- **Headless (Rust drives alone):** no DataLoader, so Rust falls back to its compile-time + `include_str!` copy (`mc-player-api/src/dispatch.rs:410` — *"build-time copy for this headless + path (no GDScript DataLoader)"*). + +When one side is edited and the other isn't, the two paths disagree. **Instance #1 (fixed this +session):** `mc-combat` promotion XP hardcoded `XP_THRESHOLDS = [10,30,60,100,…]` + 50% heal, +while the canonical `public/resources/promotions/promotions.json` said `[15,30,45,60]` + 30% +heal. This was a Rail-2 violation (Rust hardcoding game content) AND a two-path divergence. +Fixed by routing `mc-combat::{check_promotion,heal_on_promote,max_promotion_level,xp_threshold}` +through a `OnceLock`+`include_str!` config loaded from `promotions.json` (owner confirmed the JSON +values are canonical — the 4 thresholds match the 4-level promotion trees; the Rust 8-level curve +had 4 phantom levels). `cargo test -p mc-combat -p mc-turn -p mc-player-api` green. + +That fix is the per-file pattern applied once; the `ContentRegistry` (Opportunity A) is the +structural fix that makes the divergence **impossible** — both hosts feed one registry, so there +is no second copy to drift. diff --git a/.project/objectives/p3-31-replay-live-recording.md b/.project/objectives/p3-31-replay-live-recording.md new file mode 100644 index 00000000..1d17d40c --- /dev/null +++ b/.project/objectives/p3-31-replay-live-recording.md @@ -0,0 +1,83 @@ +--- +id: p3-31 +title: "Replay recording — live games archive a GameHistory (per-turn TurnSnapshot + events) on game-over" +priority: p3 +status: missing +scope: game1-stretch +category: simulation +owner: shipwright +created: 2026-06-27 +updated_at: 2026-06-27 +blocked_by: [] +follow_ups: [p3-32] +related: [p2-46, mc-replay-followup-unit-spawn-events] +--- + +## Context + +The replay surface (`p2-46`) and the projected match-chronicle (standings ladder ++ event feed via `GameHistory::standings_at`, shipped 2026-06-26) both work — but +**only against synthetic fixtures**. No real game is ever archived, so there is +nothing real to replay. + +Verified state (2026-06-27): + +- `write_game` is called **only** from `GdReplayArchive::write_fixture` + (`src/simulator/api-gdext/src/replay.rs`). No production path archives a game. +- `TurnSnapshot` is pushed **only** in `write_fixture` (`replay.rs:637`). The live + game never records a per-turn snapshot — `mc-replay/src/snapshot.rs` still + documents itself as "the schema, not the writer." +- `TurnEvent`s **are** emitted in `mc-turn` during headless turn resolution + (`p2-46` + `mc-replay-followup-unit-spawn-events`: CityFounded/CityCaptured/ + UnitKilled/UnitCreated/WonderBuilt/TechResearched land in `TurnResult`), but the + **live Godot turn loop** (`turn_manager.gd` → `AiTurnBridge`) is a separate path; + whether it populates a `TurnEventCollector` and where those events would be + archived is unwired. + +The goal: a finished game — including an `OBSERVER` cast or an `AI_ARENA` match — +produces a `GameHistory` on disk so it can be re-watched and compared. + +## Source-of-truth rails + +- **Rust (`mc-replay` + the recorder)** owns: `TurnSnapshot` derivation per clan + per turn (population / cities / army_strength / gold / tech_count / land_area / + score — already computed by the score + economy crates), event collection into + `GameHistory.events`, and `write_game` on game-over. No stats math or archive + I/O in GDScript (Rail-1). +- **GDScript** only: fires the game-over trigger, hands the recorder the + per-turn hook, and reads the archive back via the existing `GdReplayArchive` / + `GdReplayPlayer` bridges (Rail-3). +- **JSON**: archive-root path stays in `EnvConfig`; retention policy in Settings + (both already exist from `p2-46`). No new tunables hardcoded (Rail-2). + +## Acceptance bullets + +- [ ] A `GdGameRecorder` (or equivalent) holds a session `GameHistory` and, on a + per-turn hook, appends one `TurnSnapshot` **per clan** with the live stat + values. After an N-turn headless `OBSERVER`/`AI_ARENA` game, `history.snapshots` + has `N × clans` rows. Projection (`standings_at`) is unchanged. +- [ ] The live turn path collects the same `TurnEvent`s the headless `mc-turn` + path already emits (CityFounded/CityCaptured/UnitKilled/UnitCreated/WonderBuilt/ + TechResearched/WarDeclared/…) into the session `GameHistory.events`. (First + determine whether `turn_manager.gd`'s path already drains a collector; wire it + if not.) +- [ ] `write_game` is invoked on game-over (victory **and** turn-limit), writing + `meta.json` + `history.bin` under `///`. + `GdReplayArchive.list()` returns the just-finished game; `GameState`'s + `last_archived_game_id` is set for the end-game summary. +- [ ] `end_game_summary.gd` "Watch Replay" and `past_games.gd` open the + freshly-recorded game and the chronicle (standings ladder + event feed) renders + the **real** match, not a fixture. Proof screenshot reviewed in conversation. +- [ ] Rust round-trip test: build a multi-turn `GameHistory` via the recorder, + `write_game` → `read_game`, assert snapshots + events survive and + `standings_at` returns the recorded ladder. `cargo test -p mc-replay` green. +- [ ] Determinism: same seed + same controller ids → byte-identical recorded + snapshots + event-count vectors across two runs. + +## Out of scope + +- Visual map/unit replay rendering — tracked by `p3-32` (this objective only + makes real games *recordable*; that one makes them *watchable on a map*). +- Cloud sync of archives; replay branching / mid-game forking. +- Per-turn delta compression beyond bincode (the `p2-46` 10 MB cap assert still + governs; optimise only if it fires). diff --git a/.project/objectives/p3-32-replay-visual-map-rendering.md b/.project/objectives/p3-32-replay-visual-map-rendering.md new file mode 100644 index 00000000..36db655e --- /dev/null +++ b/.project/objectives/p3-32-replay-visual-map-rendering.md @@ -0,0 +1,78 @@ +--- +id: p3-32 +title: "Replay rendering — visual map playback (terrain + city/unit markers) from the archive" +priority: p3 +status: missing +scope: game1-stretch +category: presentation +owner: shipwright +created: 2026-06-27 +updated_at: 2026-06-27 +blocked_by: [p3-31] +follow_ups: [] +related: [p2-46, p3-31] +--- + +## Context + +The replay viewer currently renders a **match chronicle** — the standings ladder +(from `GameHistory::standings_at`) + a cumulative event feed — but not the map +itself. `replay_viewer.gd` documents this as deliberate: the archive carries no +per-turn geometry, so a faithful map replay needs more recorded data. + +Verified state (2026-06-27): + +- The archive stores **no terrain** — only `GameHistory.seed` + a `MapDescriptor` + (`kind`, `width`, `height`). Terrain is *reproducible* by re-running worldgen + from the seed (deterministic), not stored. +- **No `UnitMoved` event** exists in the `TurnEvent` enum + (`mc-replay/src/event.rs` — only a doc-comment mention). Units therefore cannot + be tracked turn-by-turn from the event stream. +- However `UnitCreated`, `UnitKilled`, and `UnitCaptured` **do** carry a hex + (`col`/`row`), and `CityFounded`/`CityCaptured` carry the city hex + owner. So + *spawn / death / capture positions* and *all city positions* are already + recoverable from events. + +This objective renders what the recorded data supports: a regenerated terrain map +with city markers and coarse unit pings, with continuous unit motion as a gated +stretch that requires a new event. + +## Source-of-truth rails + +- **Rust** owns: map regeneration from the seed (the existing worldgen crates), + and — for the stretch tier — a new `TurnEvent::UnitMoved { turn, unit_id, from, + to }` plus its emission in `mc-turn`. Projection of "city/unit markers at turn + t" is a pure `GameHistory` method in `mc-replay` (Rail-1), mirroring + `standings_at`. +- **GDScript** only mounts the existing `hex_renderer` / `city_renderer` in a + projection mode and paints the Rust-projected markers as the scrubber moves. No + worldgen or position logic in GDScript (Rail-3). + +## Acceptance bullets + +- [ ] **Terrain** — `replay_viewer` regenerates the map from `GameHistory.seed` + (deterministic worldgen) and renders it god-view (fog off) by mounting the + existing `hex_renderer`. The regenerated map matches the seed's live map. +- [ ] **City markers** — a Rust projection returns city {hex, owner, name} as of + turn `t` from cumulative `CityFounded`/`CityCaptured` events; `city_renderer` + paints them. Markers update correctly as the scrubber moves forward/back. +- [ ] **Unit pings (coarse)** — `UnitCreated`/`UnitKilled`/`UnitCaptured` hexes + surface as transient spawn/death/capture pings at their turn (no continuous + position; documented limitation). +- [ ] **Continuous unit positions (stretch, schema change)** — add + `TurnEvent::UnitMoved { turn, unit_id, from, to }` to `mc-replay`, emit it at + every `mc-turn` move site, record it (depends on `p3-31`), and project per-turn + unit positions so the map shows units tracking turn-by-turn. Gated; ship the + marker tiers first. +- [ ] **Proof** — `replay_viewer` showing the regenerated terrain + city markers + at a mid-turn, scrubable; screenshot reviewed in conversation per the phase-gate + ritual. Extend `proof_replay_viewer.tscn`. + +## Out of scope + +- Re-simulation under updated rules (the replay projects recorded state, never + re-runs `mc-turn` — Rail-1). +- Persisting a full per-turn `WorldSnapshot` (terrain + every entity) into the + archive — heavy bloat; only revisit if seed-regen + events prove insufficient. +- Animating combat / movement tweens beyond what the live arena playback already + does; this objective is about *what* renders, not cinematic polish. diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index 62ef1542..d56e4568 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,13 +1,13 @@ { - "generated_at": "2026-06-27T07:58:32Z", + "generated_at": "2026-06-27T13:19:18Z", "totals": { - "in_progress": 0, - "partial": 6, - "stub": 1, - "missing": 0, - "oos": 31, + "missing": 2, + "partial": 7, "done": 296, - "total": 334 + "oos": 31, + "stub": 0, + "in_progress": 0, + "total": 336 }, "objectives": [ { @@ -3344,11 +3344,31 @@ "id": "p3-30", "title": "Port wild-creature AI from GDScript to Rust (Rail-1 compliance)", "priority": "p3", - "status": "stub", + "status": "partial", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-27", "summary": "**Rail-1 gap surfaced during the p3-29 logic sweep (2026-06-27).** The live turn runs\nwild-creature AI **decision logic in GDScript**: `turn_processor.gd::_process_wild_creatures`\n(line 459) calls `wild_ai.process_wild_turn(game_map)` →\n`src/game/engine/src/modules/ai/wild_creature_ai.gd` (302 LOC — a guard / attack / roam state\nmachine over `owner == -1` units).\n\nThis is sim logic, not presentation, so it violates Rail-1 (\"GDScript is presentation only\";\n\"AI decision-making lives in Rust\"). It is **distinct from [[p0-26-ai-tactical-rust-port]]**,\nwhich ported *player* tactical AI (`simple_heuristic_ai.gd` / `ai_tactical.gd` / `ai_military.gd`)\nand explicitly did not touch wild-creature behaviour. It is also distinct from the fauna\n*population/rendering/stats* objectives (`g2-08`, `p3-12`, `p1-49`, `p2-58a`) — those model\necology; this is the per-creature **combat behaviour AI**.\n\nThe combat *resolution* for wilds already lives in Rust (`mc-combat::wilds`); only the\n*decision* layer (who to attack, when to roam, leash enforcement) is still GDScript." + }, + { + "id": "p3-31", + "title": "\"Replay recording — live games archive a GameHistory (per-turn TurnSnapshot + events) on game-over\"", + "priority": "p3", + "status": "missing", + "scope": "game1-stretch", + "owner": "shipwright", + "updated_at": "2026-06-27", + "summary": "" + }, + { + "id": "p3-32", + "title": "\"Replay rendering — visual map playback (terrain + city/unit markers) from the archive\"", + "priority": "p3", + "status": "missing", + "scope": "game1-stretch", + "owner": "shipwright", + "updated_at": "2026-06-27", + "summary": "" } ] }