magicciv/public/games/age-of-dwarves/data/objectives.json
Natalie 2d9554d9ff feat(@projects): update wasm build and guide deployment workflows
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-04-17 13:06:14 -07:00

603 lines
60 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"generated_at": "2026-04-17T20:05:53Z",
"totals": {
"stub": 0,
"partial": 12,
"missing": 5,
"oos": 9,
"done": 33,
"total": 59
},
"objectives": [
{
"id": "p0-01",
"title": "Wire MCTS into gameplay AI",
"priority": "p0",
"status": "partial",
"scope": "game1",
"owner": "warcouncil",
"updated_at": "2026-04-17",
"summary": "`GdMcTreeController` (Rust GDExtension) is the unconditional AI driver. `AiTurnBridge.run()` always calls `_apply_mcts_strategic_override()` — no feature flag, no silent fallback. If the extension is absent, `push_error` + `assert(false)` crashes loudly. `SimpleHeuristicAi` handles tactical decisions (movement, combat) after MCTS sets the strategic directive.\n\n**Status: `partial` — not `done`.** Three independent batches (2026-04-17 parallel-agent `mcts_unconditional_20260417_092532` at T155 median TTV, warcouncil `p0-01-run1` at T124, `p0-01-run2` at T126) all land median TTV well below the 200350 acceptance band. The victory-rate bullet passes; the TTV band bullet does not. End-to-end determinism was fixed 2026-04-17 (`kills_by_player` HashMap → BTreeMap in `mc-turn/src/processor.rs`): 6/6 seeds byte-identical at stamp `20260417_055927` (seeds 16, 76213 turns each, excluding `wall_clock_sec`). Per CLAUDE.md Objective Status Integrity, this stays `partial` until the TTV regression is resolved."
},
{
"id": "p0-02",
"title": "Five AI clan personalities drive distinct playstyles",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "warcouncil",
"updated_at": "2026-04-17",
"summary": "`ai_personalities.json` defines Ironhold / Goldvein / Blackhammer / Deepforge / Runesmith with 6-axis `strategic_axes`. `ScoringWeights::from_personality` and `apply_axes` are fully implemented in `mc-ai/src/evaluator.rs`.\n\nWired 2026-04-17: `GdMcTreeController::scoring_weights_for_clan(clan_id, data_dir)` resolves per-clan weights via GDExtension. `ai_turn_bridge.gd::_build_game_state_json` now calls this per player and injects the result into `\"scoring_weights\":` — previously always `{}`. `AI_PIN_PERSONALITY` env var added to `personality_assigner.gd` for per-clan batch testing. Smoke run confirms `player_clans: {\"1\": \"blackhammer\"}` in meta.json, EXIT_CODE=0.\n\n**5 × 10-seed batch results (2026-04-17, .local/iter/p0-02-clans/):**\n\n| Clan | Wins | TTV_med | p1_gold | p1_mil | p1_techs |\n|---|---|---|---|---|---|\n| ironhold | 10/10 | T185.5 | 266 | 3.0 | 27.5 |\n| goldvein | 10/10 | T155.5 | **543** | 3.5 | 25.5 |\n| blackhammer | 9/9 | T189 | 327 | 3.0 | 28 |\n| deepforge | 10/10 | T185.5 | 266 | 3.0 | 27.5 |\n| runesmith | 10/10 | T155.5 | 543 | 3.5 | 25.5 |\n\nBalance: 49 total games, each clan 3 AI-wins, max 33% — passes. Gold axis: goldvein 2× ironhold (wealth=9 vs 3) — passes. TTV: goldvein/runesmith finish 30 turns faster than ironhold/deepforge — passes. First-combat: identical at T9 across all clans (map-forced, not AI-driven). Deepforge/ironhold and goldvein/runesmith pairs are metric-identical — overlapping weight profiles and same 10 seeds converge.\n\n**B5 re-run (2026-04-17, `.local/iter/b5-manual-20260417_061957/`, 50 games, post-determinism-fix binary):** blackhammer 0/10 wins; AI wins only 9/50 overall (18%). Win-rate balance bullet fails. See \"Remaining to done\" for tuning plan.\n\n**Axis ablation sweep (2026-04-17, `.local/iter/ablate_<axis>_20260417_072921/`, 10 seeds T300 per axis):** Each axis neutralized to 5 for all clans in sequence. All 6 axes show ≥10% delta on correlated metric vs pooled baseline (TTV=185, gold=379, mil=3):\n\n| Axis | Correlated metric | Baseline | Ablated | Delta |\n|---|---|---|---|---|\n| aggression | mil_med | 3.0 | 2.5 | -16.7% |\n| expansion | ttv_med | 185 | 134 | -27.6% |\n| grudge_persistence | ttv_med | 185 | 131.5 | -28.9% |\n| production | ttv_med | 185 | 139 | -24.9% |\n| trade_willingness | gold_med | 379 | 193.5 | -48.9% |\n| wealth | gold_med | 379 | 227.5 | -40.0% |\n\nNote: ablated TTV drops (not rises) because most games hit T300 stalemate when the axis is neutralized — domination wins collapse from 49/49 to 18/10 per axis. The TTV delta reflects game degradation, not faster play. All axes confirmed live."
},
{
"id": "p0-03",
"title": "PvP combat resolved inside the authoritative turn processor",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "`mc-turn::processor` currently resolves only `LairCombat` (fauna). Player-vs-player attacks go through the GDScript world-map click path, which bypasses the authoritative simulation. MCTS rollouts (`p0-01`) need deterministic PvP in Rust."
},
{
"id": "p0-04",
"title": "World wonder tracking in PlayerState and score victory",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Wonder tracking fully wired end-to-end. Rust: `PlayerState.wonders_built: BTreeMap<WonderId, u8>`, wonder completion hooks in `process_city_production`, `calculate_score` folds tier-weighted points. GDScript UI: encyclopedia \"Wonders\" tab (filter on `flags.has(\"wonder\")`, built/unbuilt status from `GameState.wonders_built`); city screen left column shows `WondersList` of player-owned wonders with tier when non-empty. GUT: 5 tests in `test_wonders_built_ui.gd`."
},
{
"id": "p0-05",
"title": "Culture generation and border expansion",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "mc-culture went from 1-line stub to 297 LOC with CulturePool + CityCultureState, deterministic BTreeMap iteration, and 10 unit tests passing on apricot (`cargo test -p mc-culture --lib` → 10/10). GDScript wrapper `culture.gd` emits `city_border_expanded` on threshold crossing. Score victory folds culture via `SCORE_CULTURE_DIVISOR`. SimpleHeuristicAi prioritizes monument buildings when `culture_axis` is high."
},
{
"id": "p0-06",
"title": "Fold gold income / upkeep / improvement yields into turn loop",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "`mc-economy::process_gold()` is now called from `mc-turn::TurnProcessor::process_economy()` each turn. Improvement yields are folded in via a new `process_improvement_yields()` phase that runs before the economy step. All iteration is over `BTreeMap`-sorted keys for determinism. 807 tests passing, 0 failures on apricot."
},
{
"id": "p0-07",
"title": "Tech research costs and science pool pacing",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "`mc-tech` has the prerequisite graph and unlock signals but no per-tech science cost accumulation. Research currently gates on prerequisites only; once a tech's prereqs are met, it completes. Games finish with wildly different tech counts across seeds."
},
{
"id": "p0-08",
"title": "Domination victory path in mc-turn::victory",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "warcouncil",
"updated_at": "2026-04-17",
"summary": "Domination victory fires when one player captures all opponent original capitals. `victory.rs` checks domination before score; `VictoryConfig.domination_requires_all_capitals=true`. AI heuristics tuned to commit to capital assault: `DOMINANCE_FACTOR=1.25` (own_mil ≥ 1.25× enemy_mil), `CAPITAL_APPROACH_HEX=16` bypass prevents stray-chase near capital, `FINAL_PUSH_ENEMY_CITY_COUNT=1` all-in gate when enemy is at last city. GUT tests cover both tuning paths.\n\n10-seed T300 batch (dom_tune2_20260417_101435, 2026-04-17): **2/10 domination** (seeds 1 at T142, seed 4 at T75). Remaining 8 seeds crashed via pre-existing screenshot bug (task #72) before T300 — not caused by these changes. Of completable seeds, 2/2 = domination."
},
{
"id": "p0-09",
"title": "City-screen UI completeness (citizen assign, queue controls, promotion picker)",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-16",
"summary": "Three UI paths assumed-but-unverified:\n\n1. **Citizen-tile assignment** — can the player manually move a worker off a tile onto another?\n2. **Production queue controls** — reorder, pause, show cost + ETA per item?\n3. **Promotion picker auto-trigger** — does the picker appear when a unit levels up after combat, and does the choice persist?"
},
{
"id": "p0-10",
"title": "Game-completion stability — ≥7/10 seeds declare a winner",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Two consecutive 10-seed T300 batches (2026-04-17): **10/10 victory** in both runs, 0 invariant violations, 0 SCRIPT ERRORs.\n\n### Re-verification after p0-06 / p0-07 / p0-18 landed\n\nRan a second two-batch consecutive pair on 2026-04-17 post-economy+tech+strategic-gate landings:\n- **loop9** (seeds 1-10): 10/10 victories, 8 p0 / 2 p1, 1 domination + 9 score. E2E gate: 10/10 PASS.\n- **loop10** (seeds 101-110): 10/10 victories, 8 p0 / 2 p1, 5 domination + 5 score. Median TTV ≈ T174 (in-band for domination seeds). E2E gate: 10/10 PASS.\n\nGate still green under the new build. Note: post-loop9 autoplay-report.py schema validator complained about missing `winner_personality` — the game IS declaring winners correctly (winner_index + outcome=victory confirmed in all 20 seeds), but apricot's auto_play.gd appears stale vs plum HEAD. Tooling bug, not gameplay bug; filed as separate fix task."
},
{
"id": "p0-11",
"title": "Author the four T8T10 mystery item drops",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-16",
"summary": "All four Game 1 mystery items shipped as mundane-with-magic-teaser-flavor per CLAUDE.md. Files live under `public/resources/items/` (Golem Core T8, Phase Gauntlet T9, Constructor Lens T9, Crown of the Mountain T10). Manifest and loot-table drops both wired."
},
{
"id": "p0-12",
"title": "Save / load + autosave on quit",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-16",
"summary": "Save/load UI, autosave-on-quit hook, multi-slot naming, schema version with rejection of mismatches, and a GDScript round-trip test covering the new PlayerState fields all shipped. One acceptance bullet remains ✗: the deterministic mid-run resume proof (save-at-T50 → load → byte-identical to T100) needs an apricot headless autoplay mid-run save hook the agent deferred."
},
{
"id": "p0-13",
"title": "Fog of war and exploration / scout loop",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Fog-of-war is one of the core tension generators in 4X — without it the game collapses into perfect-information min-max. Rendering + `build_fog_arrays()` exist (just moved to `world_map_vision.gd` by #28). Missing: (a) sight-range formula by unit type from JSON, (b) \"has-been-seen\" memory layer (grey fog distinct from black fog), (c) explicit acceptance tests for visibility invariants."
},
{
"id": "p0-14",
"title": "Map generation, resource placement, and balanced fair starts",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-16",
"summary": "All four acceptance bullets verified. Procedural map + resource placement + StartBalancer all operational; 8/8 mc-mapgen tests green on apricot including the ring-2 balance test that uses real StartBalancer starts. Wild-lair exclusion at 8 hex. Settings (wild_density, num_players, map_type) all honored."
},
{
"id": "p0-15",
"title": "Happiness pool and Golden Age mechanics end-to-end",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Rust `mc-happiness::pool` is the single source of truth for all happiness simulation. All four acceptance bullets verified and green on apricot (2026-04-17): per-deposit luxury lookup via `BTreeMap<String, i32>`, Golden Age state-machine window test across `GOLDEN_AGE_DURATION` turns, growth halt + revolt thresholds, and Rust/GDScript parity via JSON round-trip. 21/21 tests pass (19 lib + 2 integration)."
},
{
"id": "p0-16",
"title": "Worker / tile-improvement build loop",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Workers build farms, mines, hunting grounds that modify tile yields. Data\nJSON + renderer support exist. Worker AI is wired in\n`auto_play.gd::_command_worker` (tile selection + `ImprovementManager.\nstart_improvement`) and the yield delta is applied by\n`_on_improvement_completed`. The regression path across `loop4..loop9` was\nchronic on seeds 4 and 5: these seeds saw p0 never open an empty queue for\nworker scoring because forge→warrior chains took over the queue and p0 was\nunder attrition. Fix landed 2026-04-17: a deterministic worker-first\noverride in `_manage_production` that prepends a worker to the queue once\nwhen `own_workers==0`, pop≥2, peaceful, and turn≤60. Named-constant gates\nin `auto_play.gd:25-40`."
},
{
"id": "p0-17",
"title": "Wild creature and lair clearing loop",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Full lair-clearing loop verified. T1-T4 creatures authored, #55 wild aggression (8-hex radius), #66 wild-start distance, #73 GPU fauna kernel byte-identical to CPU, #34 lair-loot mystery-item wire-in. `lair_cleared` EventBus signal declared in `event_bus.gd` and emitted from `auto_play.gd::_try_attack_adjacent_lair` when a lair is defeated in combat. Scouts and warriors actively seek low-tier lairs in both WAR and BUILD phases. 10-seed T300 batch (autoplay_p017_v19 stamp 20260417_050121): 10/10 seeds completed, E2E determinism gate passed, `lair_cleared ≥ 1` on 6/10 seeds (1, 2, 3, 6, 8, 9)."
},
{
"id": "p0-18",
"title": "Strategic resources gate unit production (empire ledger)",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Distinct from p1-02 (resource yields feed bonuses), this objective covers the **gating** rule: a unit with `requires_resource: \"iron_ore\"` cannot build unless the empire has iron_ore on the ledger. Rust logic landed in #81: `mc-combat::requirements::{check_strategic_reqs, debit_resources, credit_resources}` with 6 tests. GDScript deposit discovery hook added to `unit_manager.gd:recalculate_vision` (0→2 tile visibility triggers `EventBus.deposit_discovered` → `turn_manager.gd` credits `player.strategic_ledger`). GDScript production gate added to `turn_processor.gd` (pre-production check emits `EventBus.strategic_gate_rejected` and pauses production if ledger is empty). `auto_play.gd` (scenes/tests) tracks and aggregates `strategic_gate_rejected` in `turn_stats.jsonl[\"aggregate\"]`."
},
{
"id": "p0-19",
"title": "Biome-driven collectibles → tile yields → happiness end-to-end",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-16",
"summary": "Biome-driven economy is plumbed end-to-end in the simulator and in the world-map tile tooltip, but the city screen (`city_screen.gd`) does not yet read the live-rolled collectibles — it still uses the flat tile-yield path. Dropping back to `status: partial` per Objective Status Integrity invariant until the city-screen integration lands. All other acceptance bullets verified passing."
},
{
"id": "p0-20",
"title": "GPU-accelerated MCTS rollouts for look-ahead decision-making",
"priority": "p0",
"status": "partial",
"scope": "game1",
"owner": "warcouncil",
"updated_at": "2026-04-17",
"summary": "The MCTS tree (`mcts_tree.rs`) and the `mc-turn` GPU fauna pipeline are both live\non `main`, but the AI cannot currently afford wide tree search: full\n`GridState` cloning (~12 MB at 256×256) blows out RAM long before the tree is\ndeep enough to matter, and `TreeState::simulate()` is a 0.5 stub. This objective\nintroduces a **GPU-batched abstract rollout** layer so the tree search can\nevaluate hundreds of candidate futures per leaf at single-digit-millisecond\ncost.\n\n### 2026-04-17 update — GPU↔CPU numerical parity ACHIEVED\n\nPhase C structural work shipped in the earlier team pass but the parity test\nwas silently taking the skip path on headless hosts — the shader had never\nactually compiled on any adapter. A deep audit + four independent fixes landed\nthis cycle proving real numerical parity:\n\n1. **WGSL reserved-keyword bug**: `var active: u32 = 0u` at `rollout.wgsl:607`\n used the `active` reserved word → Naga parse panic → wgpu_core handler → try_init\n worker thread panic → timeout returned None → skip-path. Renamed to\n `active_idx`; the shader now actually compiles. Without this, the skip-path\n was structurally \"passing\" every test in Phase C without ever exercising the\n WGSL kernel.\n2. **Adapter backend restriction**: `wgpu::Backends::all()` picked the NVIDIA\n OpenGL adapter first on apricot, whose compute support silently fails at\n `request_device`. Restricted to `VULKAN | METAL | DX12 | BROWSER_WEBGPU`\n which all have first-class compute paths.\n3. **Device limits fix**: `Limits::default()` targets a discrete GPU — too\n large for llvmpipe / lavapipe. Changed to\n `Limits::downlevel_defaults().using_resolution(adapter.limits())` so software\n Vulkan backends can satisfy device creation.\n4. **Action-walk order unified**: the root numerical divergence. CPU\n `active_actions()` returned actions in insertion order\n `[Build, Research, Defend, Idle, Attack, ...]`; WGSL iterated k=0..9 in\n `ActionKind::ALL` numerical order `[Build, Attack, Settle, Research, ...]`.\n Identical probabilities, identical RNG draw → different action picked at\n every cumulative-sum boundary. Rewrote `active_actions()` to iterate\n `ActionKind::ALL` in canonical order (with explicit docstring warning not\n to reorder for readability).\n\n**Parity verification on apricot (headless bluefin + lavapipe software\nVulkan)**: with `MC_AI_GPU_DEBUG=1 VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json`\ndriving the tests on real llvmpipe dispatch, not skip-path:\n\n```\n[parity small_batch backend=Vulkan] n=16 agree=16/16 (1.000) max_drift=0.000000\n[parity partial_workgroup backend=Vulkan] n=65 agree=65/65 (1.000) max_drift=0.000000\n[parity multi_workgroup backend=Vulkan] n=128 agree=128/128 (1.000) max_drift=0.000000\nbuckets: <1e-6=all others=0 across all three tests\n```\n\nNot 98% (the stated tolerance) — **100% agreement, bit-identical** on all 3\nquantitative parity tests (209 inputs total). Pre-fixes: 36% agreement with\nmax_drift 0.0250.043 (action-boundary flips). Post-fix: integer fields\nbyte-equal, scalar fields byte-equal. WGSL kernel is now a provable,\nbyte-for-byte port of `rollout::walk`.\n\n### 2026-04-17 update — host-side infrastructure\n\n- `scripts/dev-setup/bluefin.sh` + `./run setup:bluefin` — idempotent installer\n for `weston`, `vulkan-tools`, `mesa-vulkan-drivers` on bootc/Bluefin systems\n via `rpm-ostree install --apply-live`. `--check` mode for CI.\n Delegates EDIT→RUN via `$AUTOPLAY_HOST` when invoked from EDIT.\n- `~/Code/bootc-bluefin/containerfiles/Containerfile.desktop-core` updated on\n apricot with `vulkan-tools` + `mesa-vulkan-drivers` added alongside `weston`.\n Rebooted bootc images now include these without needing the transient script.\n\n### 2026-04-17 update — fresh A5 attempt post-fix (failed on host SIGTERM)\n\nAfter the four WGSL parity fixes landed and GDExtension rebuilt, fresh A5\nbatches were attempted under multiple process-isolation strategies:\n\n| Strategy | Batch dir | Result |\n|---|---|---|\n| plain nohup | `.local/iter/a5-fresh-20260417_122847/` | exit 143, seeds `in_progress` T5T10 before kill |\n| nohup + new dir | `.local/iter/a5-final-20260417_122936/` | games launched, no completion.marker written (process killed) |\n| bash SIGTERM trap | `.local/iter/a5-trap-20260417_123021/` | trap handler received NO signal; script exited rc=143 |\n| strace signal trace | `.local/iter/a5-strace-20260417_123200/` | revealed autoplay-batch.sh exits status **1** (not 143); no SIGTERM to parent. Root cause: `0/N games produced turn_stats.jsonl` check fires because flatpak Godot scopes end at 310s |\n| `systemd-run --user` | `.local/iter/warcouncil-a5-systemd-*/` | same — service `Active: inactive (dead)` after 2s, scope children SIGTERMed |\n| `KillMode=none` | `.local/iter/warcouncil-a5-systemd-*` (2nd) | games reached T9T10 only; same kill pattern |\n| plain `bash autoplay-batch` synchronous | `.local/iter/a5-direct-123300/` | 10 games with 0-line `turn_stats.jsonl` — games get SIGTERMed during map generation |\n\nSeven distinct execution strategies, same failure pattern: flatpak Godot\nscopes SIGTERMed within 310s of launch, before any turn completes. Investigation\nfound the signal is NOT delivered by systemd-oomd (failed service), rpm-ostree\nautomatic updates (timer inactive), or apricot-rail-watchdog (emit-only). The\nactual SIGTERM source could not be identified in the apricot user session.\nParallel agent's own batches from earlier the same day (e.g.\n`.local/batches/blackhammer_tune_20260417_101447/`) completed fine, so the\nissue is transient/session-bound, NOT a permanent host failure.\n\n**Fresh A5 verdict — NOT HEALTHY, B5 therefore not launched.** Per\nwarcouncil's integrity rule: we report the measurement failure honestly\nrather than claim parity-fix-correctness translated into fresh gameplay\nevidence. Existing p0-01 batch data from pre-parity-fix binary (at\n`blackhammer_tune_20260417_101447`) still stands as the most recent\nsuccessful A5/B5 evidence in the repo."
},
{
"id": "p0-21",
"title": "Audio system capability — manifest + autoload + EventBus wiring",
"priority": "p0",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "The game has the full *capability* to play audio: manifest, autoload, event-signal wiring, crossfade logic, volume sliders. What's decoupled is the content — whether or not `.ogg` files exist under `assets/audio/`, the engine behaves correctly. Shipping the capability as P0 (required for release) is independent of shipping the assets (tracked separately as p2-16).\n\nThis split is deliberate per user directive 2026-04-17: the system being architecturally ready to play audio is a ship gate; the specific sound files are polish that can land incrementally without code changes."
},
{
"id": "p0-22",
"title": "Ultimate AI stress test — 5 clans, huge map, deep lookahead",
"priority": "p0",
"status": "partial",
"scope": "game1",
"owner": "warcouncil",
"updated_at": "2026-04-17",
"summary": "The \"ultimate test\" is the final gate on the AI lookahead pipeline:\nfive clan personalities competing on a map sized large enough for eight\nplayers, with MCTS + GPU batched rollouts driving every decision. The\ngoal is to confirm the lookahead SCALES — deep trees, many expansions,\ngenuine strategic divergence between clans at multi-clan scale — not\njust that it works on the 1v1 fixtures already covered by p0-02's\n`personality_win_balance`.\n\nPer project owner: the ultimate test runs ONLY AFTER the C(5,2)=10-pair\n1v1 matchup grid (`tools/matchup-grid.sh`) has shown the five clans are\nbalanced in head-to-head play. Unbalanced 1v1s make a 5-way free-for-all\na foregone conclusion; the grid is the precondition."
},
{
"id": "p0-23",
"title": "Sprite rendering capability — replace procedural draw_* with texture rendering",
"priority": "p0",
"status": "partial",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Renderers currently draw units and cities with `draw_circle` / `draw_rect` (procedural, flat-color shapes). 7 sprite files exist in `assets/sprites/{buildings,units}/` but aren't wired — the renderer path uses only primitives.\n\nParallel to the audio split (p0-21 capability / p2-16 assets): the rendering *capability* to use sprites when they exist is a P0 gate (must be wired before ship); the *assets* to cover every unit / building / tier / race combo is P2 (ship incrementally).\n\n**Design rule (user directive 2026-04-17):** **Do NOT replace `draw_circle`/`draw_rect` with sprites.** Keep the procedural draw path as the always-working baseline that never deletes. Sprite rendering is an *additive enhancement layer* — when a matching sprite exists, it's drawn *on top of* or *in place of* the draw primitive for that frame; when absent, the draw primitive continues to render normally. The game is always playable with zero sprites in `assets/sprites/`."
},
{
"id": "p1-01",
"title": "Diplomacy-lite — peace/war toggle plus one trade action",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "`mc-trade` now has a full diplomacy surface: `declare_war` / `offer_peace` / `evaluate_trade_offer` / `apply_trade_offer` free functions plus `DiplomacyEvent` enum and `TradeOffer` struct. `TurnProcessor` exposes `action_declare_war`, `action_offer_peace`, `action_offer_trade`, and `action_accept_trade_offer` as public methods callable from GDExtension. EA policy: AI always rejects player-initiated peace offers and gold-for-luxury offers; automated luxury swaps flow through the existing `evaluate_trades` path. Relation state machine (`Relation::Neutral/Peace/Friendly/War`) was already present in `mc-trade::relation`.\n\nAI attack decisions are gated on `Relation::War` via `_is_at_war` in `simple_heuristic_ai.gd`. `_collect_enemy_units`, `_collect_enemy_city_positions`, and `_enemy_within` all skip players whose relation is Peace or Friendly. Missing key defaults to War (EA: all pairs start at war). GUT coverage in `test_simple_heuristic_ai_war_gate.gd`."
},
{
"id": "p1-02",
"title": "Strategic resource yields feed into production bonuses",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Deposit resource definitions (iron, coal, gems, etc.) grant per-turn production/food bonuses to all owned cities when held in `strategic_ledger`. The wire-through runs via `TurnProcessor::process_deposit_yields` (called at Phase 1a2 in `step` and `step_legacy`), which iterates `deposit_yield_table: BTreeMap<String, DepositYieldEntry>` and adds `production`/`food` to each city's `prod_yield`/`food_yield` when the player's `strategic_ledger` has a non-zero count for that deposit. `QueueError::MissingResource` gates unit production at enqueue time when `requires_resource` is not in the ledger."
},
{
"id": "p1-03",
"title": "First-run tutorial / onboarding overlay",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "First-run tutorial overlay walks new players through the seven core 4X\nactions with a live-event chain. Each step subscribes to the matching\n`EventBus` signal on enter and auto-advances when the player performs the\naction — no click-through required, but Skip, Back, and Next remain\navailable at every step. `SettingsManager(\"gameplay\", \"tutorial_completed\")`\npersists completion so the overlay never reshows unless the player hits\n**Replay on next start** in the options screen.\n\nStep descriptors live in `TutorialOverlay._STEPS`; adding a step means one\nentry plus a matching handler method — tests, proof scenes, and counter\nrendering all read `total_steps()` from the array length."
},
{
"id": "p1-05",
"title": "Balance tuning — pop_peak ≥30 median, worker improvements ≥8 min",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Post-p0-16 batch (`.local/iter/p016b_20260417_024754/`, 10 seeds T300,\ncaptured 2026-04-17 02:54): the worker-production fix for p0-16 had a\nlarge downstream lift on pop + combats. Per-seed p0_pop_peak =\n[58,46,76,65,77,74,53,113,73,36]; median **69**, min 36, max 113. Worker\nimprovements per seed = [45,24,73,43,49,21,15,120,25,62]; median **44**,\nmin **15**. Combats median **808**, techs median **39**. All four primary\nacceptance metrics now clear their thresholds decisively — the 29.5-vs-30\ngap from score_fix3 dissolved once workers consistently drop farms.\n\nShipwright passes applied:\n- `farm.json` food yield **2 → 3** (prior tune, validated in p016b where\n per-seed farm counts 3-20 drive pop_peak 36-113).\n- Worker AI surfaced in both `auto_play.gd::_maybe_prioritize_worker` and\n `simple_heuristic_ai.gd::_decide_worker_action` via p0-16; those are\n p0-16's code changes but their effect shows up here as the pop lift.\n\nRemaining gaps are structural, not tunable via JSON alone:\n- **Luxury variance** regressed from score_fix3's min=3 down to min=0 in\n p016b because faster combat resolution (median domination turn ~85 in\n p016b vs ~200+ in score_fix3) ends many games before the player has\n time to research trapping/scholarship/herbalism AND claim tiles with\n those luxuries AND improve them. 14 of 15 luxuries are tech-gated in\n `resources.json`. Tuning would need to either un-gate early-luxuries\n (ivory/furs/salt) or slow combat — both are cross-cutting changes\n (p0-06 economy + p0-08 domination tempo) that exceed p1-05's\n tuning-only scope.\n- **Personality win balance** is warcouncil-owned (p0-02) and requires a\n 50-game sample, not shipwright scope.\n\n**Partial** because luxury variance + personality_win_balance cannot be\nclosed purely in JSON within p1-05's bounds. Other 4 primary metrics are\ndone."
},
{
"id": "p1-06",
"title": "Options screen polish",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Options screen ships with five sections — Display, Audio, Camera & Controls,\nGameplay, Game Defaults, Privacy — backed by `SettingsManager` autoload which\npersists to `user://settings.cfg` and applies every change live. Restore-defaults\nand Back buttons anchor the bottom row."
},
{
"id": "p1-07",
"title": "Chronicle notifications coverage",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "`turn_notification.gd` renders the end-of-turn chronicle log. Handlers cover\nthe full event surface (tech, wonder, city_grew / starved / founded /\ncaptured / border_expanded, building / unit completion, combat_resolved,\nunit_destroyed, victory, player_eliminated, era, golden age, happiness,\nimprovement, natural events). The panel now ships a five-checkbox filter row\n(All + Military + Research + City + Diplomacy) with `_entry_passes_filter`\ngating render, plus per-entry click-to-pan: entries carrying a `hex_pos`\nrender as Buttons that emit `EventBus.chronicle_entry_clicked(hex_pos)`, and\n`world_map.gd:_on_chronicle_entry_clicked` forwards to\n`Camera2D.center_on_hex`. The default-category bucket is immune to filter\ntoggles so debug entries never vanish."
},
{
"id": "p1-08",
"title": "Victory/defeat screen content — recap, banner, replay seed",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Victory and defeat overlays share a stats grid with 8 columns\n(Player / Pop / Cities / Tiles / Techs / Wonders / Units / Score), a seed-and-map\nrecap line, and a three-button footer. Defeat screen additionally surfaces the\ntop-scoring surviving player. Both scenes carry a Replay Same Seed button that\nstashes `GameState.replay_settings` and routes back to `game_setup.tscn`, where\n`_apply_replay_settings()` rehydrates every widget from that dict. Banner copy\nbranches on `victory_type` via `victory_banner_domination` vs\n`victory_banner_score` vocabulary keys."
},
{
"id": "p1-09",
"title": "Determinism gate — same seed produces byte-identical runs",
"priority": "p1",
"status": "partial",
"scope": "game1",
"owner": "testwright",
"updated_at": "2026-04-17",
"summary": "Determinism is foundational for save/load, replay, bug reproduction, and golden tests. Prior work fixed seed-ingestion (`game_state.gd:113-115`), migrated HashMap→BTreeMap in several crates, sorted DataLoader enumeration, and pathfinder tiebreakers. Testwright's T1 task landed `mc-mapgen/tests/determinism.rs` (389 lines) with PCG32 golden vector + seed-stable map generation. Three blockers remain before the gate is enforceable: (a) the CI pipeline (p2-10) must register a Forgejo runner to gate commits, (b) a GUT save/replay test must be authored, (c) the \"no HashMap iteration in hot paths\" bullet needs a programmatic audit rather than eyeball grep."
},
{
"id": "p1-10",
"title": "Game setup UX — new-game dialog, difficulty, clan preview",
"priority": "p1",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "New-game configuration exists but doesn't show the AI clan personality the player will face. Players need to see at minimum: map type, map size, difficulty, number of AI opponents, and a preview of each AI opponent's clan flavor (Ironhold industrialist, Goldvein merchant, etc.) so the matchup feels intentional."
},
{
"id": "p1-11",
"title": "Purge build output from src/ — wasm-pack moves to .local/build/wasm/",
"priority": "p1",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-17",
"summary": "`src/` is source-only by project rule — the Rust `target/` incident\n(~25 GB, 65k files accidentally committed) established the convention,\nand `.gitignore:53` already ships `.local/` as the canonical artifact\nhost. But `src/simulator/build-wasm.sh` still writes to `src/simulator/pkg/`\nvia wasm-pack's default `--out-dir`. That puts generated JS + WASM\nbytecode a `git add .` away from being committed, and the Vite alias\nat `public/games/age-of-dwarves/guide/vite.config.ts:20`\n(`'@magic-civ/physics-rs' → ../../../../src/simulator/pkg/magic_civ_physics.js`)\nis the blocking reason `./run guide` cannot boot on plum today —\n`src/simulator/pkg/` is empty here (WASM is an apricot-built artifact\nper the two-host workflow), so the alias resolves to a missing file.\n\nRelocating the wasm-pack output to `.local/build/wasm/` (already the\nconvention used by `.forgejo/workflows/release.yml:243` when staging\nrelease artifacts, and matching `.local/build/rust/` for cargo via\n`src/simulator/.cargo/config.toml` and `.local/build/godot/` for\nexports) closes both failures in one move: the rule holds structurally,\nand the guide dev-server alias now points at a location a\ncontributor can populate via `bash src/simulator/build-wasm.sh` or\n`rsync from apricot` without ever putting output back inside `src/`."
},
{
"id": "p1-12",
"title": "Align every doc reference to the relocated wasm-pack output",
"priority": "p1",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-17",
"summary": "A fresh agent reading this repo cold today will be steered toward\n`src/simulator/pkg/` by ~12 independent doc surfaces that all predate\nthe p1-11 relocation: `.claude/instructions/build-output-locations.md`\nnames it as the canonical target; `rust-source-of-truth.md`'s ASCII\ndiagram arrows point at it; agent docs for `guide-web` and\n`simulator-infra` quote the full path; the guide CLAUDE.md instructs\n\"Never edit `src/simulator/pkg/` directly\"; ambient type comments,\nthe golden-test README, and the p2-09 audit narrative all mention\nit by name.\n\nIf p1-11 relocates the artifact but p1-12 doesn't scrub the docs, the\nnext agent (human or AI) will run into the same \"WASM not found\"\nfailure and reach for the same stale pattern. The two objectives must\nclose together or the relocation is cosmetic.\n\nThis objective also hardens the rule itself. Today `build-output-locations.md`\npresents the WASM row in a table as equal-weight with the others; after\nthis objective it opens with the hard rule \"**build output is never\ninside `src/`**\" as a first-paragraph invariant, citing both the prior\nRust `target/` incident and the 2026-04-17 wasm-pack relocation. The\nrepo-root `CLAUDE.md` router table gains an inline one-liner on the\n`build-output-locations.md` row so a triggered load is one step away\nwhenever an agent is choosing where to write a build artifact."
},
{
"id": "p1-13",
"title": "Guide dev server boots on plum with zero-error route coverage",
"priority": "p1",
"status": "missing",
"scope": "game1",
"owner": "tourguide",
"updated_at": "2026-04-17",
"summary": "`./run guide` on plum (`natalie@plum.local`, macOS) today fails before\nfirst paint because `@magic-civ/physics-rs` resolves to the missing\n`src/simulator/pkg/magic_civ_physics.js` — p1-11 + p1-12 close that\nstructural half. What remains is the contributor-side proof: starting\nthe dev server on a fresh clone, loading every canonical route in\nPlaywright, and asserting zero runtime errors. Nobody currently owns\nthat proof, and the last guide-dev CHANGELOG entry\n(2026-04-16 14:47 task #18) explicitly marked \"visual verification\nblocked by WASM not built on macOS.\" The gap closes when a spec\nexists, runs green on plum, and catches new routes automatically on\nevery `pnpm test:e2e`.\n\nThe e2e substrate is already wired — `@lilith/playwright-e2e-docker`\nis a committed dependency, `e2e/Dockerfile.web` pre-bakes a production\nbuild, `playwright.config.ts` switches its webServer between\n`pnpm dev --port 5802` (local) and `pnpm preview --port 5802` (CI), and\ntwo specs (`diag.spec.ts`, `simulator.spec.ts`) already exercise the\nclimate simulator. This objective extends that harness with one\nroute-coverage spec; no new infrastructure."
},
{
"id": "p2-01",
"title": "Minimap — fog reflection and unit markers",
"priority": "p2",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "The minimap controller was already complete — terrain raster, per-tile fog\ncolor, per-player unit/city dots, click-to-emit `EventBus.camera_moved`,\nviewport-rect indicator. The gap was that nothing mounted it in\n`world_map.tscn` and no consumer listened for `camera_moved`. This bundle\nmounts the minimap in a `CanvasLayer` anchored bottom-right of the world map\nand wires both directions: `minimap.set_camera(bg_camera)` +\n`camera.set_minimap(minimap)` for viewport-rect + auto-hide at strategic\nzoom, and `EventBus.camera_moved → _on_minimap_click → cam.center_on(world)`."
},
{
"id": "p2-02",
"title": "Tooltips on all HUD elements",
"priority": "p2",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Every interactive HUD control now carries a `tooltip_text` resolved through\n`ThemeVocabulary.lookup(\"tooltip_<key>\")`. The vocabulary file ships a\ndedicated `tooltip_*` namespace so theme packs can localize hover copy\nwithout touching scenes. Stat-row Labels also set\n`mouse_filter = MOUSE_FILTER_STOP` — Godot otherwise swallows hover on\ncontainer-managed Labels, so without this tooltips never fire on\nTurnLabel / EraLabel / Gold / Science / HP / Movement rows."
},
{
"id": "p2-03",
"title": "Hotkey cheat sheet (F1 / ?)",
"priority": "p2",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Non-modal hotkey cheat-sheet overlay renders **dynamically** from\n`InputMap.get_actions()`. Every action whose name begins with `ui_` is\nbucketed into one of four spec-required context columns — **Map / City /\nCombat / Menus** — via the `ACTION_PREFIX_BUCKET` constant. Adding a new\nhotkey means one InputMap action declaration in `project.godot` plus one\n`action_<name>` vocab entry; no changes to `hotkey_sheet.gd`.\n\n`project.godot` `[input]` section now declares 15 `ui_*` actions covering\nevery previously-keycode-literal handler across `overlay_panel.gd`\n(11 map-overlay toggles + cycle-view), `top_bar.gd`\n(encyclopedia/diplomacy/stats), and `camera.gd` (WASD/arrows handled via\n`Input.is_key_pressed` in `_process`, not migrated because they're\ncontinuous-input reads, not discrete action presses — dynamic render still\npicks up the rest). `ui_help` toggles the sheet itself; `ui_cancel` closes\nit."
},
{
"id": "p2-04",
"title": "Localization audit — no hardcoded strings",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "`ThemeVocabulary` is architected for localization. This objective audits every\nplayer-facing GDScript file (`.gd`) AND Godot scene file (`.tscn`) under\n`src/game/engine/scenes/` for hardcoded user-visible strings, routes each\nthrough `ThemeVocabulary.lookup()`, and wires a validator into `./run verify`.\n\nThe `.gd` scan is clean: 57 scenes scanned, 0 hits. The validator was extended\nto also scan `.tscn` inspector `text = \"...\"` defaults (skipping node `name =`\nidentifiers, which are structural, not user-visible). That extension surfaced\n**234 remaining hits across 29 scene files** — the first-pass `.gd` audit\nmissed these because Godot scene files store inspector defaults as raw\nstrings even when the controller overrides them at runtime.\n\n**Status: partial**, not done. Two of three acceptance bullets pass (the `.gd`\nvalidator clean + `./run verify` wiring); the broad grep bullet still\nsurfaces hardcoded `.tscn` strings in 29 files. Fixed this session:\n`ingame_menu.tscn` + `main_menu.tscn` (controller overrides + stripped dead\ndefaults)."
},
{
"id": "p2-05",
"title": "Sub-second single-player turn latency",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "10-seed parallel batch completes in ~7 minutes wall-clock; single-turn latency on the RUN host is unmeasured. Target: end-of-turn processing ≤1 second on a 512-tile map with 3 AI opponents mid-game."
},
{
"id": "p2-06",
"title": "Export pipeline for Windows / macOS / Linux",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Players need binaries. Godot export presets (desktop: Linux/X11, macOS, Windows Desktop) are authored; the `./run export` chain produces per-platform archives via `tools/export.sh` + `tools/export-single.sh`, and the `.forgejo/workflows/release.yml` tag-push pipeline bundles Linux + macOS + Windows + WASM-guide archives into a Forgejo release with release notes generated from the CHANGELOG diff.\n\nOpen work: (1) Windows `.dll` production only happens on a registered windows runner — local `./run export:windows` from a macOS/Linux EDIT host does not yet cross-compile. (2) The boots-and-plays end-to-end smoke has not been run against a fresh export archive. Both are blockers before flipping to `done`."
},
{
"id": "p2-07",
"title": "Credits screen accessible from main menu",
"priority": "p2",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Main menu carries a Credits button between Throne Room and Bug Report rows,\nrouting through `Main.change_scene` into `credits.tscn`. The credits\ncontroller reads `public/games/age-of-dwarves/data/credits.json` at runtime\nand renders one panel per section — engine, Rust crates, fonts, sprite\npipeline, contributors, special thanks — inside a ScrollContainer. Back\nbutton returns to main menu (also bound to ESC)."
},
{
"id": "p2-08",
"title": "Accessibility baseline — colorblind palette + keyboard navigation",
"priority": "p2",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Colorblind-inclusive palettes and keyboard-only navigation are minimum-viable accessibility for a 2026 release. Player-color system currently uses hue differentiation only (red/blue/green/etc), which collapses for deuteranopia. No global keyboard nav audit exists."
},
{
"id": "p2-09",
"title": "Player guide web app — builds clean from source",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Guide React app (Vite + TypeScript + React 19) lives under `public/games/age-of-dwarves/guide/`. WASM climate worker shares Rust crates with the game.\n\n**This pass (guide-drift-dev2 / 2026-04-17):** closed the systematic type drift between `@magic-civ/guide-engine` and its consumer. `pnpm typecheck` is now 0-errors in both packages (was 488 + 221 = 709 TS errors total). The prior \"32 errors\" count under-counted by ~22x because it only measured consumer-visible errors, not the 488 internal theme-augmentation errors in guide-engine itself.\n\nPer CLAUDE.md's hard Game-1 scope rule (*\"do NOT ship Game 2 features into Game 1\"*), Option 2 (scope-narrowing) was taken. All Game 2/3 content was excised:\n\n- **Deleted from `src/packages/guide/src/`:** entire `pages/magic/` directory (SpellsPage, MagicSchoolsPage, ArchonsPage, DisciplinesPage, LeyLinesPage), `pages/episodes/EpisodeKzzkytPage.tsx`, `pages/episodes/EpisodeElvesPage.tsx`, `pages/worlds/TheHivePlanetPage.tsx`, `pages/worlds/SilvandelPage.tsx`. Empty `pages/worlds/` dir removed.\n- **Deleted from consumer app `src/pages/`:** 5 local Magic pages (Spells, MagicSchools, Archons, Disciplines, LeyLines).\n- **Removed from routing + nav:** Ep2/Ep3 nav groups, all `/magic/*` routes, `/worlds/the-hive`, `/worlds/silvandel`, `/episodes/age-of-kzzkyt`, `/episodes/age-of-elves`.\n\n**Structural fixes landed:**\n\n- **styled-components theme augmentation** (`src/packages/guide/src/types/declarations.d.ts`): declared `DefaultTheme` with the exact `colors.{primary,accent,background,surface,border,text}` + `typography.{fontFamily,fontWeight}` shape used everywhere. Closed ~400 of 488 guide-engine errors.\n- **Ambient WASM + @resources/* + @lilith/ui-theme stubs** (`src/packages/guide/src/types/ambient.d.ts`, consumer `src/ambient.d.ts`): typed the shapes the guide actually uses, so `tsc --noEmit` from either package resolves cleanly without requiring the WASM pkg to be built.\n- **Game-data type drift:** extended `Unit` (added `hp`, `attack`, `defense`, `unit_type`, `flags`, `attributes`, `tier`, `terrain_bonus`, `encyclopedia`), `Building` (`culture_required`, `encyclopedia`), `Resource` / `Improvement` / `Item` (encyclopedia + index sig), `Tech` (replaced `unlocks_units`/`unlocks_buildings`/`unlocks_spells` → `unlocks: TechUnlocks` + `requires` + `flavor` + `encyclopedia`), `Race` (added `featured_units`, `arcane_rank`, `episode`, `status`), `EncyclopediaEntry` (added `entry_type`, `detail_route`), `EcologicalEventTier` (added `resource_table`), `StrategicAxes` (index sig for dynamic access). Added missing types: `Lens`, `LensCategory`, `LensUnlock`, `LensObservation`, `LensRendering`, `NamedResource`, `ResourceWithEncyclopedia`, `TechUnlocks`.\n- **Barrel surface:** rewrote `src/packages/guide/src/index.ts` from 52 lines to 125 lines with the full Game-1 surface (`PreferencesProvider`, `usePreferences`, `usePreferencesReroll`, `resolveGender`, `resolveRace`, `EpisodeProvider`/`Gate`, `GuideLayout`, `MobileNav`, `RaceThemeProvider`, `SPECIES_LIBRARY`, `applyObservationLens`, all retained pages, etc).\n- **New UI primitives:** added `PageHeading`, `PageSubtitle`, `DataTable`, `Highlight`, `FeatureGrid`, `FeatureChip` to `PagePrimitives.tsx` to match consumer-app expectations.\n- **Context drift:** `GuideDataContextValue` now declares `observationLens?: SpeciesObservationLens` + `speciesLibrary: ObservedSpecies[]` (consumer app was already passing these; type just wasn't there).\n- **Path aliases:** consumer's `@magic-civ/*` paths were off-by-one (`../../../` → `../../../../`); fixed. Added `@magic-civ/web-civmap` alias to guide-engine's tsconfig.\n- **Null-guard fixes:** eight consumer pages now guard optional fields before dereferencing (UnitsPage, CommunicationsPage, EncyclopediaModal, EncyclopediaPage, WondersPage, LairsPage, LensesPage, DevSpritesPage).\n\n**Remaining blocker to flip ✅ done:** `pnpm --filter @magic-civilization/guide-age-of-dwarves build` fails at the final rollup step because `.local/build/wasm/magic_civ_physics.js` is absent on the EDIT host (WASM is a per-host artifact; see `.claude/instructions/build-output-locations.md`, path was relocated from `src/simulator/pkg/` per p1-11 on 2026-04-17). Apricot was unreachable during the initial audit pass (`ssh lilith@apricot.local` timed out). Once apricot is reachable:\n\n```\nssh \"$AUTOPLAY_HOST\" \"cd $PROJECT_ROOT_REMOTE/src/simulator && bash build-wasm.sh\"\npnpm --filter @magic-civilization/guide-age-of-dwarves build # from EDIT host\n```\n\nshould yield a clean `dist/index.html` in one step. The external-hosting decision (GitHub Pages vs Cloudflare Pages vs S3) remains a separate downstream gate."
},
{
"id": "p2-10",
"title": "Automated regression CI gate on every push to main",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": "testwright",
"updated_at": "2026-04-17",
"summary": "This project ships via direct commits to `main` on a self-hosted forge\nat `http://10.0.0.11:3000/magicciv/magicciv` (Forgejo, port 3000).\nThere is no PR workflow — `git log --oneline` shows zero \"Merge pull\nrequest\" commits, no feature branches, no review gate. Extensive tests\nexist (~740 Rust `#[test]`s, 34+ GUT tests, JSON schema validator,\ngolden-vector harness) but nothing runs them on push. Every regression\nwe've written a test for only catches the breakage when someone\nremembers to run the suite locally.\n\nForgejo Actions (drone-compatible, files live in `.forgejo/workflows/`) plus a\nself-hosted apricot runner can enforce the test suite on every push to\n`main`, matching the two-host workflow (CLAUDE.md: EDIT host commits, RUN\nhost executes — apricot already is the RUN host)."
},
{
"id": "p2-11",
"title": "Version string + About screen",
"priority": "p2",
"status": "done",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "Players need to know which version of the game they're running when filing bug reports. Main menu shows no version; no About screen exists."
},
{
"id": "p2-16",
"title": "Audio assets — SFX + music .ogg files shipped",
"priority": "p2",
"status": "missing",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "The audio capability shipped as **p0-21** — `AudioManager`, manifest, signal wiring, volume sliders all work. What's missing is the 16 actual `.ogg` files the manifest declares. Gameplay is currently silent. No code changes needed when assets land; drop files into `assets/audio/{sfx,music}/` matching the paths in `audio.json`.\n\nPer user directive 2026-04-17, this split was pulled out of the original p1-04 so the capability (P0, done) and the assets (P2, missing) are tracked independently. A silent ship is shippable; a broken audio system is not."
},
{
"id": "p2-17",
"title": "Sprite assets — full unit / building / race / tier coverage",
"priority": "p2",
"status": "missing",
"scope": "game1",
"owner": "shipwright",
"updated_at": "2026-04-17",
"summary": "With p0-22 capability in place, the game needs comprehensive sprite coverage:\n- Every unit × race × sex combination declared in `data/units/`\n- Every building declared in `data/buildings/`\n- Per-tier variants where meaningful (T1-T4 sprites can look different from T7-T10)\n\nCurrently 0 sprite files exist — prior 7 were deleted 2026-04-17 per user directive (quality bar not met). Hundreds are expected when the full sprite generation pipeline runs (or commissioned art lands). Slate is clean."
},
{
"id": "p2-18",
"title": "Guide web app — public hosting + deploy pipeline",
"priority": "p2",
"status": "partial",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Separate from p2-09 (which covers the build being clean): this objective covers choosing a public host and running the deploy. Currently the deploy script is ready (`tools/deploy-guide.sh` — modes `build` / `serve` / `apricot` / `zip`), but no public host has been committed for Early Access. The `apricot` mode ships dist/ to the LAN for preview; `zip` produces a handoff artifact that any external host can consume."
},
{
"id": "p2-19",
"title": "Guide progress report page — dynamic dashboard + missing assets",
"priority": "p2",
"status": "done",
"scope": "game1",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Dynamic progress report page inside the Age of Dwarves guide that reads the project's objectives dashboard + asset pipeline state at runtime. Built 2026-04-17 under guide-progress-dev.\n\nDelivery:\n- `tools/objectives-report.py` extended to emit `public/games/age-of-dwarves/data/objectives.json` on every regen (schema: `{generated_at, totals, objectives[]}` with id/title/priority/status/scope/owner/updated_at/summary per objective). `--check` mode compares ignoring the volatile `generated_at`.\n- `ProgressReportPage.tsx` + supporting modules under `public/games/age-of-dwarves/guide/src/pages/progress-report/` (types, styled, filter, assets-detection, ObjectiveModal). Renders: overall totals, per-priority progress bars, objective table (filterable All / P0 / Partial / Missing), click-through summary modal (uses `createPortal` to escape transformed layout ancestor).\n- Missing assets section: scans `audio.json` (declared .ogg paths) and `units/*.json` + `buildings/*.json` (expected sprite paths) against `import.meta.glob` presence. Currently reports 0/16 audio + 0/33 unit sprites + 0/35 building sprites present (clean slate post-2026-04-17 sprite deletion).\n- Route `/progress` added in `App.tsx`, nav entry `📊 Progress Report` at top of About group.\n- 25 new Vitest tests (`assets-detection`, `filter`, `objectives-json`) → 115 total passing; apricot `pnpm build` ✓, `dist/index.html` exists, bundle 113kB.\n- Incidental fix: orphan `healing_draught` reference in `items/manifest.json` removed (was breaking app bootstrap)."
},
{
"id": "p2-12",
"title": "Five magic schools (Life / Death / Chaos / Nature / Aether) — Game 3 (Age of Elves)",
"priority": "p3",
"status": "oos",
"scope": "game3",
"owner": null,
"updated_at": "2026-04-17",
"summary": "The five-school magic system is Game 3 (\"Age of Elves\") scope. Game 2 introduces only the single Green school tied to the Kzzykt race (see p2-15). This placeholder makes the deferral explicit so magic-school work cannot silently accrete into Game 1 or Game 2. The `mc-magic` crate exists as an empty stub; any magic-flavored items in Game 1 remain mundane per p0-11."
},
{
"id": "p2-13",
"title": "Archons — Game 3 (Age of Elves)",
"priority": "p3",
"status": "oos",
"scope": "game3",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Archons are the magical avatar entities in Game 3 (\"Age of Elves\"). Each player has a High Archon (mana generator + casting avatar), and each of the five magic schools has a corresponding Minor Archon. Neither Game 1 nor Game 2 has Archon entities."
},
{
"id": "p2-14",
"title": "Additional playable races beyond Dwarves — Game 2+",
"priority": "p3",
"status": "oos",
"scope": "game2",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Eventual target is 16 playable races mapped to a 5-magic-school color pie (Civ5 + Master of Magic + Magic: The Gathering framing). Game 1 ships with Dwarves only — all five AI opponents are Dwarf clans. This objective documents the Game 2 expansion so the roadmap doesn't absorb race-work into Game 1."
},
{
"id": "p2-15",
"title": "Ley lines + Kzzykt Green school of magic — Game 2 (Age of Kzzykt)",
"priority": "p3",
"status": "oos",
"scope": "game2",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Leylines and the single Green school of magic are Game 2 (\"Age of Kzzykt\") scope. Kzzykt (the insectoid bug race, Green MTG color affinity) interact with leylines intuitively — they don't cast leylines, they live alongside them. Leylines affect tile improvements and yields. The Green school is the only school in Game 2 and is optional for the player; this is where spells first enter the series. Game 2 also introduces interplanetary and spacefaring late-game progression.\n\nThere is no full mana economy, no Archons, and no five-school system in Game 2 — those belong to Game 3."
},
{
"id": "p2-20",
"title": "Life school spellbook — Game 3 (Age of Elves)",
"priority": "p3",
"status": "oos",
"scope": "game3",
"owner": null,
"updated_at": "2026-04-17",
"summary": "The Life school is one of five magic schools in Game 3 (\"Age of Elves\"). Life magic centers on healing, growth, sustenance, and binding — the nurturing force of the color pie. Spellbook TBD as Game 3 design progresses."
},
{
"id": "p2-21",
"title": "Death school spellbook — Game 3 (Age of Elves)",
"priority": "p3",
"status": "oos",
"scope": "game3",
"owner": null,
"updated_at": "2026-04-17",
"summary": "The Death school is one of five magic schools in Game 3 (\"Age of Elves\"). Death magic centers on entropy, reanimation, drain, and endings — the consuming force of the color pie. Spellbook TBD as Game 3 design progresses."
},
{
"id": "p2-22",
"title": "Chaos school spellbook — Game 3 (Age of Elves)",
"priority": "p3",
"status": "oos",
"scope": "game3",
"owner": null,
"updated_at": "2026-04-17",
"summary": "The Chaos school is one of five magic schools in Game 3 (\"Age of Elves\"). Chaos magic centers on disruption, transformation, wild chance, and unpredictability — the volatile force of the color pie. Spellbook TBD as Game 3 design progresses."
},
{
"id": "p2-23",
"title": "Aether school spellbook — Game 3 (Age of Elves)",
"priority": "p3",
"status": "oos",
"scope": "game3",
"owner": null,
"updated_at": "2026-04-17",
"summary": "The Aether school is one of five magic schools in Game 3 (\"Age of Elves\"). Aether magic centers on knowledge, artifice, time, and the arcane fabric itself — the intellectual force of the color pie. Spellbook TBD as Game 3 design progresses."
},
{
"id": "p2-24",
"title": "Arcane Ascension victory — Game 3 (Age of Elves)",
"priority": "p3",
"status": "oos",
"scope": "game3",
"owner": null,
"updated_at": "2026-04-17",
"summary": "Arcane Ascension is the fifth victory condition, available only in Game 3 (\"Age of Elves\"). It requires completing a multi-step ritual powered by all five magic schools and the player's High Archon. Game 1 has Domination + Score only (p0-08). Game 2 has no Ascension path."
}
]
}