docs(@projects/@magic-civilization): ✅ p3-26 Gap 3 DONE (equipment/crafting verified headless) + Gap 4 scope assessment
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 <noreply@anthropic.com>
This commit is contained in:
parent
22f7fa1116
commit
b4c402e766
9 changed files with 317 additions and 24 deletions
10
.mcp.json
10
.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",
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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** |
|
||||
|
||||
</td><td valign='top' style='padding-left:2em'>
|
||||
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
| Team Lead | Remaining |
|
||||
|---|---|
|
||||
| [warcouncil](../team-leads/warcouncil.md) | 7 |
|
||||
| [shipwright](../team-leads/shipwright.md) | 2 |
|
||||
|
||||
</td></tr></table>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Queueable>` (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
|
||||
|
||||
|
|
|
|||
|
|
@ -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::<T>("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.
|
||||
|
|
|
|||
83
.project/objectives/p3-31-replay-live-recording.md
Normal file
83
.project/objectives/p3-31-replay-live-recording.md
Normal file
|
|
@ -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 `<archive_root>/<pack>/<game-id>/`.
|
||||
`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).
|
||||
78
.project/objectives/p3-32-replay-visual-map-rendering.md
Normal file
78
.project/objectives/p3-32-replay-visual-map-rendering.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue