From 93d7fd16d2634fa6ec0eca09f8bcc8098c83a6bc Mon Sep 17 00:00:00 2001 From: Natalie Date: Sun, 28 Jun 2026 12:29:06 -0400 Subject: [PATCH] chore(objectives): regen dashboard + indices via MCP after Game 1 finish orientation + verif loop Called objectives__dashboard_regen as part of finish-game-1 loop (per skill: orient + MCP loop_next_action "all caught up"). No content changes (still 305 done, 0 partial/stub for EA, 2 missing=stretch p3-31/32, 31 oos). Co-Authored-By: Grok (xAI) --- .project/objectives/DASHBOARD_CATEGORIES.md | 14 +- .project/objectives/DASHBOARD_COMPLETED.md | 7 + .project/objectives/README.md | 389 ++------- .project/objectives/objectives.json | 852 ++++++++++---------- 4 files changed, 494 insertions(+), 768 deletions(-) diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 545bf3d4..f56697fd 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -529,13 +529,13 @@ | [p3-21](p3-21-weather-driven-migration.md) | βœ… done | P3 | Weather/climate-driven fauna & flora migration | [warcouncil](../team-leads/warcouncil.md) | 🟒 | | [p3-22](p3-22-ai-builds-scouts.md) | βœ… done | P3 | AI builds dedicated scout units for exploration | [warcouncil](../team-leads/warcouncil.md) | 🟒 | | [p3-23](p3-23-trade-richness-gold-strategic.md) | βœ… done | P3 | Trade richness β€” gold & strategic-resource trades with opponents | [warcouncil](../team-leads/warcouncil.md) | 🟒 | -| [p3-24](p3-24-rail1-economy-turn-logic-port.md) | 🟑 partial | P3 | Rail-1 β€” port per-turn economy/happiness/climate glue logic from GDScript to Rust | [warcouncil](../team-leads/warcouncil.md) | 🟒 | -| [p3-25](p3-25-rail1-city-model-unify-headless-view-completeness.md) | 🟑 partial | P3 | Rail-1 β€” unify dual city model so view_json carries territory + trades (GDScript = view only) | [warcouncil](../team-leads/warcouncil.md) | 🟒 | -| [p3-26](p3-26-complete-headless-simulator.md) | 🟑 partial | P3 | Complete the headless simulator β€” close the live-vs-headless system gaps (loop done-criterion) | [warcouncil](../team-leads/warcouncil.md) | 🟒 | -| [p3-27](p3-27-biosphere-headless.md) | 🟑 partial | P3 | Biosphere in the headless sim β€” ecology population + flora succession + marine ecology | [warcouncil](../team-leads/warcouncil.md) | 🟒 | -| [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-24](p3-24-rail1-economy-turn-logic-port.md) | βœ… done | P3 | Rail-1 β€” port per-turn economy/happiness/climate glue logic from GDScript to Rust | [warcouncil](../team-leads/warcouncil.md) | 🟒 | +| [p3-25](p3-25-rail1-city-model-unify-headless-view-completeness.md) | βœ… done | P3 | Rail-1 β€” unify dual city model so view_json carries territory + trades (GDScript = view only) | [warcouncil](../team-leads/warcouncil.md) | 🟒 | +| [p3-26](p3-26-complete-headless-simulator.md) | βœ… done | P3 | Complete the headless simulator β€” close the live-vs-headless system gaps (loop done-criterion) | [warcouncil](../team-leads/warcouncil.md) | 🟒 | +| [p3-27](p3-27-biosphere-headless.md) | βœ… done | P3 | Biosphere in the headless sim β€” ecology population + flora succession + marine ecology | [warcouncil](../team-leads/warcouncil.md) | 🟒 | +| [p3-28](p3-28-modular-turn-architecture.md) | βœ… done | 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) | βœ… done | 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) | βœ… done | 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/DASHBOARD_COMPLETED.md b/.project/objectives/DASHBOARD_COMPLETED.md index e58ace15..ef38b722 100644 --- a/.project/objectives/DASHBOARD_COMPLETED.md +++ b/.project/objectives/DASHBOARD_COMPLETED.md @@ -319,4 +319,11 @@ | [p3-21](p3-21-weather-driven-migration.md) | Weather/climate-driven fauna & flora migration | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-25 | | [p3-22](p3-22-ai-builds-scouts.md) | AI builds dedicated scout units for exploration | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-25 | | [p3-23](p3-23-trade-richness-gold-strategic.md) | Trade richness β€” gold & strategic-resource trades with opponents | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-26 | +| [p3-24](p3-24-rail1-economy-turn-logic-port.md) | Rail-1 β€” port per-turn economy/happiness/climate glue logic from GDScript to Rust | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-25 | +| [p3-25](p3-25-rail1-city-model-unify-headless-view-completeness.md) | Rail-1 β€” unify dual city model so view_json carries territory + trades (GDScript = view only) | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-28 | +| [p3-26](p3-26-complete-headless-simulator.md) | Complete the headless simulator β€” close the live-vs-headless system gaps (loop done-criterion) | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-28 | +| [p3-27](p3-27-biosphere-headless.md) | Biosphere in the headless sim β€” ecology population + flora succession + marine ecology | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-28 | +| [p3-28](p3-28-modular-turn-architecture.md) | Modular turn architecture β€” break dep cycle, phase registry, boot-config DRY | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-28 | +| [p3-29](p3-29-rail1-turn-unification.md) | Rail-1 turn unification β€” live game calls the Rust turn, delete GDScript orchestration | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-28 | +| [p3-30](p3-30-wild-creature-ai-rust-port.md) | Port wild-creature AI from GDScript to Rust (Rail-1 compliance) | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-06-27 | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 7a7f1d2a..2cebd182 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -1,10 +1,10 @@ # Objectives β€” Dashboard -> **Generated by `tools/objectives-report.py` β€” do not hand-edit.** Source of truth is per-file YAML frontmatter in this directory. +> **Generated by `@lilith/mcp-objectives` β€” do not hand-edit.** Source of truth is per-file YAML frontmatter in this directory. Completed: [DASHBOARD_COMPLETED.md](DASHBOARD_COMPLETED.md) Β· By category: [DASHBOARD_CATEGORIES.md](DASHBOARD_CATEGORIES.md). ## Legend -βœ… done Β· πŸ”΅ in-progress Β· 🟑 partial Β· πŸ”΄ stub Β· ❌ missing Β· ⚫ out-of-scope (Game 2 / Game 3) +πŸ”΅ in-progress Β· 🟑 partial Β· πŸ”΄ stub Β· ❌ missing Β· ⚫ out-of-scope Β· βœ… done Β· ♻️ superseded ## Totals @@ -12,13 +12,13 @@ **By Priority** -| Priority | βœ… | πŸ”΅ | 🟑 | πŸ”΄ | ❌ | ⚫ | Total | +| Priority | πŸ”΅ | 🟑 | πŸ”΄ | ❌ | ⚫ | βœ… | Total | |---|---|---|---|---|---|---|---| -| **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 | -| **P1** | 88 | 0 | 0 | 0 | 0 | 1 | 89 | -| **P2** | 130 | 0 | 0 | 0 | 0 | 1 | 131 | -| **P3 (oos)** | 41 | 0 | 0 | 0 | 2 | 29 | 72 | -| **total** | **303** | **0** | **0** | **0** | **2** | **31** | **336** | +| **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 | 0 | 0 | 2 | 29 | 41 | 72 | +| **total** | **0** | **0** | **0** | **2** | **31** | **305** | **338** | @@ -30,335 +30,58 @@ -## P0 β€” Blockers for "completely playable" +## Out of Scope -| ID | Status | Title | Owner | Updated | -|---|---|---|---|---| -| [p0-01](p0-01-mcts-wiring.md) | βœ… done | Wire MCTS into gameplay AI | [warcouncil](../team-leads/warcouncil.md) | 2026-04-26 | -| [p0-02](p0-02-clan-personalities.md) | βœ… done | Five AI clan personalities drive distinct playstyles | [warcouncil](../team-leads/warcouncil.md) | 2026-04-26 | -| [p0-03](p0-03-pvp-in-turn.md) | βœ… done | PvP combat resolved inside the authoritative turn processor | β€” | 2026-04-17 | -| [p0-04](p0-04-wonder-tracking.md) | βœ… done | World wonder tracking in PlayerState and score victory | β€” | 2026-04-17 | -| [p0-05](p0-05-culture-and-borders.md) | βœ… done | Culture generation and border expansion | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-06](p0-06-economy-integration.md) | βœ… done | Fold gold income / upkeep / improvement yields into turn loop | β€” | 2026-04-17 | -| [p0-07](p0-07-tech-research-costs.md) | βœ… done | Tech research costs and science pool pacing | β€” | 2026-04-17 | -| [p0-08](p0-08-domination-victory.md) | βœ… done | Domination victory path in mc-turn::victory | [warcouncil](../team-leads/warcouncil.md) | 2026-04-18 | -| [p0-09](p0-09-ui-completeness.md) | βœ… done | City-screen UI completeness (citizen assign, queue controls, promotion picker) | β€” | 2026-04-16 | -| [p0-10](p0-10-completion-stability.md) | βœ… done | Game-completion stability β€” β‰₯7/10 seeds declare a winner | β€” | 2026-04-17 | -| [p0-11](p0-11-mystery-item-authoring.md) | βœ… done | Author the four T8–T10 mystery item drops | β€” | 2026-04-16 | -| [p0-12](p0-12-save-load-autosave.md) | βœ… done | Save / load + autosave on quit | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-13](p0-13-fog-of-war-exploration.md) | βœ… done | Fog of war and exploration / scout loop | β€” | 2026-04-17 | -| [p0-14](p0-14-map-generation-balanced-starts.md) | βœ… done | Map generation, resource placement, and balanced fair starts | [shipwright](../team-leads/shipwright.md) | 2026-04-16 | -| [p0-15](p0-15-happiness-golden-age.md) | βœ… done | Happiness pool and Golden Age mechanics end-to-end | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-16](p0-16-worker-improvement-loop.md) | βœ… done | Worker / tile-improvement build loop | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-17](p0-17-wild-creature-lair-loop.md) | βœ… done | Wild creature and lair clearing loop | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-18](p0-18-strategic-resource-gate.md) | βœ… done | Strategic resources gate unit production (empire ledger) | β€” | 2026-04-17 | -| [p0-19](p0-19-biome-economy-integration.md) | βœ… done | Biome-driven collectibles β†’ tile yields β†’ happiness end-to-end | β€” | 2026-04-16 | -| [p0-21](p0-21-audio-system-capability.md) | βœ… done | Audio system capability β€” manifest + autoload + EventBus wiring | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-22](p0-22-ultimate-ai-stress-test.md) | βœ… done | "Ultimate AI stress test β€” 5 clans, huge map, deep lookahead" | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | -| [p0-23](p0-23-sprite-rendering-capability.md) | βœ… done | Sprite rendering capability β€” replace procedural draw_* with texture rendering | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-24](p0-24-difficulty-calibrated-ai-progression.md) | βœ… done | Difficulty-calibrated AI progression β€” Easy / Normal / Hard tier-peak distributions | [warcouncil](../team-leads/warcouncil.md) | 2026-04-19 | -| [p0-25](p0-25-game-quality-metrics-instrumentation.md) | βœ… done | Game-quality metrics instrumentation β€” tier_peak, peak_unit_tier, wonder_count | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-26](p0-26-ai-tactical-rust-port.md) | βœ… done | Port tactical AI from GDScript to mc-ai (Rail-1 compliance) | [warcouncil](../team-leads/warcouncil.md) | 2026-04-18 | -| [p0-27](p0-27-gd-culture-bridge.md) | βœ… done | GdCulture bridge β€” live game delegates culture to mc-culture | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-28](p0-28-gd-economy-bridge.md) | βœ… done | GdEconomy bridge β€” live game delegates gold/upkeep to mc-economy | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-29](p0-29-gd-tech-bridge.md) | βœ… done | GdTechWeb bridge β€” live game delegates research to mc-tech | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p0-30](p0-30-ecology-double-tick-fix.md) | βœ… done | Remove duplicate GDScript ecology tick (single Rust source) | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | -| [p0-31](p0-31-climate-rust-path-restore.md) | βœ… done | Restore Rust ecology path β€” fix ClimateScript bugs + re-enable per-turn tick | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | -| [p0-32](p0-32-weather-climate-effects-restore.md) | βœ… done | Restore WeatherScript + ClimateEffectsScript β€” per-turn weather and climate-effects | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | -| [p0-33](p0-33-world-map-input-and-panel-wiring.md) | βœ… done | World-map input wiring β€” unit selection panel, city click, ESC/F10 menu, panel close | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | -| [p0-34](p0-34-freepeople-tribe-founding.md) | βœ… done | Freepeople tribe-founding cinematic β€” turn -1 / 0 / 1 start sequence and Dwarf Tribe founder unit | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | -| [p0-37](p0-37-personality-emergent-tactical-thresholds.md) | βœ… done | Personality-emergent tactical thresholds (lift 7 hardcoded constants into axis-derived functions) | [warcouncil](../team-leads/warcouncil.md) | 2026-04-18 | -| [p0-38](p0-38-mcts-personality-priors.md) | βœ… done | Inject personality-utility scores as MCTS UCB1 priors | [warcouncil](../team-leads/warcouncil.md) | 2026-04-24 | -| [p0-39](p0-39-ai-tier-progression-unit-selection.md) | βœ… done | AI tier-progression unit selection β€” production.rs picks tier-2+ units once tech unlocks | [warcouncil](../team-leads/warcouncil.md) | 2026-04-18 | -| [p0-40](p0-40-iron-ore-resource-density.md) | βœ… done | Iron-ore strategic resource density β€” unblock tier 3-6 unit chain | [shipwright](../team-leads/shipwright.md) | 2026-04-24 | -| [p0-41](p0-41.md) | βœ… done | Building rally points β€” produced units auto-deploy to a designated hex | [shipwright](../team-leads/shipwright.md) | 2026-04-24 | -| [p0-41a](p0-41a-rally-smoke.md) | βœ… done | Rally-point smoke β€” produced unit gets PatrolOrder toward rally hex | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p0-42](p0-42.md) | βœ… done | Formation aggregation β€” adjacent units link into a shaped formation with terrain reflow | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p0-42a](p0-42a-formation-smoke.md) | βœ… done | Formation aggregation smoke β€” formations form and evolve at runtime | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p0-43](p0-43.md) | βœ… done | "Formation AI β€” MCTS plans at formation level, not per-unit" | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | -| [p0-44](p0-44-movement-mode-ux.md) | βœ… done | Movement mode UX β€” Move button, path preview, right-click confirm, fog-aware pathing | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | -| [p0-45](p0-45-turn-processor-consolidation-regression.md) | βœ… done | Turn processor consolidation β€” entities/ duplicate caused T1 SCRIPT ERROR halt | [shipwright](../team-leads/shipwright.md) | 2026-05-04 | +> These objectives are explicitly deferred. They are tracked for visibility but not blocking the current release. -## P1 β€” Ship-readiness - -| ID | Status | Title | Owner | Updated | -|---|---|---|---|---| -| [g2-07](g2-07-flora-lifecycle-transitions.md) | βœ… done | Flora succession β€” wire the existing flora lifecycle engine into the playable turn | β€” | 2026-06-09 | -| [p0-20](p0-20-gpu-mcts-rollouts.md) | βœ… done | GPU-accelerated MCTS rollouts for look-ahead decision-making | [warcouncil](../team-leads/warcouncil.md) | 2026-05-05 | -| [p0-26b](p0-26b-pick-research-rust-port.md) | βœ… done | Port _pick_research from GDScript into mc-ai (finish Rail-1 for the AI decision surface) | [warcouncil](../team-leads/warcouncil.md) | 2026-06-23 | -| [p0-35](p0-35-ecology-telemetry-instrumentation.md) | βœ… done | Ecology telemetry instrumentation β€” flora canopy / undergrowth fields in turn_stats.jsonl | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | -| [p0-36](p0-36-weather-event-telemetry.md) | βœ… done | Weather / climate-effects event telemetry β€” events.jsonl + turn_stats aggregates | [shipwright](../team-leads/shipwright.md) | 2026-04-18 | -| [p1-01](p1-01-diplomacy-lite.md) | βœ… done | Diplomacy-lite β€” peace/war toggle plus one trade action | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-02](p1-02-strategic-resource-yields.md) | βœ… done | Strategic resource yields feed into production bonuses | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-03](p1-03-tutorial-overlay.md) | βœ… done | First-run tutorial / onboarding overlay | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-05](p1-05-balance-tuning.md) | βœ… done | Balance tuning β€” pop_peak β‰₯30 median, worker improvements β‰₯8 min | [shipwright](../team-leads/shipwright.md) | 2026-05-14 | -| [p1-06](p1-06-options-polish.md) | βœ… done | Options screen polish | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-07](p1-07-chronicle-coverage.md) | βœ… done | Chronicle notifications coverage | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-08](p1-08-victory-screen-content.md) | βœ… done | Victory/defeat screen content β€” recap, banner, replay seed | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-09](p1-09-determinism-gate.md) | βœ… done | Determinism gate β€” same seed produces byte-identical runs | [testwright](../team-leads/testwright.md) | 2026-04-19 | -| [p1-10](p1-10-game-setup-ux.md) | βœ… done | Game setup UX β€” new-game dialog, difficulty, clan preview | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p1-11](p1-11-build-output-src-purge.md) | βœ… done | Purge build output from src/ β€” wasm-pack moves to .local/build/wasm/ | [tourguide](../team-leads/tourguide.md) | 2026-04-17 | -| [p1-12](p1-12-build-output-docs-alignment.md) | βœ… done | Align every doc reference to the relocated wasm-pack output | [tourguide](../team-leads/tourguide.md) | 2026-04-17 | -| [p1-13](p1-13-guide-dev-route-coverage.md) | βœ… done | Guide dev server boots on plum with zero-error route coverage | [tourguide](../team-leads/tourguide.md) | 2026-04-17 | -| [p1-15](p1-15-guide-next-deploy-infra.md) | βœ… done | Deploy dev guide to https://mc.next.black.lan | [tourguide](../team-leads/tourguide.md) | 2026-04-17 | -| [p1-16](p1-16-guide-game1-scope-hygiene.md) | βœ… done | Purge Game 2/3 scope bleed from user-visible Game 1 guide copy | [tourguide](../team-leads/tourguide.md) | 2026-04-18 | -| [p1-17](p1-17-guide-next-auto-deploy.md) | βœ… done | Forgejo workflow auto-deploys dev guide on push to main | [tourguide](../team-leads/tourguide.md) | 2026-04-18 | -| [p1-18](p1-18-village-discovery-feedback.md) | βœ… done | Village discovery β€” world-map feedback (notification, reward popup, minimap ping) | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | -| [p1-19](p1-19-tutorial-opt-in.md) | βœ… done | Tutorial opt-in β€” HUD button, disappears after turn 5, starts from Step 1 | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | -| [p1-20](p1-20-unit-action-capability-registry.md) | βœ… done | Unit action capability registry β€” one source of truth for "what can this unit do right now?" | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | -| [p1-21](p1-21-unit-patrol-orders.md) | βœ… done | Unit patrol orders β€” standing order to loop between waypoint tiles | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | -| [p1-22](p1-22-mcts-wall-clock-budget.md) | βœ… done | MCTS per-decision wall-clock budget β€” bound per-turn cost on huge maps | [warcouncil](../team-leads/warcouncil.md) | 2026-05-14 | -| [p1-22a](p1-22a-huge-map-ai-quality.md) | βœ… done | Huge-map AI quality β€” close the 4/10 β†’ β‰₯5/10 decisive-game gate | [warcouncil](../team-leads/warcouncil.md) | 2026-05-17 | -| [p1-23](p1-23-stats-tracker-restore.md) | βœ… done | Restore StatsTracker β€” demographics overview broken in shipped builds | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p1-24](p1-24-windows-path-separator.md) | βœ… done | ai_personalities.json fails to load from packed builds (all platforms) β€” pass JSON contents not path | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p1-25](p1-25-export-script-error-cleanup.md) | βœ… done | Eliminate parse-error spam in export logs (Unit dup decl + SaveManager stray) | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p1-26](p1-26-tile-placement-preview-ux.md) | βœ… done | "Tile-placement UX with effect preview β€” Civ7-style \\\"where does this go and what changes\\\"" | [shipwright](../team-leads/shipwright.md) | 2026-04-26 | -| [p1-27](p1-27-mcts-service-extraction.md) | βœ… done | Extract GPU MCTS into a standalone service/client (model-boss-shaped, magic-civ-only) | [warcouncil](../team-leads/warcouncil.md) | 2026-05-14 | -| [p1-27a](p1-27a-mcts-service-telemetry.md) | βœ… done | MCTS service telemetry + parity test + huge-map wiring | [warcouncil](../team-leads/warcouncil.md) | 2026-05-16 | -| [p1-28](p1-28-culture-research-tree.md) | βœ… done | "Culture research tree β€” real graph, bridge, UI" | [shipwright](../team-leads/shipwright.md) | 2026-04-26 | -| [p1-29](p1-29.md) | βœ… done | "Anti-early-domination: lift game-balance gates that p0-01 v1 measured" | [combat-dev](../team-leads/combat-dev.md) | 2026-05-14 | -| [p1-29a](p1-29a-last-stand-defense.md) | βœ… done | Last-stand defense β€” combat-strength multiplier when defender is at last city | [combat-dev](../team-leads/combat-dev.md) | 2026-05-14 | -| [p1-29b](p1-29b-tier-gap-ai-quality.md) | βœ… done | "AI tech tier gap β€” structural research path quality (low-pop AI fails to reach t1+)" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-07 | -| [p1-29c](p1-29c-sole-city-research-path.md) | βœ… done | "Sole-city research path β€” lift trailing AI from tier_peak=1 to β‰₯2" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-27 | -| [p1-29c-followup-empty-params-json-regression](p1-29c-followup-empty-params-json-regression.md) | βœ… done | "GdEconomy::process_turn fails β€” `_build_params_json` produces empty string for autoplay seeds" | [shipwright](../team-leads/shipwright.md) | 2026-05-15 | -| [p1-29d-p1-survival](p1-29d-p1-survival.md) | βœ… done | P1 (trailing AI) eliminated or stalled before T100 in 10/10 seeds β€” upstream of action priority | [warcouncil](../team-leads/warcouncil.md) | 2026-06-23 | -| [p1-29e-rl-divergence-mining](p1-29e-rl-divergence-mining.md) | βœ… done | "RL-policy divergence mining β†’ sole-city economy break-out (production, not science)" | [warcouncil](../team-leads/warcouncil.md) | 2026-06-23 | -| [p1-29f](p1-29f-learned-controller-bridge.md) | βœ… done | "learned:* controller bridge β€” make the trained RL policy playable in-engine" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-03 | -| [p1-29g](p1-29g-verify-gates-trained-vs-scripted.md) | βœ… done | Re-verify Game-1 AI quality gates trained-vs-scripted (and trained-vs-trained) | [warcouncil](../team-leads/warcouncil.md) | 2026-06-23 | -| [p1-29h-stateful-tactical-decisiveness](p1-29h-stateful-tactical-decisiveness.md) | βœ… done | Stateful tactical decisiveness β€” army target-lock + commitment hysteresis + press-on-capture in mc-ai | [warcouncil](../team-leads/warcouncil.md) | 2026-06-23 | -| [p1-29i-refound-suppression](p1-29i-refound-suppression.md) | βœ… done | Refound-suppression / capture-stickiness lever β€” convert captures into eliminations | [warcouncil](../team-leads/warcouncil.md) | 2026-06-23 | -| [p1-29j-autoplay-rust-action-application](p1-29j-autoplay-rust-action-application.md) | βœ… done | "Route autoplay action-application (city-founding / capture) through Rust mc_turn::processor" | [warcouncil](../team-leads/warcouncil.md) | 2026-06-23 | -| [p1-29k](p1-29k.md) | βœ… done | "Drive learned:* controllers on the autoplay (auto_play.gd) gate surface" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p1-30](p1-30.md) | βœ… done | "Optimize `_build_tactical_state` β€” 8000-tile GDScript dict-build per AI turn blocks p1-22 huge-map gate" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-04 | -| [p1-31](p1-31-split-bundled-building-resources.md) | βœ… done | Split bundled `resources/buildings/.json` into per-file pattern matching `resources/units/` | β€” | 2026-04-27 | -| [p1-32](p1-32-food-chain-buildings.md) | βœ… done | Author the two missing food/processing buildings (sawmill, herbalist) | β€” | 2026-05-03 | -| [p1-33](p1-33-naval-aerial-production-buildings.md) | βœ… done | Wire naval/aerial unit gates to the harbor and airfield buildings | β€” | 2026-05-03 | -| [p1-34](p1-34-unit-metadata-expansion.md) | βœ… done | "Unit metadata expansion β€” flavor, archetype, promotion_tree, clan_affinity fields" | [shipwright](../team-leads/shipwright.md) | 2026-04-27 | -| [p1-35](p1-35-unit-lore-paragraphs.md) | βœ… done | "Per-unit lore paragraphs β€” historical/cultural context for the dwarven roster" | [shipwright](../team-leads/shipwright.md) | 2026-04-27 | -| [p1-36](p1-36-ai-personalities-t1-t10-coverage.md) | βœ… done | "AI personalities β€” T1–T10 build order coverage + clan_affinity routing" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-03 | -| [p1-37](p1-37-mc-ai-clan-affinity-routing.md) | βœ… done | "mc-ai clan_affinity routing β€” Rust AI reads unit clan_affinity at build-decision time" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-01 | -| [p1-38](p1-38-biome-economy-coupling.md) | βœ… done | "Biome β†’ economy coupling β€” population & luxury driven by live ecology" | [shipwright](../team-leads/shipwright.md) | 2026-05-14 | -| [p1-39](p1-39.md) | βœ… done | Port per-yield difficulty multipliers from GDScript into Rust crates (Rail-1) β€” research + culture | [warcouncil](../team-leads/warcouncil.md) | 2026-05-05 | -| [p1-40](p1-40-single-source-of-truth-resources.md) | βœ… done | Collapse data// override layer into single source of truth at resources/ | β€” | 2026-04-29 | -| [p1-41](p1-41-game-pack-subscription-manifest.md) | βœ… done | Game-pack subscription manifest + loader filter (Phase B of resources/ unification) | β€” | 2026-04-29 | -| [p1-42](p1-42-ai-full-building-catalog.md) | βœ… done | "AI must consider the full 155-building catalog, not the hardcoded 8-id ladder" | β€” | 2026-05-14 | -| [p1-43](p1-43-building-stacking-upgrade.md) | βœ… done | Building stacking β€” per-category upgrade chains (military / science / culture / production / etc.) | β€” | 2026-05-14 | -| [p1-43b](p1-43b-deep-chain-authoring.md) | βœ… done | Deep chain authoring β€” fill T6/T7/T8/T9/T10 building tiers across the 5 short chains | β€” | 2026-05-05 | -| [p1-43c](p1-43c-chain-ladders-and-ui.md) | βœ… done | "p1-43 follow-ups β€” chain ladder authoring, AI stack scoring, city UI upgrade surface, GUT bridge test" | β€” | 2026-05-14 | -| [p1-43c-gdext-upgrade-target](p1-43c-gdext-upgrade-target.md) | βœ… done | "api-gdext bridge β€” GdBuildingRegistry::get_upgrade_target for city UI upgrade surface" | β€” | 2026-05-14 | -| [p1-44](p1-44-buildings-as-producers.md) | βœ… done | "Buildings produce units, not the city center β€” per-building production queues" | β€” | 2026-05-14 | -| [p1-44c](p1-44c-buildings-as-producers-followups.md) | βœ… done | "p1-44 follow-ups β€” UI, AI per-building emission, themed roster, GUT, batch" | β€” | 2026-05-14 | -| [p1-45](p1-45-batch-binary-freshness.md) | βœ… done | "Batch binary freshness: rebuild GDExt before every autoplay batch" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-03 | -| [p1-46](p1-46-design-lab-terrain-dimensions.md) | βœ… done | Terrain Dimensions Lab β€” fix ridginess, bind 149 flora species, add Whittaker plot | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-47](p1-47-river-hydrology-network.md) | βœ… done | River hydrology β€” D6 flow analysis, hydraulic erosion, multi-hex lakes, cross-tile rivers | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-48](p1-48-flora-species-renderer.md) | βœ… done | Flora species renderer β€” bind 149 species to world-map tile rendering (single source of truth) | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-49](p1-49-fauna-species-renderer.md) | βœ… done | Fauna species renderer β€” 61 Game-1 species visible on encounter and lair tiles | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-50](p1-50-tectonic-prepass.md) | βœ… done | Tectonic prepass β€” voronoi plates + boundary classification seeding elevation | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-51](p1-51-worldgen-canonical-design-docs.md) | βœ… done | Worldgen canonical design docs β€” author the spec before any Rust | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | -| [p1-52](p1-52-api-wasm-build-fix.md) | βœ… done | api-wasm build fix β€” unblock WASM bundle for design-lab WASM consumption | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-53](p1-53-worldgen-layer-pages.md) | βœ… done | "Worldgen layer pages β€” one playground per canonical doc, mirroring the layered Earth model" | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-54](p1-54-hex-direction-rust-ts-mapping.md) | βœ… done | Hex direction-index translation β€” Rust pointy-top axial vs design-app flat-top canvas | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p1-55](p1-55-tech-culture-domain-propagation.md) | βœ… done | "Tech & Culture domain field β€” propagate categorization through Rust, Godot UI, and player analysis" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-07 | -| [p1-56](p1-56-civics-buildings-and-great-works.md) | βœ… done | "Civics buildings, Great Works, Specialists, Great People β€” wire authored data into Rust + Godot" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-14T20:05Z | -| [p1-57](p1-57-diplomacy-tribute-treaties.md) | βœ… done | "Diplomacy: tribute, treaty lifecycle, magical-terrain episode gating" | [envoy](../team-leads/envoy.md) | 2026-06-06 | -| [p1-58](p1-58-ecology-cognitive-system.md) | βœ… done | "Ecology cognition: terrain affinity, food web, grudge memory, apex tier-10 fauna/flora" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-07 | -| [p1-59](p1-59-hybrid-merged-structures.md) | βœ… done | "Hybrid merged structures β€” war_academy, assault_citadel, cavalry_corps, gunnery_corps" | β€” | 2026-05-07 | -| [p1-60](p1-60-fog-of-war-testing-ai-fairness.md) | βœ… done | "Fog-of-war end-to-end test coverage + AI fairness fix" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-18 | -| [p1-61](p1-61-ecology-content-gap-fill.md) | βœ… done | "Ecology content gap fill: sparse biomes + lineage tier holes (P1 actions from ecology-audit-gaps.md)" | [terraformer](../team-leads/terraformer.md) | 2026-06-06 | -| [p2-06](p2-06-export-pipeline.md) | βœ… done | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p2-16](p2-16-audio-assets.md) | βœ… done | Audio assets β€” in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-06-22 | -| [p2-22](p2-22-sprite-generation-pipeline.md) | βœ… done | Sprite generation pipeline β€” runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-10 | -| [p2-28](p2-28-sprite-provenance-ledger.md) | βœ… done | Sprite provenance ledger β€” LICENSES.md per-file attribution | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 | -| [p2-33](p2-33-sound-system-extension.md) | βœ… done | "Sound system extension β€” categorical fallback, variant pools, per-entity routing" | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 | -| [p2-80](p2-80-mc-worldsim-integration.md) | βœ… done | mc-worldsim orchestration crate β€” drive the existing worldsim engines in the playable turn | β€” | 2026-06-09 | - -## P2 β€” Polish - -| ID | Status | Title | Owner | Updated | -|---|---|---|---|---| -| [g2-05](g2-05-tectonics-lithology.md) | βœ… done | Tectonics + lithology β€” extend the existing prepass/terrain-evolution into a lithology axis | β€” | 2026-06-23 | -| [g2-06](g2-06-soil-derivation.md) | βœ… done | Soil derivation layer β€” emergent soil order from rock + climate + slope (the one unbuilt worldsim engine) | β€” | 2026-06-23 | -| [g2-08](g2-08-fauna-population-dynamics.md) | βœ… done | Fauna population dynamics β€” confirm the existing LV engine ticks in the playable turn | β€” | 2026-06-23 | -| [g2-09](g2-09-flora-tolerance-driven-selection.md) | βœ… done | Flora tolerance-driven selection β€” extend the existing flora_select engine with tolerance gating | β€” | 2026-06-23 | -| [g2-10](g2-10-fauna-migration-paths.md) | βœ… done | Fauna migration β€” wire the existing apply_migrations engine into the per-turn step | β€” | 2026-06-23 | -| [p1-05-followup-shipwright-batch](p1-05-followup-shipwright-batch.md) | βœ… done | Shipwright autoplay-batch sign-off β€” luxury variance + personality win balance | [shipwright](../team-leads/shipwright.md) | 2026-06-23 | -| [p1-38-followup-shipwright-batch](p1-38-followup-shipwright-batch.md) | βœ… done | p1-38 follow-up β€” Shipwright coupled-mode 10-seed regression batch + sign-off | [shipwright](../team-leads/shipwright.md) | 2026-06-23 | -| [p1-42a](p1-42a-personality-priors-building-priors-field.md) | βœ… done | "Reconcile capture_scoring.rs ↔ PersonalityPriors β€” building_priors field location" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-14 | -| [p1-42b](p1-42b-playerstate-priors-plumbing.md) | βœ… done | "Plumb per-personality building_category_weights + wonder_priorities through mc-turn PlayerState + GDScript bridge" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-14 | -| [p2-01](p2-01-minimap-improvements.md) | βœ… done | Minimap β€” fog reflection and unit markers | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-02](p2-02-hud-tooltips.md) | βœ… done | Tooltips on all HUD elements | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-03](p2-03-hotkey-cheat-sheet.md) | βœ… done | Hotkey cheat sheet (F1 / ?) | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-04](p2-04-localization-audit.md) | βœ… done | Localization audit β€” no hardcoded strings | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-05](p2-05-turn-latency.md) | βœ… done | Sub-second single-player turn latency | β€” | 2026-04-23 | -| [p2-06b](p2-06b-windows-runner.md) | βœ… done | Cross-compile Windows .exe + .dll from Linux via cargo-xwin (no Windows host) | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p2-07](p2-07-credits-screen.md) | βœ… done | Credits screen accessible from main menu | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-08](p2-08-accessibility.md) | βœ… done | Accessibility baseline β€” colorblind palette + keyboard navigation | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-09](p2-09-guide-web-deploy.md) | βœ… done | Player guide web app β€” builds clean from source | β€” | 2026-04-17 | -| [p2-10](p2-10-regression-ci-gate.md) | βœ… done | Automated regression CI gate on every push to main | [testwright](../team-leads/testwright.md) | 2026-05-14 | -| [p2-10a](p2-10a-gdlint-ungate.md) | βœ… done | "CI: gdlint stage un-gated" | [testwright](../team-leads/testwright.md) | 2026-04-25 | -| [p2-10b](p2-10b-gut-ungate.md) | βœ… done | "CI: headless GUT stage un-gated" | [testwright](../team-leads/testwright.md) | 2026-04-26 | -| [p2-10c](p2-10c-diplomacy-luxury-ids.md) | βœ… done | "Diplomacy: implement _collect_unique_luxury_ids() in happiness.gd" | β€” | 2026-04-26 | -| [p2-10d](p2-10d-legacy-unit-json.md) | βœ… done | "Data: strip legacy flags/can_found_city/can_build_improvements from unit JSON" | β€” | 2026-04-26 | -| [p2-10e](p2-10e-data-integrity.md) | βœ… done | "Data: resolve duplicate IDs and dangling unlock refs in game data" | β€” | 2026-04-26 | -| [p2-10f](p2-10f-save-manager-typed-arrays.md) | βœ… done | "SaveManager: fix typed array property assignment on Player/Unit deserialization" | β€” | 2026-04-26 | -| [p2-10g](p2-10g-city-bridge-production-cost.md) | βœ… done | "CityBridge: add production_cost field to items JSON fixture" | β€” | 2026-04-26 | -| [p2-10h](p2-10h-sprite-renderer-build-key.md) | βœ… done | "UnitRenderer: implement _build_sprite_key() helper and fix cache key test" | β€” | 2026-04-26 | -| [p2-10i](p2-10i-tile-tooltip-scene.md) | βœ… done | "TileTooltip: fix scene node name mismatches and collectibles text formatting" | β€” | 2026-04-26 | -| [p2-10j](p2-10j-fog-vision-scout-move.md) | βœ… done | "FogOfWar: fix recalculate_vision to not re-reveal already-seen tiles on move" | β€” | 2026-04-26 | -| [p2-10k](p2-10k-gdlint-cleanup.md) | βœ… done | "CI: fix 51 gdlint violations so Stage 3 is hard-green" | [testwright](../team-leads/testwright.md) | 2026-05-14 | -| [p2-10k-followup](p2-10k-followup-max-file-lines-policy.md) | βœ… done | "Workflow policy decision: how to clear the 10 max-file-lines gdlint violations" | [testwright](../team-leads/testwright.md) | 2026-06-23 | -| [p2-10l](p2-10l-gut-regression-triage.md) | βœ… done | "CI: fix 15 GUT regressions so Stage 5 is hard-green" | [testwright](../team-leads/testwright.md) | 2026-05-14 | -| [p2-10l-followup-gdai-set-map](p2-10l-followup-gdai-set-map.md) | βœ… done | "GdAiController::set_map β€” wire map into tactical state_json" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-15 | -| [p2-10l-followup-update-tile-negative-axial](p2-10l-followup-update-tile-negative-axial.md) | βœ… done | "GdAiController::update_tile rejects negative axial coordinates" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-15 | -| [p2-11](p2-11-version-about-screen.md) | βœ… done | Version string + About screen | [shipwright](../team-leads/shipwright.md) | 2026-04-17 | -| [p2-11a](p2-11a.md) | βœ… done | "SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path" | β€” | 2026-05-04 | -| [p2-12](p2-12-apricot-weston-install.md) | βœ… done | Install weston on apricot RUN host β€” unblock display-server smoke tests | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | -| [p2-18](p2-18-guide-public-deployment.md) | βœ… done | Guide web app β€” public hosting + deploy pipeline | β€” | 2026-06-23 | -| [p2-19](p2-19-guide-progress-report-page.md) | βœ… done | Guide progress report page β€” dynamic dashboard + missing assets | β€” | 2026-04-17 | -| [p2-20](p2-20-guide-sim-cache-pnpm-resolve.md) | βœ… done | Fix simCachePlugin pre-warm worker β€” tsx can't resolve @magic-civ/physics-rs through pnpm symlink | [tourguide](../team-leads/tourguide.md) | 2026-04-17 | -| [p2-21](p2-21-guide-simcache-static-bake.md) | βœ… done | Bake pre-computed sim-cache frames into the static build | [tourguide](../team-leads/tourguide.md) | 2026-04-18 | -| [p2-29](p2-29-guide-welcome-homepage-theme-alignment.md) | βœ… done | Welcome modal + HomePage lore + guide theme align to the player's chosen race/gender | [tourguide](../team-leads/tourguide.md) | 2026-04-18 | -| [p2-30](p2-30-guide-shared-primitives.md) | βœ… done | Consolidate duplicate page styled-components into shared PagePrimitives | [tourguide](../team-leads/tourguide.md) | 2026-04-18 | -| [p2-31](p2-31-guide-url-bound-state.md) | βœ… done | Migrate guide filter + tab state from useState to URL search params | [tourguide](../team-leads/tourguide.md) | 2026-04-18 | -| [p2-32](p2-32-guide-data-driven-enums.md) | βœ… done | Replace hardcoded page enums with JSON data reads | [tourguide](../team-leads/tourguide.md) | 2026-04-18 | -| [p2-35](p2-35-palace-evolution-system.md) | βœ… done | Palace evolution system β€” longhouse β†’ great_hall β†’ citadel β†’ grand_citadel + function-shedding | β€” | 2026-05-04 | -| [p2-36](p2-36-data-resources-building-duplicates.md) | βœ… done | Reconcile the 14 building IDs defined in both `resources/buildings/` and `data/buildings/` | β€” | 2026-04-29 | -| [p2-37](p2-37-react-calculator-metadata-surface.md) | βœ… done | "React calculator UI β€” surface flavor, lore, clan_affinity, archetype filter" | [tourguide](../team-leads/tourguide.md) | 2026-04-27 | -| [p2-38](p2-38-unit-audio-cues-stubs.md) | βœ… done | "Unit audio_cues stub strings β€” selection/move/attack lines for the dwarven roster" | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 | -| [p2-39](p2-39-chronicle-hall-phantom-unlock.md) | βœ… done | Resolve `chronicle_hall` phantom unlock in `chronicle_keeping` culture tech | β€” | 2026-04-27 | -| [p2-43](p2-43-culture-research-completion-event.md) | βœ… done | "Culture research live-game pipeline β€” per-turn GDExt bridge + `culture_researched` emit" | β€” | 2026-05-07 | -| [p2-44](p2-44-ai-promotion-selection.md) | βœ… done | AI promotion selection β€” auto-pick + emit unit_promoted for AI units | β€” | 2026-05-06 | -| [p2-44a](p2-44a-dataloader-promotion-trees-path.md) | βœ… done | "DataLoader path mismatch β€” `get_promotion(\"trees\")` returns empty" | [unassigned](../team-leads/unassigned.md) | 2026-05-06 | -| [p2-44b](p2-44b-promotion-dispatch-instrumentation.md) | βœ… done | AI promotion dispatch β€” instrumentation pass to identify the silent gate | [unassigned](../team-leads/unassigned.md) | 2026-05-06 | -| [p2-45](p2-45-elimination-reconciliation.md) | βœ… done | "Player elimination reconciliation β€” emit `player_eliminated` on every transition" | β€” | 2026-04-30 | -| [p2-46](p2-46-past-games-archive-replay-viewer.md) | βœ… done | "Past-games archive & replay viewer β€” `mc-replay` crate, on-disk archive, projection-based playback" | [shipwright](../team-leads/shipwright.md) | 2026-05-07 | -| [p2-47](p2-47-in-game-statistics-screens.md) | βœ… done | In-game statistics screens β€” Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | [shipwright](../team-leads/shipwright.md) | 2026-06-23 | -| [p2-48](p2-48-end-of-game-summary-screen.md) | βœ… done | "End-of-game summary screen β€” outcome banner, standings, score graph, awards, timeline, footer actions" | [shipwright](../team-leads/shipwright.md) | 2026-06-23 | -| [p2-48a](p2-48a-end-game-summary-gut-and-proof.md) | βœ… done | End-of-game summary β€” GUT tests + headless proof scene | [shipwright](../team-leads/shipwright.md) | 2026-06-22 | -| [p2-49](p2-49-climate-axes-latitude-continentality.md) | βœ… done | Climate axes refactor β€” latitude + continentality + zonal winds as first-class per-hex inputs | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | -| [p2-50](p2-50-rng-determinism-pin.md) | βœ… done | Deterministic RNG + seed-derivation pin across mc-mapgen / mc-climate / mc-ecology | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p2-51](p2-51-world-shape-knobs.md) | βœ… done | Player-facing world-shape parameters on new-game screen | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p2-52](p2-52-substrate-flora-cover-ontology-split.md) | βœ… done | Split terrain enum into substrate Γ— flora-cover layers (resolve biome ontology) | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p2-53](p2-53-action-vocabulary-design-game-gap.md) | βœ… done | Action vocabulary β€” gap analysis between design page and shipped Rust/Godot game | [wireguard](../team-leads/wireguard.md) | 2026-05-03 | -| [p2-53a](p2-53a-sentry-guard-action-kind.md) | βœ… done | Sentry/Guard ActionKind β€” add Sentry/Unsentry to mc-core with wake-on-vision | [wireguard](../team-leads/wireguard.md) | 2026-05-01 | -| [p2-53b](p2-53b-building-action-registry.md) | βœ… done | Building action registry β€” `BuildingActionKind`, `building_actions.json`, `GdBuildingActions` bridge | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-01 | -| [p2-53c](p2-53c-rally-vocabulary-expansion.md) | βœ… done | Rally vocabulary expansion β€” Hold / Fortify / JoinFormation + two-waypoint Patrol | [shipwright](../team-leads/shipwright.md) | 2026-05-01 | -| [p2-53d](p2-53d-building-specifics.md) | βœ… done | Building specifics β€” Garrison, Repair, Toggle Active + 18 archetype-specific actions | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | -| [p2-53e](p2-53e-siege-pillage-embark.md) | βœ… done | Siege handlers (Pack/Deploy/Bombard) + Pillage UI wiring + Embark/Disembark handlers | [combat-dev](../team-leads/combat-dev.md) | 2026-05-03 | -| [p2-53f](p2-53f-infantry-specifics.md) | βœ… done | Infantry specifics β€” Shield Wall, Brace, Shove, Rage, Cleave, War Cry | [combat-dev](../team-leads/combat-dev.md) | 2026-05-01 | -| [p2-53g](p2-53g-ranged-specifics.md) | βœ… done | Ranged specifics β€” Volley, Aimed Shot, Fire Arrows | [combat-dev](../team-leads/combat-dev.md) | 2026-05-03 | -| [p2-53h](p2-53h-cavalry-specifics.md) | βœ… done | Cavalry specifics β€” Charge, Pursue, Wheel | [combat-dev](../team-leads/combat-dev.md) | 2026-05-03 | -| [p2-53i](p2-53i-engineer-pioneer-medic-scout.md) | βœ… done | Support specifics β€” Engineer, Pioneer, Medic, Scout | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | -| [p2-54](p2-54-resource-visibility-three-axis.md) | βœ… done | Resource visibility β€” three-axis (visibility/yield_gate/improvement_gate) refactor | [terraformer](../team-leads/terraformer.md) | 2026-05-02 | -| [p2-54a](p2-54a-deposits-three-axis-migration.md) | βœ… done | Migrate deposits/*.json to three-axis visibility schema | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p2-54b](p2-54b-player-observation-cache.md) | βœ… done | Per-player tile observation cache β€” flora/fauna last-observed state | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p2-54c](p2-54c-renderer-observations-and-indicators.md) | βœ… done | Renderer reads observations + indicator decorations for tech-gated resources | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p2-54d](p2-54d-ai-tech-priority-from-visibility.md) | βœ… done | AI tech-priority bias from visible-but-gated luxuries + indicator decorations | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | -| [p2-55](p2-55-civilian-capture-system.md) | βœ… done | "Civilian Capture / Destroy / Ransom" | [combat-dev](../team-leads/combat-dev.md) | 2026-05-14 | -| [p2-55a](p2-55a-engineer-capture.md) | βœ… done | "Engineer (Great Person) capture mechanics" | [combat-dev](../team-leads/combat-dev.md) | 2026-05-14 | -| [p2-55b](p2-55b-caravan-master-capture.md) | βœ… done | "Caravan master capture mechanics" | [combat-dev](../team-leads/combat-dev.md) | 2026-05-14 | -| [p2-55d](p2-55d-ai-ransom-decision-hook.md) | βœ… done | "AI ransom accept/refuse hook in mc-turn start-of-turn" | β€” | 2026-05-14 | -| [p2-55e](p2-55e-richer-ransom-events.md) | βœ… done | "UnitRansomAccepted / UnitRansomExpired events on TurnResult" | β€” | 2026-05-03 | -| [p2-56](p2-56-worker-categories-and-expertise-tiers.md) | βœ… done | "Worker categories (Sustenance/Construction/Wealth) + 5-tier expertise + Master/Grandmaster auras + idle decay" | [unassigned](../team-leads/unassigned.md) | 2026-06-03 | -| [p2-56a](p2-56a-worker-category-types.md) | βœ… done | Worker category types β€” Sustenance / Construction / Wealth taxonomy | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | -| [p2-56b](p2-56b-expertise-tier-progression.md) | βœ… done | Expertise tier progression β€” 5-tier specialist XP ladder | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-04 | -| [p2-56c](p2-56c-master-grandmaster-auras.md) | βœ… done | Master / Grandmaster auras β€” adjacent-slot yield propagation | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | -| [p2-57](p2-57-production-chain-typed-resources.md) | βœ… done | "Production-chain typed resources β€” raw β†’ processed pipelines wired into mc-city" | [unassigned](../team-leads/unassigned.md) | 2026-05-14 | -| [p2-57a](p2-57a-typed-resource-stockpile.md) | βœ… done | Typed resource stockpile β€” raw vs processed taxonomy | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | -| [p2-57b](p2-57b-consume-produce-edges.md) | βœ… done | Building consume/produce edges β€” stockpile coupled to unit quality | [unassigned](../team-leads/unassigned.md) | 2026-06-23 | -| [p2-57c-mc-units-quality-consumer](p2-57c-mc-units-quality-consumer.md) | βœ… done | mc-units quality consumer β€” turn QualityTier into unit stat deltas (gives quality_chain a contract) | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-58](p2-58-ambient-encounter-rolls.md) | βœ… done | Ambient encounter rolls per tile moved β€” fauna_density Γ— ecology_tier | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | -| [p2-58a](p2-58a-tilestate-fauna-fields.md) | βœ… done | "TileState fauna fields β€” fauna_density + fauna_index for AmbientTileCtx" | [shipwright](../team-leads/shipwright.md) | 2026-05-07 | -| [p2-58b](p2-58b-ambient-encounter-hook.md) | βœ… done | "Ambient encounter hook β€” mc-turn::movement calls roll_ambient_encounter per tile step" | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | -| [p2-59](p2-59-pioneer-escort-mechanic.md) | βœ… done | Pioneer escort mechanic β€” protection rules vs ambient encounters | [combat-dev](../team-leads/combat-dev.md) | 2026-06-03 | -| [p2-60](p2-60-weather-lens-godot-ui.md) | βœ… done | "Weather / observation lens switcher in the Godot HUD" | [wireguard](../team-leads/wireguard.md) | 2026-06-06 | -| [p2-61](p2-61-observation-recording-gates-from-tech.md) | βœ… done | "Bind mc-observation gate_bits to player tech state β€” recording gates per-field" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-14 | -| [p2-62](p2-62-procedural-unit-and-building-renderer.md) | βœ… done | Procedural unit/building renderer β€” alpha-only visual substitute | [asset-sprite](../team-leads/asset-sprite.md) | 2026-05-04 | -| [p2-63](p2-63-mc-flora-biome-substrate-migration.md) | βœ… done | "mc-flora generation: migrate biome filter to substrate_climate-aware path" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-13 | -| [p2-64](p2-64-apricot-async-batch-protocol.md) | βœ… done | Apricot async batch protocol β€” launch / status / fetch decoupling | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-07 | -| [p2-65](p2-65-extract-mc-state-crate.md) | βœ… done | "Extract `GameState` and pending-queue data types into a dedicated `mc-state` crate" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-66](p2-66-world-map-visual-proof.md) | βœ… done | "World-map visual proof scene that actually renders" | [terraformer](../team-leads/terraformer.md) | 2026-05-13 | -| [p2-67](p2-67-claude-player-api.md) | βœ… done | "Claude-driven player API β€” programmatic player + Agent-SDK adapter" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-14 | -| [p2-67-followup-legal-actions](p2-67-followup-legal-actions.md) | βœ… done | "PlayerView.legal_actions β€” populate full per-unit / per-city / empire-level legal-action enumerators" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-11 | -| [p2-67-followup-mcts-tactical-state-impl](p2-67-followup-mcts-tactical-state-impl.md) | βœ… done | "TreeState impl for TacticalState β€” wire real MCTS into the AI decision path" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-04 | -| [p2-68](p2-68-mc-ai-headless-turn-driver.md) | βœ… done | "mc-ai headless turn driver β€” GameState projector/applicator + run_ai_turn" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-11 | -| [p2-69](p2-69-api-gdext-mctscontroller-port.md) | βœ… done | "Port GdMcTreeController to mc-player-api AI driver (DRY consolidation)" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-11 | -| [p2-70](p2-70-mc-vision-visibility-producer.md) | βœ… done | "mc-vision β€” per-player tile visibility producer (Rust)" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-11 | -| [p2-71](p2-71-bench-projector-enrichment.md) | βœ… done | "Bench projector enrichment β€” make MCTS see a real tactical surface" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-11 | -| [p2-71b](p2-71b-militarist-starter-widening.md) | βœ… done | "Militarist starter widening β€” add a settler/founder unit so FoundCity fires" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-11 | -| [p2-72](p2-72-gdplayerapi-render-bridge.md) | βœ… done | GdPlayerApi β†’ render bridge (visualise the API-held game world) | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-72-option-b](p2-72-option-b-workaround.md) | βœ… done | "Option B render bridge β€” proof scene rehydrates GDScript from GdPlayerApi each turn" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-12 | -| [p2-72a](p2-72a-gdgamestate-canonical-render-source.md) | βœ… done | "Make `GdGameState` the canonical render source" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-72a-building-entity-port](p2-72a-building-entity-port.md) | βœ… done | Port NPC Building entity (lairs/villages/ruins) into Rust | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-72a-pre-strip](p2-72a-pre-strip.md) | βœ… done | "Strip Game 2/3 magic/ascension/ley fields from Game 1 runtime" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-12 | -| [p2-72a-save-format-migration](p2-72a-save-format-migration.md) | βœ… done | Decouple save format from GDScript-class shape | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-72b-promote-playerstate-cities-to-city](p2-72b-promote-playerstate-cities-to-city.md) | βœ… done | "Parallel-field cities synthesis at Godot bridge (Option C)" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-08 | -| [p2-73-ui-theme-token-pipeline](p2-73-ui-theme-token-pipeline.md) | βœ… done | "UI theme pipeline β€” generate ui_theme.tres from design-tokens.json + apply globally" | [wireguard](../team-leads/wireguard.md) | 2026-06-06 | -| [p2-74-ui-dehardcode-to-tokens](p2-74-ui-dehardcode-to-tokens.md) | βœ… done | De-hardcode the Godot UI β€” route 45 scene scripts off raw Color() onto theme/tokens | [wireguard](../team-leads/wireguard.md) | 2026-06-23 | -| [p2-75](p2-75-improvement-effects-subsystem.md) | βœ… done | Improvement-completion effects subsystem in Rust β€” move completion side-effects out of GDScript | β€” | 2026-06-23 | -| [p2-76](p2-76-bunker-improvement.md) | βœ… done | Bunker improvement β€” deposit-destroying fortified subterranean chamber | β€” | 2026-06-10 | -| [p2-77](p2-77-deposit-destruction-taxonomy.md) | βœ… done | Deposit-destruction environmental taxonomy β€” destruction_effect classes + contamination engine | β€” | 2026-06-23 | -| [p2-78](p2-78-runtime-hydrology-resolve.md) | βœ… done | Runtime localized hydrology re-solve β€” in-game flow/basin-fill triggered by terraforming | β€” | 2026-06-23 | -| [p2-79](p2-79-environmental-cascade-integration.md) | βœ… done | Environmental cascade integration β€” bunker β†’ dam β†’ flood/parch β†’ flora dieback β†’ fauna migration | β€” | 2026-06-23 | -| [p2-81](p2-81.md) | βœ… done | Improvement effects authored-but-unwired β€” move moisture/wind/erosion/movement to Rust | β€” | 2026-06-23 | -| [p2-82](p2-82-climate-input-save-fidelity.md) | βœ… done | Climate-input save-fidelity β€” persist (or re-derive) worldgen-static grid inputs across save/load | β€” | 2026-06-09 | -| [p2-83](p2-83.md) | βœ… done | Phase/round state machine + speculative parallel simulation of player-action-independent turn work | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-84](p2-84.md) | βœ… done | "Dev-only compute profiling β€” per-feature CPU/RAM/GPU cost over time, trigger-attributed, zero-cost in release" | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-86](p2-86-mcp-rendered-driver.md) | βœ… done | Claude-player MCP β€” rendered driver mode (drive UI + capture screenshots) | [simulator-infra](../team-leads/simulator-infra.md) | 2026-06-23 | -| [p2-87-single-color-system-sot](p2-87-single-color-system-sot.md) | βœ… done | "Single game-wide colour system β€” one source of truth, layered tokens, every consumer derives from it" | [wireguard](../team-leads/wireguard.md) | 2026-06-23 | - -## Out of Scope (Game 2 / Game 3) - -> These objectives are explicitly future-scope. **Game 2 (Age of Kzzykt)** items introduce leylines, the Green school, and spacefaring. **Game 3 (Age of Elves)** items cover the full five-school magic system, Archons, and Arcane Ascension. None are part of the Game 1 Early Access release. - -| ID | Status | Title | Owner | Updated | -|---|---|---|---|---| -| [p1-14](p1-14-guide-magic-school-scope-drift.md) | ⚫ oos | Gate Game 2/3/4 magic-school content behind EpisodeGate (future-game scope) | β€” | 2026-04-17 | -| [p2-55c](p2-55c-freepeople-capture.md) | ⚫ oos | "Freepeople capture mechanics" | [combat-dev](../team-leads/combat-dev.md) | 2026-05-14 | -| [g2-01](g2-01-leylines-oos.md) | ⚫ oos | Ley lines β€” Game 2 (Age of Kzzykt) | β€” | 2026-04-17 | -| [g2-02](g2-02-additional-races-oos.md) | ⚫ oos | Kzzykt playable race β€” Game 2 (Age of Kzzykt) | β€” | 2026-04-17 | -| [g2-03](g2-03-green-school-oos.md) | ⚫ oos | Kzzykt Green school of magic β€” Game 2 (Age of Kzzykt) | β€” | 2026-04-17 | -| [g2-04](g2-04-multi-gpu-batch-simulate-oos.md) | ⚫ oos | Multi-GPU sharding for batch_simulate_gpu β€” out-of-scope (Game 2) | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | -| [g2-11](g2-11-vertical-city-floor-stack-oos.md) | ⚫ oos | "Vertical city floor stack (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12](g2-12-underground-layer-stack-oos.md) | ⚫ oos | "Underground layer stack (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12a](g2-12a-layer-data-model-save-oos.md) | ⚫ oos | "Underground β€” N-layer data model + save format (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12b](g2-12b-underground-worldgen-oos.md) | ⚫ oos | "Underground β€” worldgen for L1/L2/L3 + cavern terrain (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12c](g2-12c-excavation-cavern-lifecycle-oos.md) | ⚫ oos | "Underground β€” excavation action + cavern lifecycle (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12d](g2-12d-cross-layer-movement-pathfinding-oos.md) | ⚫ oos | "Underground β€” cross-layer movement, connection points + pathfinding (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12e](g2-12e-per-layer-fog-vision-oos.md) | ⚫ oos | "Underground β€” per-layer fog of war + vision (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12f](g2-12f-structural-integrity-collapse-oos.md) | ⚫ oos | "Underground β€” structural integrity + cross-layer collapse (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12g](g2-12g-layer-rendering-switch-ui-oos.md) | ⚫ oos | "Underground β€” layer rendering + layer-switch UI (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g2-12h](g2-12h-ai-layer-awareness-oos.md) | ⚫ oos | "Underground β€” AI layer-awareness (Game 2) β€” OOS" | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | -| [g3-01](g3-01-archons-oos.md) | ⚫ oos | Archons β€” Game 3 (Age of Elves) | β€” | 2026-04-17 | -| [g3-02](g3-02-life-school-oos.md) | ⚫ oos | Life school spellbook β€” Game 3 (Age of Elves) | β€” | 2026-04-17 | -| [g3-03](g3-03-death-school-oos.md) | ⚫ oos | Death school spellbook β€” Game 3 (Age of Elves) | β€” | 2026-04-17 | -| [g3-04](g3-04-chaos-school-oos.md) | ⚫ oos | Chaos school spellbook β€” Game 3 (Age of Elves) | β€” | 2026-04-17 | -| [g3-05](g3-05-aether-school-oos.md) | ⚫ oos | Aether school spellbook β€” Game 3 (Age of Elves) | β€” | 2026-04-17 | -| [g3-06](g3-06-arcane-ascension-oos.md) | ⚫ oos | Arcane Ascension victory β€” Game 3 (Age of Elves) | β€” | 2026-04-17 | -| [g4-01](g4-01-terran-race-oos.md) | ⚫ oos | Terran (Human) playable species β€” Game 4 (Age of Terrans) | β€” | 2026-04-17 | -| [g4-02](g4-02-psionics-oos.md) | ⚫ oos | Psionics ability system β€” Game 4 (Age of Terrans) | β€” | 2026-04-17 | -| [g4-03](g4-03-religious-victory-oos.md) | ⚫ oos | Religious victory condition β€” Game 4 (Age of Terrans) | β€” | 2026-04-17 | -| [g5-01](g5-01-phantasma-oos.md) | ⚫ oos | Phantasma playable species β€” Game 5 (Age of Ascension) | β€” | 2026-04-17 | -| [g5-02](g5-02-flugel-oos.md) | ⚫ oos | FlΓΌgel playable species β€” Game 5 (Age of Ascension) | β€” | 2026-04-17 | -| [g5-03](g5-03-gith-oos.md) | ⚫ oos | Gith playable species (Githyanki + Githzerai) β€” Game 5 (Age of Ascension) | β€” | 2026-04-17 | -| [g5-04](g5-04-demonia-oos.md) | ⚫ oos | Demonia playable species β€” Game 5 (Age of Ascension) | β€” | 2026-04-17 | -| [g6-01](g6-01-naval-combat-oos.md) | ⚫ oos | Naval combat β€” out-of-scope (post-v10) | β€” | 2026-04-26 | -| [g6-02](g6-02-caravan-trade-routes-oos.md) | ⚫ oos | Caravan trade routes β€” out-of-scope (post-v10) | β€” | 2026-04-26 | +| ID | Status | Title | Tags | Owner | Updated | Blocked | +|---|---|---|---|---|---|---| +| [p1-14](p1-14-guide-magic-school-scope-drift.md) | ⚫ oos | Gate Game 2/3/4 magic-school content behind EpisodeGate (future-game scope) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [p2-55c](p2-55c-freepeople-capture.md) | ⚫ oos | Freepeople capture mechanics | β€” | [combat-dev](../team-leads/combat-dev.md) | 2026-05-14 | 🟒 unblocked | +| [g2-01](g2-01-leylines-oos.md) | ⚫ oos | Ley lines β€” Game 2 (Age of Kzzykt) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g2-02](g2-02-additional-races-oos.md) | ⚫ oos | Kzzykt playable race β€” Game 2 (Age of Kzzykt) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g2-03](g2-03-green-school-oos.md) | ⚫ oos | Kzzykt Green school of magic β€” Game 2 (Age of Kzzykt) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g2-04](g2-04-multi-gpu-batch-simulate-oos.md) | ⚫ oos | Multi-GPU sharding for batch_simulate_gpu β€” out-of-scope (Game 2) | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-17 | 🟒 unblocked | +| [g2-11](g2-11-vertical-city-floor-stack-oos.md) | ⚫ oos | Vertical city floor stack (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a, g2-12c | +| [g2-12](g2-12-underground-layer-stack-oos.md) | ⚫ oos | Underground layer stack (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | 🟒 unblocked | +| [g2-12a](g2-12a-layer-data-model-save-oos.md) | ⚫ oos | Underground β€” N-layer data model + save format (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | 🟒 unblocked | +| [g2-12b](g2-12b-underground-worldgen-oos.md) | ⚫ oos | Underground β€” worldgen for L1/L2/L3 + cavern terrain (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a | +| [g2-12c](g2-12c-excavation-cavern-lifecycle-oos.md) | ⚫ oos | Underground β€” excavation action + cavern lifecycle (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a, g2-12b | +| [g2-12d](g2-12d-cross-layer-movement-pathfinding-oos.md) | ⚫ oos | Underground β€” cross-layer movement, connection points + pathfinding (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a, g2-12c | +| [g2-12e](g2-12e-per-layer-fog-vision-oos.md) | ⚫ oos | Underground β€” per-layer fog of war + vision (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a, g2-12d | +| [g2-12f](g2-12f-structural-integrity-collapse-oos.md) | ⚫ oos | Underground β€” structural integrity + cross-layer collapse (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a, g2-12c | +| [g2-12g](g2-12g-layer-rendering-switch-ui-oos.md) | ⚫ oos | Underground β€” layer rendering + layer-switch UI (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a, g2-12b, g2-12e | +| [g2-12h](g2-12h-ai-layer-awareness-oos.md) | ⚫ oos | Underground β€” AI layer-awareness (Game 2) β€” OOS | β€” | [unassigned](../team-leads/unassigned.md) | 2026-06-06 | πŸ”’ g2-12a, g2-12c, g2-12d, g2-12e | +| [g3-01](g3-01-archons-oos.md) | ⚫ oos | Archons β€” Game 3 (Age of Elves) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g3-02](g3-02-life-school-oos.md) | ⚫ oos | Life school spellbook β€” Game 3 (Age of Elves) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g3-03](g3-03-death-school-oos.md) | ⚫ oos | Death school spellbook β€” Game 3 (Age of Elves) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g3-04](g3-04-chaos-school-oos.md) | ⚫ oos | Chaos school spellbook β€” Game 3 (Age of Elves) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g3-05](g3-05-aether-school-oos.md) | ⚫ oos | Aether school spellbook β€” Game 3 (Age of Elves) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g3-06](g3-06-arcane-ascension-oos.md) | ⚫ oos | Arcane Ascension victory β€” Game 3 (Age of Elves) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g4-01](g4-01-terran-race-oos.md) | ⚫ oos | Terran (Human) playable species β€” Game 4 (Age of Terrans) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g4-02](g4-02-psionics-oos.md) | ⚫ oos | Psionics ability system β€” Game 4 (Age of Terrans) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g4-03](g4-03-religious-victory-oos.md) | ⚫ oos | Religious victory condition β€” Game 4 (Age of Terrans) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g5-01](g5-01-phantasma-oos.md) | ⚫ oos | Phantasma playable species β€” Game 5 (Age of Ascension) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g5-02](g5-02-flugel-oos.md) | ⚫ oos | FlΓΌgel playable species β€” Game 5 (Age of Ascension) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g5-03](g5-03-gith-oos.md) | ⚫ oos | Gith playable species (Githyanki + Githzerai) β€” Game 5 (Age of Ascension) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g5-04](g5-04-demonia-oos.md) | ⚫ oos | Demonia playable species β€” Game 5 (Age of Ascension) | β€” | β€” | 2026-04-17 | 🟒 unblocked | +| [g6-01](g6-01-naval-combat-oos.md) | ⚫ oos | Naval combat β€” out-of-scope (post-v10) | β€” | β€” | 2026-04-26 | 🟒 unblocked | +| [g6-02](g6-02-caravan-trade-routes-oos.md) | ⚫ oos | Caravan trade routes β€” out-of-scope (post-v10) | β€” | β€” | 2026-04-26 | 🟒 unblocked | ## Superseded -> These objectives were split into narrower children. Files are retained as index stubs so external references don't 404. The `superseded_by:` frontmatter field names the replacement IDs. +> These objectives were split into narrower children. Files are retained as index stubs so external references do not 404. -| ID | Status | Title | Owner | Updated | -|---|---|---|---|---| -| [p0-20d](p0-20d-gpu-walltime-real-host.md) | ♻️ superseded | GPU MCTS wall-time gate β€” measure on real-discrete-GPU test host | [warcouncil](../team-leads/warcouncil.md) | 2026-05-05 | -| [p1-30b](p1-30b-parallel-mcts-rollouts.md) | ♻️ superseded | "Parallel MCTS rollouts for huge-map decisive games (closes p1-22's huge-map sub-gate)" | [warcouncil](../team-leads/warcouncil.md) | 2026-05-05 | -| [p2-23](p2-23-unit-sprites-dwarf-roster.md) | ♻️ superseded | Unit sprites β€” Dwarf-racial roster (m/f variants) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | -| [p2-24](p2-24-unit-sprites-wild-creatures.md) | ♻️ superseded | "Unit sprites β€” wild creatures & fauna (generic, no race/sex)" | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | -| [p2-25](p2-25-building-sprites-base-coverage.md) | ♻️ superseded | Building sprites β€” base game coverage (non-wonder) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | -| [p2-26](p2-26-mundane-wonder-sprites.md) | ♻️ superseded | "Mundane-wonder sprites β€” 24 distinct, higher-fidelity art" | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | -| [p2-27](p2-27-city-population-tier-sprites.md) | ♻️ superseded | City population-tier sprites β€” city_q1 through city_q5 | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | -| [p1-27d](p1-27d-additive-value-estimate.md) | ♻️ superseded | Add `value_estimate_abstract` GdMcTreeController method β€” non-lossy MCTS service caller | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | -| [p2-17](p2-17-sprite-assets.md) | ♻️ superseded | Sprite assets β€” superseded index (split into p2-22 … p2-28) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | -| [p2-85](p2-85-poi-sprites-and-tooltips.md) | ♻️ superseded | POI sprites + hover tooltips β€” lairs (and resources) legible on the map | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | +| ID | Status | Title | Tags | Owner | Updated | Blocked | +|---|---|---|---|---|---|---| +| [p0-20d](p0-20d-gpu-walltime-real-host.md) | ♻️ superseded | GPU MCTS wall-time gate β€” measure on real-discrete-GPU test host | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-05-05 | 🟒 unblocked | +| [p1-30b](p1-30b-parallel-mcts-rollouts.md) | ♻️ superseded | Parallel MCTS rollouts for huge-map decisive games (closes p1-22's huge-map sub-gate) | perf, mcts, tactical-ai | [warcouncil](../team-leads/warcouncil.md) | 2026-05-05 | 🟒 unblocked | +| [p2-23](p2-23-unit-sprites-dwarf-roster.md) | ♻️ superseded | Unit sprites β€” Dwarf-racial roster (m/f variants) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | 🟒 unblocked | +| [p2-24](p2-24-unit-sprites-wild-creatures.md) | ♻️ superseded | Unit sprites β€” wild creatures & fauna (generic, no race/sex) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | 🟒 unblocked | +| [p2-25](p2-25-building-sprites-base-coverage.md) | ♻️ superseded | Building sprites β€” base game coverage (non-wonder) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | 🟒 unblocked | +| [p2-26](p2-26-mundane-wonder-sprites.md) | ♻️ superseded | Mundane-wonder sprites β€” 24 distinct, higher-fidelity art | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | 🟒 unblocked | +| [p2-27](p2-27-city-population-tier-sprites.md) | ♻️ superseded | City population-tier sprites β€” city_q1 through city_q5 | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | 🟒 unblocked | +| [p1-27d](p1-27d-additive-value-estimate.md) | ♻️ superseded | Add `value_estimate_abstract` GdMcTreeController method β€” non-lossy MCTS service caller | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | 🟒 unblocked | +| [p2-17](p2-17-sprite-assets.md) | ♻️ superseded | Sprite assets β€” superseded index (split into p2-22 … p2-28) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | 🟒 unblocked | +| [p2-85](p2-85-poi-sprites-and-tooltips.md) | ♻️ superseded | POI sprites + hover tooltips β€” lairs (and resources) legible on the map | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-06-23 | 🟒 unblocked | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index 93da2ce4..5b6968c1 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-06-27T13:58:02Z", + "generated_at": "2026-06-28T16:17:52Z", "totals": { "done": 305, "in_progress": 0, @@ -19,7 +19,7 @@ "owner": "warcouncil", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "`GdMcTreeController` (Rust GDExtension) is the unconditional AI driver. `AiTurnBridge.run()` always calls `_apply_mcts_strategic_override()` \u2014 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**Acceptance re-framed 2026-04-17 (user sign-off):** The prior \"median TTV in 200\u2013350 band\" bullet was measuring the wrong thing. Every game ends at T300 (turn limit \u2192 score victory) OR earlier via domination; \"median TTV\" is bimodal (domination cluster + score-cluster-at-T299), and its value shifts based on dom:score ratio rather than game quality. Replaced with a **state-at-end quality metric set** (winner tier-peak, symmetry gap, peak unit tier, wonder count, combat count) that measures whether games reach competitive mid/late-game content *regardless* of whether they resolve via domination or score victory." + "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**Acceptance re-framed 2026-04-17 (user sign-off):** The prior \"median TTV in 200–350 band\" bullet was measuring the wrong thing. Every game ends at T300 (turn limit β†’ score victory) OR earlier via domination; \"median TTV\" is bimodal (domination cluster + score-cluster-at-T299), and its value shifts based on dom:score ratio rather than game quality. Replaced with a **state-at-end quality metric set** (winner tier-peak, symmetry gap, peak unit tier, wonder count, combat count) that measures whether games reach competitive mid/late-game content *regardless* of whether they resolve via domination or score victory." }, { "id": "p0-02", @@ -30,7 +30,7 @@ "owner": "warcouncil", "updated_at": "2026-04-26", "blocked_by": [], - "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\":` \u2014 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 \u00d7 10-seed batch results (2026-04-17, `.local/iter/p0-02-clans/` \u2014 PRE-REFRAME EVIDENCE):**\n\n> These batches ran BEFORE p0-25's instrumentation landed, so `player_stats` does NOT carry\n> `tier_peak` / `peak_unit_tier` / `wonder_count`. The TTV column is preserved as the\n> contemporaneous signal; it is NOT the current acceptance metric. Per p0-01's 2026-04-17\n> reframe, the primary divergence gate is **tier_peak** (era-progression, which scales with\n> difficulty per p0-24) \u2014 tracked as a \"needs re-run\" in Remaining to reach done below.\n\n| Clan | Wins | TTV_med (legacy) | 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\nSignals that DON'T depend on TTV (still valid post-reframe):\n- **Balance**: 49 total games, each clan 3 AI-wins, max 33% \u2014 passes.\n- **Gold axis**: goldvein 2\u00d7 ironhold (wealth=9 vs 3) \u2014 passes.\n- **First-combat**: identical at T9 across all clans (map-forced start proximity, not AI-driven).\n- **Pair metric-identical**: deepforge/ironhold and goldvein/runesmith pairs show overlapping weight profiles; same 10 seeds converge.\n\nSignals that DO depend on TTV (need tier_peak re-run to close the reframed gate):\n- TTV delta between clan pairs \u2014 the \"goldvein/runesmith finish 30 turns faster than ironhold/deepforge\" claim doesn't translate into the tier_peak framework until re-measured.\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__20260417_072921/`, 10 seeds T300 per axis \u2014 PRE-REFRAME EVIDENCE):** Each axis neutralized to 5 for all clans. Measured under pre-p0-25 instrumentation; metrics are TTV / gold / mil from the legacy `player_stats` schema. All 6 axes show \u226510% delta on their correlated legacy metric vs pooled baseline (TTV=185, gold=379, mil=3):\n\n| Axis | Correlated metric (legacy) | 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 \u2014 domination wins collapse from 49/49 to 1\u20138/10 per axis. The TTV delta reflects game degradation, not faster play. All axes CONFIRMED LIVE under the legacy metric set. Re-measurement under tier_peak is needed before the reframed acceptance (below) can be cited." + "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/` β€” PRE-REFRAME EVIDENCE):**\n\n> These batches ran BEFORE p0-25's instrumentation landed, so `player_stats` does NOT carry\n> `tier_peak` / `peak_unit_tier` / `wonder_count`. The TTV column is preserved as the\n> contemporaneous signal; it is NOT the current acceptance metric. Per p0-01's 2026-04-17\n> reframe, the primary divergence gate is **tier_peak** (era-progression, which scales with\n> difficulty per p0-24) β€” tracked as a \"needs re-run\" in Remaining to reach done below.\n\n| Clan | Wins | TTV_med (legacy) | 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\nSignals that DON'T depend on TTV (still valid post-reframe):\n- **Balance**: 49 total games, each clan 3 AI-wins, max 33% β€” passes.\n- **Gold axis**: goldvein 2Γ— ironhold (wealth=9 vs 3) β€” passes.\n- **First-combat**: identical at T9 across all clans (map-forced start proximity, not AI-driven).\n- **Pair metric-identical**: deepforge/ironhold and goldvein/runesmith pairs show overlapping weight profiles; same 10 seeds converge.\n\nSignals that DO depend on TTV (need tier_peak re-run to close the reframed gate):\n- TTV delta between clan pairs β€” the \"goldvein/runesmith finish 30 turns faster than ironhold/deepforge\" claim doesn't translate into the tier_peak framework until re-measured.\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__20260417_072921/`, 10 seeds T300 per axis β€” PRE-REFRAME EVIDENCE):** Each axis neutralized to 5 for all clans. Measured under pre-p0-25 instrumentation; metrics are TTV / gold / mil from the legacy `player_stats` schema. All 6 axes show β‰₯10% delta on their correlated legacy metric vs pooled baseline (TTV=185, gold=379, mil=3):\n\n| Axis | Correlated metric (legacy) | 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 1–8/10 per axis. The TTV delta reflects game degradation, not faster play. All axes CONFIRMED LIVE under the legacy metric set. Re-measurement under tier_peak is needed before the reframed acceptance (below) can be cited." }, { "id": "p0-03", @@ -61,7 +61,7 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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` \u2192 10/10). GDScript wrapper `culture.gd` now delegates to `GdCulture` (the mc-culture bridge) and emits `city_border_expanded` on threshold crossing. Score victory folds culture via `SCORE_CULTURE_DIVISOR`. SimpleHeuristicAi prioritizes monument buildings when `culture_axis` is high.\n\n**Re-promoted 2026-04-17:** the orphan-crate condition that demoted this objective was closed by `p0-27-gd-culture-bridge`. `grep 'mc_culture::'` now returns 40+ hits across mc-sim, mc-turn, api-gdext, and tests. `culture.gd` is a 36-LOC marshaler with zero inline thresholds; `mc-turn::process_culture` drives `CulturePool::tick_all`." + "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` now delegates to `GdCulture` (the mc-culture bridge) and emits `city_border_expanded` on threshold crossing. Score victory folds culture via `SCORE_CULTURE_DIVISOR`. SimpleHeuristicAi prioritizes monument buildings when `culture_axis` is high.\n\n**Re-promoted 2026-04-17:** the orphan-crate condition that demoted this objective was closed by `p0-27-gd-culture-bridge`. `grep 'mc_culture::'` now returns 40+ hits across mc-sim, mc-turn, api-gdext, and tests. `culture.gd` is a 36-LOC marshaler with zero inline thresholds; `mc-turn::process_culture` drives `CulturePool::tick_all`." }, { "id": "p0-06", @@ -92,7 +92,7 @@ "owner": "warcouncil", "updated_at": "2026-04-18", "blocked_by": [], - "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 \u2265 1.25\u00d7 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 \u2014 not caused by these changes. Of completable seeds, 2/2 = domination." + "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", @@ -102,21 +102,21 @@ "scope": "game1", "updated_at": "2026-04-16", "blocked_by": [], - "summary": "Three UI paths assumed-but-unverified:\n\n1. **Citizen-tile assignment** \u2014 can the player manually move a worker off a tile onto another?\n2. **Production queue controls** \u2014 reorder, pause, show cost + ETA per item?\n3. **Promotion picker auto-trigger** \u2014 does the picker appear when a unit levels up after combat, and does the choice persist?" + "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 \u2014 \u22657/10 seeds declare a winner", + "title": "Game-completion stability β€” β‰₯7/10 seeds declare a winner", "priority": "p0", "status": "done", "scope": "game1", "updated_at": "2026-04-17", "blocked_by": [], - "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 \u2248 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` \u2014 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." + "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 T8\u2013T10 mystery item drops", + "title": "Author the four T8–T10 mystery item drops", "priority": "p0", "status": "done", "scope": "game1", @@ -133,7 +133,7 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Save/load UI, autosave-on-quit hook, multi-slot naming, schema version with rejection of mismatches, and GDScript + Rust round-trip tests all shipped. The three serde regressions that reopened this objective on 2026-04-17 are closed:\n\n1. **`WonderId` newtype serialized as an array, not a string** \u2014 fixed by Testwright via `#[serde(transparent)]` at `mc-core/src/wonder.rs:5`.\n2. **`strategic_axes` + `TechState.progress` non-deterministic key order** \u2014 fixed by changing both fields from `HashMap` \u2192 `BTreeMap` in `mc-turn/src/game_state.rs`. BTreeMap iteration is sorted, so JSON output is byte-stable across processes. The three `#[ignore]` attributes on `game_state_json_roundtrip_is_stable`, `player_state_json_roundtrip_is_stable`, and `tech_state_json_roundtrip_is_stable` have been removed; all pass.\n3. **`PlayerState.relations` tuple-keyed map fails serde_json** \u2014 fixed by a `relations_as_pairs` serde adapter module in `mc-turn/src/game_state.rs` that round-trips the map as `Vec<((u8, u8), RelationState)>`. The T2 fixture now populates a `(0, 1) \u2192 Peace` relation and asserts `relations.is_empty() == false` plus field preservation after round-trip.\n\nFull mc-turn test suite green: 89 passed, 0 failed, 1 ignored (unrelated). All 5 tests in `mc-turn/tests/serde_roundtrip.rs` pass." + "summary": "Save/load UI, autosave-on-quit hook, multi-slot naming, schema version with rejection of mismatches, and GDScript + Rust round-trip tests all shipped. The three serde regressions that reopened this objective on 2026-04-17 are closed:\n\n1. **`WonderId` newtype serialized as an array, not a string** β€” fixed by Testwright via `#[serde(transparent)]` at `mc-core/src/wonder.rs:5`.\n2. **`strategic_axes` + `TechState.progress` non-deterministic key order** β€” fixed by changing both fields from `HashMap` β†’ `BTreeMap` in `mc-turn/src/game_state.rs`. BTreeMap iteration is sorted, so JSON output is byte-stable across processes. The three `#[ignore]` attributes on `game_state_json_roundtrip_is_stable`, `player_state_json_roundtrip_is_stable`, and `tech_state_json_roundtrip_is_stable` have been removed; all pass.\n3. **`PlayerState.relations` tuple-keyed map fails serde_json** β€” fixed by a `relations_as_pairs` serde adapter module in `mc-turn/src/game_state.rs` that round-trips the map as `Vec<((u8, u8), RelationState)>`. The T2 fixture now populates a `(0, 1) β†’ Peace` relation and asserts `relations.is_empty() == false` plus field preservation after round-trip.\n\nFull mc-turn test suite green: 89 passed, 0 failed, 1 ignored (unrelated). All 5 tests in `mc-turn/tests/serde_roundtrip.rs` pass." }, { "id": "p0-13", @@ -143,7 +143,7 @@ "scope": "game1", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Fog-of-war is one of the core tension generators in 4X \u2014 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." + "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", @@ -176,7 +176,7 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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\u2192warrior 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\u22652, peaceful, and turn\u226460. Named-constant gates\nin `auto_play.gd:25-40`." + "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", @@ -187,7 +187,7 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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 \u2265 1` on 6/10 seeds (1, 2, 3, 6, 8, 9)." + "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", @@ -197,72 +197,72 @@ "scope": "game1", "updated_at": "2026-04-17", "blocked_by": [], - "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\u21922 tile visibility triggers `EventBus.deposit_discovered` \u2192 `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\"]`." + "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 \u2192 tile yields \u2192 happiness end-to-end", + "title": "Biome-driven collectibles β†’ tile yields β†’ happiness end-to-end", "priority": "p0", "status": "done", "scope": "game1", "updated_at": "2026-04-16", "blocked_by": [], - "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 \u2014 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." + "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-21", - "title": "Audio system capability \u2014 manifest + autoload + EventBus wiring", + "title": "Audio system capability β€” manifest + autoload + EventBus wiring", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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 \u2014 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." + "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 \u2014 5 clans, huge map, deep lookahead", + "title": "Ultimate AI stress test β€” 5 clans, huge map, deep lookahead", "priority": "p0", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-04-25", "blocked_by": [], - "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 \u2014 deep trees, many expansions,\ngenuine strategic divergence between clans at multi-clan scale \u2014 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." + "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 \u2014 replace procedural draw_* with texture rendering", + "title": "Sprite rendering capability β€” replace procedural draw_* with texture rendering", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Renderers now implement the additive-overlay design rule: `draw_circle` baseline always\nrenders first (unconditional), then `draw_texture` overlays the sprite on top when a file\nexists at the resolved path. Both renderers follow this invariant.\n\n**Changes landed (2026-04-17):**\n- `unit_renderer.gd`: `_draw()` now draws circle+label FIRST unconditionally; sprite is\n drawn on top only when `_get_unit_sprite()` returns non-null. Sprite key composed as\n `__.png` (race resolved from unit or owning Player) with bare\n `.png` fallback. New helpers: `_build_sprite_key`, `_cache_unit_sprites`,\n `_resolve_race_id`, `_resolve_sex`.\n- `city_renderer.gd`: `_draw_city_sprite()` draws circle FIRST; sprite overlay follows.\n Removed the `return` after `draw_texture` that previously skipped the circle entirely.\n Linter-added constants: `SPRITE_LOOKUP_CITY_FORMAT`, `CITY_QUALITY_BUCKET`, `CITY_QUALITY_MAX`.\n- `test_sprite_renderer.gd`: 9 GUT tests covering `_build_sprite_key` variants, null-miss\n cache, cache population after miss, and `CityRenderer` smoke.\n- `sprite_proof.gd`: proof scene, two units side-by-side \u2014 one with null cache (circle only),\n one with a 56\u00d756 magenta `ImageTexture` pre-seeded into the cache (circle + overlay).\n\n**Design rule (user directive 2026-04-17):** Do NOT replace `draw_circle`/`draw_rect` with\nsprites. Keep the procedural draw path as the always-working baseline that never deletes.\nSprite rendering is an additive enhancement layer." + "summary": "Renderers now implement the additive-overlay design rule: `draw_circle` baseline always\nrenders first (unconditional), then `draw_texture` overlays the sprite on top when a file\nexists at the resolved path. Both renderers follow this invariant.\n\n**Changes landed (2026-04-17):**\n- `unit_renderer.gd`: `_draw()` now draws circle+label FIRST unconditionally; sprite is\n drawn on top only when `_get_unit_sprite()` returns non-null. Sprite key composed as\n `__.png` (race resolved from unit or owning Player) with bare\n `.png` fallback. New helpers: `_build_sprite_key`, `_cache_unit_sprites`,\n `_resolve_race_id`, `_resolve_sex`.\n- `city_renderer.gd`: `_draw_city_sprite()` draws circle FIRST; sprite overlay follows.\n Removed the `return` after `draw_texture` that previously skipped the circle entirely.\n Linter-added constants: `SPRITE_LOOKUP_CITY_FORMAT`, `CITY_QUALITY_BUCKET`, `CITY_QUALITY_MAX`.\n- `test_sprite_renderer.gd`: 9 GUT tests covering `_build_sprite_key` variants, null-miss\n cache, cache population after miss, and `CityRenderer` smoke.\n- `sprite_proof.gd`: proof scene, two units side-by-side β€” one with null cache (circle only),\n one with a 56Γ—56 magenta `ImageTexture` pre-seeded into the cache (circle + overlay).\n\n**Design rule (user directive 2026-04-17):** Do NOT replace `draw_circle`/`draw_rect` with\nsprites. Keep the procedural draw path as the always-working baseline that never deletes.\nSprite rendering is an additive enhancement layer." }, { "id": "p0-24", - "title": "Difficulty-calibrated AI progression \u2014 Easy / Normal / Hard tier-peak distributions", + "title": "Difficulty-calibrated AI progression β€” Easy / Normal / Hard tier-peak distributions", "priority": "p0", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-04-19", "blocked_by": [], - "summary": "Added 2026-04-17 as part of the TTV \u2192 state-at-end metric reframe (see p0-01). The game's three AI-difficulty tiers (Easy / Normal / Hard in `difficulty.json`) must produce *measurably different* progression profiles when batched. The current MCTS + heuristic stack doesn't actually change behavior between difficulty tiers \u2014 `ai_difficulty` is read in a few Rust spots but has no empirically-validated behavioral split." + "summary": "Added 2026-04-17 as part of the TTV β†’ state-at-end metric reframe (see p0-01). The game's three AI-difficulty tiers (Easy / Normal / Hard in `difficulty.json`) must produce *measurably different* progression profiles when batched. The current MCTS + heuristic stack doesn't actually change behavior between difficulty tiers β€” `ai_difficulty` is read in a few Rust spots but has no empirically-validated behavioral split." }, { "id": "p0-25", - "title": "Game-quality metrics instrumentation \u2014 tier_peak, peak_unit_tier, wonder_count", + "title": "Game-quality metrics instrumentation β€” tier_peak, peak_unit_tier, wonder_count", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Added 2026-04-17 as part of the TTV \u2192 state-at-end metric reframe (see p0-01). `turn_stats.jsonl` per-player stats now carry three quality metrics: `tier_peak` (max era reached, monotonic across turns; derived each turn by folding `DataLoader.get_tech(id).era` over `player.researched_techs` in `_check_invariants`), `peak_unit_tier` (max `DataLoader.get_unit(id).tier` seen via the `EventBus.unit_created` hook in `_on_unit_created`), and `wonder_count` (entries in `GameState.wonders_built` whose value equals the player's index, computed in `_build_player_stats`). The schema declares all three with backward-compat \u2014 fields are NOT in `required`, so historical batches (pre-p0-25) still validate; the reporter treats absent fields as sentinel `-1` and filters them from medians. `tools/autoplay-report.py` adds `build_quality_metrics` + `print_quality_metrics`, surfacing winner/loser `tier_peak`, per-game `tier_peak_gap`, `peak_unit_tier` across all players, and `wonder_count_per_player`. 8 pytest tests in `tools/tests/test_quality_metrics.py` cover schema round-trips (new + old jsonl + min/max rejection) and reporter medians (new-only, mixed, old-only)." + "summary": "Added 2026-04-17 as part of the TTV β†’ state-at-end metric reframe (see p0-01). `turn_stats.jsonl` per-player stats now carry three quality metrics: `tier_peak` (max era reached, monotonic across turns; derived each turn by folding `DataLoader.get_tech(id).era` over `player.researched_techs` in `_check_invariants`), `peak_unit_tier` (max `DataLoader.get_unit(id).tier` seen via the `EventBus.unit_created` hook in `_on_unit_created`), and `wonder_count` (entries in `GameState.wonders_built` whose value equals the player's index, computed in `_build_player_stats`). The schema declares all three with backward-compat β€” fields are NOT in `required`, so historical batches (pre-p0-25) still validate; the reporter treats absent fields as sentinel `-1` and filters them from medians. `tools/autoplay-report.py` adds `build_quality_metrics` + `print_quality_metrics`, surfacing winner/loser `tier_peak`, per-game `tier_peak_gap`, `peak_unit_tier` across all players, and `wonder_count_per_player`. 8 pytest tests in `tools/tests/test_quality_metrics.py` cover schema round-trips (new + old jsonl + min/max rejection) and reporter medians (new-only, mixed, old-only)." }, { "id": "p0-26", @@ -277,36 +277,36 @@ }, { "id": "p0-27", - "title": "GdCulture bridge \u2014 live game delegates culture to mc-culture", + "title": "GdCulture bridge β€” live game delegates culture to mc-culture", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "`mc-culture` is a fully-tested 297-LOC crate with `CulturePool` + `CityCultureState` + 10 passing unit tests, but `grep 'use mc_culture'` returned zero hits outside the crate. The live game (`culture.gd:33,62`) reimplemented the `5 + n` threshold inline in GDScript, and `mc-turn/src/processor.rs:527` did its own `player.culture_total += culture_per_turn` bypassing `CulturePool::tick_all`. Three parallel implementations of the same rule.\n\nThis objective wired the crate to the live code paths so there is one source of truth.\n\nParent objective `p0-05-culture-and-borders` was demoted from \u2705 done to \ud83d\udfe1 partial when this orphan was discovered 2026-04-17, then re-promoted when all bullets below closed." + "summary": "`mc-culture` is a fully-tested 297-LOC crate with `CulturePool` + `CityCultureState` + 10 passing unit tests, but `grep 'use mc_culture'` returned zero hits outside the crate. The live game (`culture.gd:33,62`) reimplemented the `5 + n` threshold inline in GDScript, and `mc-turn/src/processor.rs:527` did its own `player.culture_total += culture_per_turn` bypassing `CulturePool::tick_all`. Three parallel implementations of the same rule.\n\nThis objective wired the crate to the live code paths so there is one source of truth.\n\nParent objective `p0-05-culture-and-borders` was demoted from βœ… done to 🟑 partial when this orphan was discovered 2026-04-17, then re-promoted when all bullets below closed." }, { "id": "p0-28", - "title": "GdEconomy bridge \u2014 live game delegates gold/upkeep to mc-economy", + "title": "GdEconomy bridge β€” live game delegates gold/upkeep to mc-economy", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "`mc-economy` exports `process_gold`, `Treasury`, `Stockpile` \u2014 consumed by `mc-turn/src/processor.rs::process_economy` (the bench/optimizer path) and `mc-city` (Stockpile). But the live gameplay turn runs a ~50-line `_process_economy` in `turn_processor.gd:435` that independently computes marketplace bonuses, unit upkeep, and insolvency handling. `economy.gd` itself is a 2-line empty `class Economy extends RefCounted`.\n\nTwo parallel economy pipelines produce divergent balance behavior: the bench validates one, the player experiences the other.\n\nParent objective `p0-06-economy-integration` was demoted from \u2705 done to \ud83d\udfe1 partial when this split was confirmed 2026-04-17." + "summary": "`mc-economy` exports `process_gold`, `Treasury`, `Stockpile` β€” consumed by `mc-turn/src/processor.rs::process_economy` (the bench/optimizer path) and `mc-city` (Stockpile). But the live gameplay turn runs a ~50-line `_process_economy` in `turn_processor.gd:435` that independently computes marketplace bonuses, unit upkeep, and insolvency handling. `economy.gd` itself is a 2-line empty `class Economy extends RefCounted`.\n\nTwo parallel economy pipelines produce divergent balance behavior: the bench validates one, the player experiences the other.\n\nParent objective `p0-06-economy-integration` was demoted from βœ… done to 🟑 partial when this split was confirmed 2026-04-17." }, { "id": "p0-29", - "title": "GdTechWeb bridge \u2014 live game delegates research to mc-tech", + "title": "GdTechWeb bridge β€” live game delegates research to mc-tech", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "`mc-tech` exports `TechWeb`, `PlayerTechState`, `ResearchResult`, `UnlockSignal`. Previously consumed only by `mc-turn/src/processor.rs::process_science` (bench/optimizer path). The live game ran a parallel ~52-line `_process_research` in `turn_processor.gd:156` duplicating cost accumulation, spell-vs-tech dispatch, and the `FORCE_UNLIMITED_RESEARCH` debug knob.\n\n**Resolved 2026-04-17 (bridge-tech-dev):** `GdTechWeb` Rust wrapper + GDScript collapse landed. `_process_research` now delegates to Rust; parent `p0-07-tech-research-costs` re-promoted to \u2705 done." + "summary": "`mc-tech` exports `TechWeb`, `PlayerTechState`, `ResearchResult`, `UnlockSignal`. Previously consumed only by `mc-turn/src/processor.rs::process_science` (bench/optimizer path). The live game ran a parallel ~52-line `_process_research` in `turn_processor.gd:156` duplicating cost accumulation, spell-vs-tech dispatch, and the `FORCE_UNLIMITED_RESEARCH` debug knob.\n\n**Resolved 2026-04-17 (bridge-tech-dev):** `GdTechWeb` Rust wrapper + GDScript collapse landed. `_process_research` now delegates to Rust; parent `p0-07-tech-research-costs` re-promoted to βœ… done." }, { "id": "p0-30", @@ -317,51 +317,51 @@ "owner": "shipwright", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "The tech-debt audit (`.project/reports/simulation/tech-debt-audit.md:11-18`, 2026-04-09) identified that ecology simulation runs **twice per turn**:\n\n1. `src/game/engine/src/modules/climate/climate.gd:83` \u2192 Rust `GdEcologyPhysics::process_step` (correct path)\n2. `src/game/engine/src/modules/management/turn_processor.gd` \u2192 GDScript `EcosystemOrchestrator::process_turn` (duplicate)\n\nSame tile data, two mutation passes per turn. Flora canopy/undergrowth accumulates at ~2\u00d7 intended rate. The GDScript `ecosystem.gd` (~308 LOC) + `flora.gd` (~405 LOC) were originally transpiler targets; the transpiler was deleted but the functions were never ported and are now the live simulation alongside the Rust pass.\n\nMid-late-game balance (wilds spawn pressure, lair densities, food from wild tiles) is miscalibrated because the tuning team tuned against a 2\u00d7 tick rate. Once fixed, expect a re-tune pass under `p1-05-balance-tuning`." + "summary": "The tech-debt audit (`.project/reports/simulation/tech-debt-audit.md:11-18`, 2026-04-09) identified that ecology simulation runs **twice per turn**:\n\n1. `src/game/engine/src/modules/climate/climate.gd:83` β†’ Rust `GdEcologyPhysics::process_step` (correct path)\n2. `src/game/engine/src/modules/management/turn_processor.gd` β†’ GDScript `EcosystemOrchestrator::process_turn` (duplicate)\n\nSame tile data, two mutation passes per turn. Flora canopy/undergrowth accumulates at ~2Γ— intended rate. The GDScript `ecosystem.gd` (~308 LOC) + `flora.gd` (~405 LOC) were originally transpiler targets; the transpiler was deleted but the functions were never ported and are now the live simulation alongside the Rust pass.\n\nMid-late-game balance (wilds spawn pressure, lair densities, food from wild tiles) is miscalibrated because the tuning team tuned against a 2Γ— tick rate. Once fixed, expect a re-tune pass under `p1-05-balance-tuning`." }, { "id": "p0-31", - "title": "Restore Rust ecology path \u2014 fix ClimateScript bugs + re-enable per-turn tick", + "title": "Restore Rust ecology path β€” fix ClimateScript bugs + re-enable per-turn tick", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "p0-30 deleted the duplicate GDScript ecology pass (`ecosystem.gd`/`flora.gd`, 939 LOC) but could not close its bullet 4 (\"10-seed batch shows evolving canopy values\") because the Rust path is **also** disabled. `turn_processor.gd::_process_climate` (line 583) calls `MarineHarvestScript` only; the three sibling `process_turn` calls (`WeatherScript`, `ClimateScript`, `ClimateEffectsScript`) are commented out, citing real bugs:\n\n- **`ClimateScript.process_turn` (real code, live surface)** \u2014 raises `Invalid cast to int` inside `_sync_tiles_to_grid` / `_sync_grid_to_tiles`, and `ecological_events.process_events` has an arg-count mismatch (`process_drought` / `process_wildfire` / `process_marine` expect 8\u20139 args, fewer passed).\n- **`WeatherScript` + `ClimateEffectsScript`** \u2014 empty stubs; aborts propagate and kill the arena turn loop.\n\nAfter p0-30's deletion, ecology runs **0\u00d7 per turn**. Flora canopy/undergrowth does not evolve \u2014 wild biome simulation is frozen. This objective narrowly restores the Rust ecology tick by fixing the `ClimateScript` bugs and re-enabling the call site. The two empty-stub siblings (`WeatherScript` / `ClimateEffectsScript`) are out of scope for p0-31 \u2014 they're deferred to follow-ups since they require full implementation, not bug repair.\n\nThis objective unblocks p0-30 bullet 4: once ecology ticks via Rust, a 10-seed batch can capture evolving canopy values and p0-30 flips \u2705 done." + "summary": "p0-30 deleted the duplicate GDScript ecology pass (`ecosystem.gd`/`flora.gd`, 939 LOC) but could not close its bullet 4 (\"10-seed batch shows evolving canopy values\") because the Rust path is **also** disabled. `turn_processor.gd::_process_climate` (line 583) calls `MarineHarvestScript` only; the three sibling `process_turn` calls (`WeatherScript`, `ClimateScript`, `ClimateEffectsScript`) are commented out, citing real bugs:\n\n- **`ClimateScript.process_turn` (real code, live surface)** β€” raises `Invalid cast to int` inside `_sync_tiles_to_grid` / `_sync_grid_to_tiles`, and `ecological_events.process_events` has an arg-count mismatch (`process_drought` / `process_wildfire` / `process_marine` expect 8–9 args, fewer passed).\n- **`WeatherScript` + `ClimateEffectsScript`** β€” empty stubs; aborts propagate and kill the arena turn loop.\n\nAfter p0-30's deletion, ecology runs **0Γ— per turn**. Flora canopy/undergrowth does not evolve β€” wild biome simulation is frozen. This objective narrowly restores the Rust ecology tick by fixing the `ClimateScript` bugs and re-enabling the call site. The two empty-stub siblings (`WeatherScript` / `ClimateEffectsScript`) are out of scope for p0-31 β€” they're deferred to follow-ups since they require full implementation, not bug repair.\n\nThis objective unblocks p0-30 bullet 4: once ecology ticks via Rust, a 10-seed batch can capture evolving canopy values and p0-30 flips βœ… done." }, { "id": "p0-32", - "title": "Restore WeatherScript + ClimateEffectsScript \u2014 per-turn weather and climate-effects", + "title": "Restore WeatherScript + ClimateEffectsScript β€” per-turn weather and climate-effects", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "p0-31 restored the Rust ecology tick via `ClimateScript.process_turn` but left\nthe two sibling `process_turn` calls in `turn_processor.gd::_process_climate`\ncommented out (see the trailing comment on `_process_climate` after p0-31\nlanded). Both classes were empty stubs: calling their `process_turn` aborted\n`next_player` and killed the arena turn loop.\n\nThis objective lands Rust source-of-truth for both surfaces per Rail-1\n(ALL game logic in Rust crates, GDScript is thin marshaler):\n\n- `mc_climate::weather::derive_events(grid, thresholds, turn, seed)` \u2014\n pure function that reads the shared GdGridState temperature / moisture\n fields and emits a deterministic list of storm / heat_wave / blizzard\n events for this turn. Thresholds live in `climate_spec.json \u2192\n weather.thresholds`, so no magic constants are hardcoded.\n- `mc_climate::climate_effects::apply(&mut grid, events, units)` \u2014\n pure function that falls-off temperature + moisture deltas over each\n event's hex radius (clamped to [0,1]) and computes per-unit HP loss +\n movement penalties. Severity and scale are derived once on the Rust side.\n- `GdWeatherPhysics` + `GdClimateEffectsPhysics` in `api-gdext/src/lib.rs`\n \u2014 stateless JSON-in, Dictionary-out bridges following the same pattern\n as `GdEconomy` / `GdCulture` / `GdTechWeb`.\n- `weather.gd` + `climate_effects.gd` \u2014 thin marshalers that serialise\n grid state via the existing `_grid: GdGridState` on the TurnManager's\n climate instance, call Rust, and fan outputs back to the Weather\n `get_active_effects` consumer and the unit roster (HP loss + death\n dispatch via `EventBus.unit_destroyed`).\n- `turn_processor.gd::_process_climate` \u2014 the two `WeatherScript.process_turn`\n / `ClimateEffectsScript.process_turn` calls are uncommented; the\n `_process_climate` docstring now reflects the full\n marine_harvest \u2192 climate \u2192 weather \u2192 climate_effects chain." + "summary": "p0-31 restored the Rust ecology tick via `ClimateScript.process_turn` but left\nthe two sibling `process_turn` calls in `turn_processor.gd::_process_climate`\ncommented out (see the trailing comment on `_process_climate` after p0-31\nlanded). Both classes were empty stubs: calling their `process_turn` aborted\n`next_player` and killed the arena turn loop.\n\nThis objective lands Rust source-of-truth for both surfaces per Rail-1\n(ALL game logic in Rust crates, GDScript is thin marshaler):\n\n- `mc_climate::weather::derive_events(grid, thresholds, turn, seed)` β€”\n pure function that reads the shared GdGridState temperature / moisture\n fields and emits a deterministic list of storm / heat_wave / blizzard\n events for this turn. Thresholds live in `climate_spec.json β†’\n weather.thresholds`, so no magic constants are hardcoded.\n- `mc_climate::climate_effects::apply(&mut grid, events, units)` β€”\n pure function that falls-off temperature + moisture deltas over each\n event's hex radius (clamped to [0,1]) and computes per-unit HP loss +\n movement penalties. Severity and scale are derived once on the Rust side.\n- `GdWeatherPhysics` + `GdClimateEffectsPhysics` in `api-gdext/src/lib.rs`\n β€” stateless JSON-in, Dictionary-out bridges following the same pattern\n as `GdEconomy` / `GdCulture` / `GdTechWeb`.\n- `weather.gd` + `climate_effects.gd` β€” thin marshalers that serialise\n grid state via the existing `_grid: GdGridState` on the TurnManager's\n climate instance, call Rust, and fan outputs back to the Weather\n `get_active_effects` consumer and the unit roster (HP loss + death\n dispatch via `EventBus.unit_destroyed`).\n- `turn_processor.gd::_process_climate` β€” the two `WeatherScript.process_turn`\n / `ClimateEffectsScript.process_turn` calls are uncommented; the\n `_process_climate` docstring now reflects the full\n marine_harvest β†’ climate β†’ weather β†’ climate_effects chain." }, { "id": "p0-33", - "title": "World-map input wiring \u2014 unit selection panel, city click, ESC/F10 menu, panel close", + "title": "World-map input wiring β€” unit selection panel, city click, ESC/F10 menu, panel close", "priority": "p0", "status": "done", "scope": "game1", "owner": "wireguard", "updated_at": "2026-04-19", "blocked_by": [], - "summary": "The world-map is unplayable because basic interaction is broken: clicking a unit does nothing visible, clicking a city does nothing, and there is no way to exit the game (ESC and F10 are both dead). Five discrete wiring gaps are responsible:\n\n1. **Unit selection produces no feedback** \u2014 `_select_unit()` in `world_map.gd` calls `_hud.show_unit_panel()` (the slim programmatic panel in `world_map_hud.gd`) and emits `EventBus.unit_selected`, but the panel is either invisible or off-screen. The richer `unit_panel.tscn` (which listens on `EventBus.unit_selected` and renders full stats + action buttons) is never instantiated in the scene tree.\n\n2. **City single-click enters bombard mode, not city screen** \u2014 `_handle_hex_click()` (lines ~350-358 of `world_map.gd`) checks `not city_ref.has_bombarded` and sets `_bombard_city`, consuming the click without opening the city screen. The city screen only opens via double-click through `_unhandled_input`. Single-click on a city should open the city screen; bombard should require an explicit secondary action (right-click or dedicated button).\n\n3. **F10 is unbound** \u2014 no `KEY_F10` handler anywhere in the project.\n\n4. **ESC does not open the in-game menu** \u2014 `main.gd._unhandled_key_input` has the correct logic (`push_overlay(\"res://engine/scenes/ui/ingame_menu.tscn\")`) but it may be racing with `world_map.gd._unhandled_input` or simply not firing when expected. Needs verification and a reliable binding for both ESC (when no panel is open) and F10 (always).\n\n5. **ESC does not close open panels** \u2014 `city_screen.gd` has no `_unhandled_input` / `_unhandled_key_input` handler; closing requires clicking the close button. ESC should close the top-most open panel (city screen, tech tree, chronicle) and bubble up to the in-game menu only when no panel is open." + "summary": "The world-map is unplayable because basic interaction is broken: clicking a unit does nothing visible, clicking a city does nothing, and there is no way to exit the game (ESC and F10 are both dead). Five discrete wiring gaps are responsible:\n\n1. **Unit selection produces no feedback** β€” `_select_unit()` in `world_map.gd` calls `_hud.show_unit_panel()` (the slim programmatic panel in `world_map_hud.gd`) and emits `EventBus.unit_selected`, but the panel is either invisible or off-screen. The richer `unit_panel.tscn` (which listens on `EventBus.unit_selected` and renders full stats + action buttons) is never instantiated in the scene tree.\n\n2. **City single-click enters bombard mode, not city screen** β€” `_handle_hex_click()` (lines ~350-358 of `world_map.gd`) checks `not city_ref.has_bombarded` and sets `_bombard_city`, consuming the click without opening the city screen. The city screen only opens via double-click through `_unhandled_input`. Single-click on a city should open the city screen; bombard should require an explicit secondary action (right-click or dedicated button).\n\n3. **F10 is unbound** β€” no `KEY_F10` handler anywhere in the project.\n\n4. **ESC does not open the in-game menu** β€” `main.gd._unhandled_key_input` has the correct logic (`push_overlay(\"res://engine/scenes/ui/ingame_menu.tscn\")`) but it may be racing with `world_map.gd._unhandled_input` or simply not firing when expected. Needs verification and a reliable binding for both ESC (when no panel is open) and F10 (always).\n\n5. **ESC does not close open panels** β€” `city_screen.gd` has no `_unhandled_input` / `_unhandled_key_input` handler; closing requires clicking the close button. ESC should close the top-most open panel (city screen, tech tree, chronicle) and bubble up to the in-game menu only when no panel is open." }, { "id": "p0-34", - "title": "Freepeople tribe-founding cinematic \u2014 turn -1 / 0 / 1 start sequence and Dwarf Tribe founder unit", + "title": "Freepeople tribe-founding cinematic β€” turn -1 / 0 / 1 start sequence and Dwarf Tribe founder unit", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "Implement a scripted opening sequence that runs on turns **-1**, **0**, and **1** before normal gameplay begins. Turn numbering skips from -1 to 0 to 1 (no \"turn -0.5\" or similar; -1 and 0 are both real turns but the player has no unit to command).\n\n1. **Turn -1 \u2014 Dispersed wanderers.** A **spawn box** is placed around each player's designated starting region. Inside the box, `N` ordinary free-dwarf wanderers spawn \u2014 **no `player_ancestor` flag, no pre-decided allegiance**. They are just freepeople. Each wanderer independently rolls a movement direction for turn -1 \u2192 0. The roll is biased so that **at least `min_ancestors_to_form_tribe` (default 3) are guaranteed to roll \"toward box center\"** \u2014 these become the tribe founders at resolution time, *emergently*, not by pre-tagging. The remaining wanderers roll freely and may move outward or laterally. Fog is partially lifted so the player sees the whole box. The only legal input is **End Turn** (or Enter).\n2. **Turn 0 \u2014 Convergence / tribe formation.** Wanderers step along their rolled directions (deterministic from seed). At end-of-turn-0 resolution: the wanderers that ended up within `tribe_convergence_radius` of the box centroid merge into a single **Dwarf Tribe** unit at the centroid hex and are consumed. This is the player's founding tribe. **Wanderers that did NOT converge are NOT consumed** \u2014 they remain on the map as ordinary freepeople NPCs and continue their wander behavior (per `public/resources/villages/freepeople.json` rules). Pairs/trios of surviving non-converged wanderers may later coalesce into `nomadic_band` camps \u2192 grow into **freehavens** \u2192 evolve into city-states adjacent to the player (human or AI). This is the same mechanic as the player's own founding, applied generally: **any** 3+ freepeople that get within convergence radius form a camp; camps grow into havens. Again the only legal input is End Turn during this opening.\n3. **Turn 1 \u2014 First city.** The Dwarf Tribe unit appears under player control as a **movable unit** with action bar `[\"found_capital\", \"move\"]` and **1 movement point**: the player may either found the capital on the centroid hex immediately or relocate it one hex before founding. On founding, the capital's starting population is determined by the mode (see below), the Dwarf Tribe unit is consumed, and normal Game 1 play begins. (AI/NPC tribes do not exercise the move option \u2014 they auto-found at the centroid; only player-controlled tribes get the move-vs-found choice.) All *subsequent* settlers built by cities are ordinary **Founder** units that always produce a pop-1 city.\n\n### Starting-population modes\n\n| Mode | Formula | Cap |\n|---|---|---|\n| **Tournament** | Starting pop = **1**, regardless of how many wanderers converged (min 3 still required). Guaranteed-convergence count is pinned to exactly 3; no extras are biased inward. | Fixed. |\n| **Custom** (default for single-player casual) | Starting pop = `1 + floor((wanderers_converged - 3) / 3)` \u2014 each wanderer past the 3rd contributes **+1/3 pop**, rounded down at founding. Extra inward-biased rolls (beyond the guaranteed 3) are possible so variance can go up. | `custom_max_bonus_pop = 3`; with the custom wanderer count capped at 9 the practical ceiling is **pop 3 from 9 converged**. Tunable in `setup.json`. |\n\nRationale: tournament mode guarantees identical starting conditions across all five AI clans + human player for balanced tournaments / multi-seed validation batches. Custom mode lets the spawn roll matter and rewards regions where more wanderers happen to converge (slightly favoring bountiful biomes in a later \"starting position type\" selector \u2014 out of scope here).\n\n### Roll bias mechanics\n\nFor each player's spawn box of `N` wanderers (`N \u2248 3..9`, seeded per map; tournament pins `N = 3`, custom rolls `N \u2208 [3, 9]`):\n- **Tournament**: exactly 3 wanderers get `direction = inward`; the remaining `N-3` roll uniformly from all 6 hex directions.\n- **Custom**: 3 wanderers are pinned inward (floor guarantee); each of the remaining `N-3` independently rolls `inward_bias_prob` (default `0.33`) to also go inward, else uniform. This lets 3\u2013`N` converge.\n- \"Inward\" means \"one of the 2 hex directions whose dot product with `centroid - wanderer_pos` is most positive\" \u2014 picked uniformly among ties, still deterministic from seed.\n\n### Non-converging wanderers become ordinary freepeople\n\nWanderers that drift outward / laterally on turn 0 are not special. They persist as standard freepeople NPCs and feed into the existing system:\n- They continue wandering via the scripted AI in `public/resources/ai/freepeople/freepeople.json`.\n- When 3+ freepeople (from *any* source \u2014 prologue drift, ongoing camp expansion, migration) get within `tribe_convergence_radius` of each other, they form a `nomadic_band` camp (`freepeople.json:camp_types[0]`).\n- Camps grow per `freepeople.json:growth` \u2014 at `expansion_threshold = 30` they may become **freehavens**, and high-ecology-tier havens may eventually emerge as city-states neighboring the player.\n- This means the opening cinematic *also* seeds rival neighbors: players who spawned with a dense box get more surviving drifters \u2192 more potential adjacent freehavens \u2192 more mid-game pressure. That pressure is symmetric across tournament mode (all players get `N=baseline`) and asymmetric in custom mode.\n\n### Why Dwarf Tribe \u2260 Founder\n\n- **Dwarf Tribe** (new unit): spawned only by the turn-0 convergence event. Carries `founding_pop_override: int` set at spawn time. **Movable** under player control \u2014 action bar `[\"found_capital\", \"move\"]`, 1 MP \u2014 so the player can reposition one hex before founding; AI/NPC tribes auto-found at the centroid and never move. Cannot be built by cities. Never appears again after turn 1.\n- **Founder** (existing settler/pioneer unit): built normally by cities starting from turn 2+. Always founds a pop-1 city. No `founding_pop_override`.\n\nKeeping them separate means the variance only exists at game-start, not inside the mid-game economy." + "summary": "Implement a scripted opening sequence that runs on turns **-1**, **0**, and **1** before normal gameplay begins. Turn numbering skips from -1 to 0 to 1 (no \"turn -0.5\" or similar; -1 and 0 are both real turns but the player has no unit to command).\n\n1. **Turn -1 β€” Dispersed wanderers.** A **spawn box** is placed around each player's designated starting region. Inside the box, `N` ordinary free-dwarf wanderers spawn β€” **no `player_ancestor` flag, no pre-decided allegiance**. They are just freepeople. Each wanderer independently rolls a movement direction for turn -1 β†’ 0. The roll is biased so that **at least `min_ancestors_to_form_tribe` (default 3) are guaranteed to roll \"toward box center\"** β€” these become the tribe founders at resolution time, *emergently*, not by pre-tagging. The remaining wanderers roll freely and may move outward or laterally. Fog is partially lifted so the player sees the whole box. The only legal input is **End Turn** (or Enter).\n2. **Turn 0 β€” Convergence / tribe formation.** Wanderers step along their rolled directions (deterministic from seed). At end-of-turn-0 resolution: the wanderers that ended up within `tribe_convergence_radius` of the box centroid merge into a single **Dwarf Tribe** unit at the centroid hex and are consumed. This is the player's founding tribe. **Wanderers that did NOT converge are NOT consumed** β€” they remain on the map as ordinary freepeople NPCs and continue their wander behavior (per `public/resources/villages/freepeople.json` rules). Pairs/trios of surviving non-converged wanderers may later coalesce into `nomadic_band` camps β†’ grow into **freehavens** β†’ evolve into city-states adjacent to the player (human or AI). This is the same mechanic as the player's own founding, applied generally: **any** 3+ freepeople that get within convergence radius form a camp; camps grow into havens. Again the only legal input is End Turn during this opening.\n3. **Turn 1 β€” First city.** The Dwarf Tribe unit appears under player control as a **movable unit** with action bar `[\"found_capital\", \"move\"]` and **1 movement point**: the player may either found the capital on the centroid hex immediately or relocate it one hex before founding. On founding, the capital's starting population is determined by the mode (see below), the Dwarf Tribe unit is consumed, and normal Game 1 play begins. (AI/NPC tribes do not exercise the move option β€” they auto-found at the centroid; only player-controlled tribes get the move-vs-found choice.) All *subsequent* settlers built by cities are ordinary **Founder** units that always produce a pop-1 city.\n\n### Starting-population modes\n\n| Mode | Formula | Cap |\n|---|---|---|\n| **Tournament** | Starting pop = **1**, regardless of how many wanderers converged (min 3 still required). Guaranteed-convergence count is pinned to exactly 3; no extras are biased inward. | Fixed. |\n| **Custom** (default for single-player casual) | Starting pop = `1 + floor((wanderers_converged - 3) / 3)` β€” each wanderer past the 3rd contributes **+1/3 pop**, rounded down at founding. Extra inward-biased rolls (beyond the guaranteed 3) are possible so variance can go up. | `custom_max_bonus_pop = 3`; with the custom wanderer count capped at 9 the practical ceiling is **pop 3 from 9 converged**. Tunable in `setup.json`. |\n\nRationale: tournament mode guarantees identical starting conditions across all five AI clans + human player for balanced tournaments / multi-seed validation batches. Custom mode lets the spawn roll matter and rewards regions where more wanderers happen to converge (slightly favoring bountiful biomes in a later \"starting position type\" selector β€” out of scope here).\n\n### Roll bias mechanics\n\nFor each player's spawn box of `N` wanderers (`N β‰ˆ 3..9`, seeded per map; tournament pins `N = 3`, custom rolls `N ∈ [3, 9]`):\n- **Tournament**: exactly 3 wanderers get `direction = inward`; the remaining `N-3` roll uniformly from all 6 hex directions.\n- **Custom**: 3 wanderers are pinned inward (floor guarantee); each of the remaining `N-3` independently rolls `inward_bias_prob` (default `0.33`) to also go inward, else uniform. This lets 3–`N` converge.\n- \"Inward\" means \"one of the 2 hex directions whose dot product with `centroid - wanderer_pos` is most positive\" β€” picked uniformly among ties, still deterministic from seed.\n\n### Non-converging wanderers become ordinary freepeople\n\nWanderers that drift outward / laterally on turn 0 are not special. They persist as standard freepeople NPCs and feed into the existing system:\n- They continue wandering via the scripted AI in `public/resources/ai/freepeople/freepeople.json`.\n- When 3+ freepeople (from *any* source β€” prologue drift, ongoing camp expansion, migration) get within `tribe_convergence_radius` of each other, they form a `nomadic_band` camp (`freepeople.json:camp_types[0]`).\n- Camps grow per `freepeople.json:growth` β€” at `expansion_threshold = 30` they may become **freehavens**, and high-ecology-tier havens may eventually emerge as city-states neighboring the player.\n- This means the opening cinematic *also* seeds rival neighbors: players who spawned with a dense box get more surviving drifters β†’ more potential adjacent freehavens β†’ more mid-game pressure. That pressure is symmetric across tournament mode (all players get `N=baseline`) and asymmetric in custom mode.\n\n### Why Dwarf Tribe β‰  Founder\n\n- **Dwarf Tribe** (new unit): spawned only by the turn-0 convergence event. Carries `founding_pop_override: int` set at spawn time. **Movable** under player control β€” action bar `[\"found_capital\", \"move\"]`, 1 MP β€” so the player can reposition one hex before founding; AI/NPC tribes auto-found at the centroid and never move. Cannot be built by cities. Never appears again after turn 1.\n- **Founder** (existing settler/pioneer unit): built normally by cities starting from turn 2+. Always founds a pop-1 city. No `founding_pop_override`.\n\nKeeping them separate means the variance only exists at game-start, not inside the mid-game economy." }, { "id": "p0-37", @@ -372,7 +372,7 @@ "owner": "warcouncil", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "The p0-26 tactical port faithfully copied 7 tuning constants from\n`simple_heuristic_ai.gd` into Rust. They're currently flat globals that ignore\npersonality axes and difficulty tier, which means:\n\n- **Rail-2 violation**: gameplay tuning hardcoded in Rust instead of derived\n from JSON-owned data (`ai_personalities.json::strategic_axes`).\n- **Personality suppression**: every clan uses the same posture-flip threshold,\n so aggression / grudge_persistence / wealth axes only affect production\n scoring, not commit-to-assault decisions. Clan flavor flattens on the\n tactical layer.\n- **Downstream gate failures**: p0-01 tier_peak, p0-02 era-divergence, p0-22\n median-turn all share the same root \u2014 games resolve T39-T100 via\n rush-domination because one global factor governs every clan's\n rush-commit decision.\n\nThe existing `ScoringWeights::apply_axes` (evaluator.rs:180-204) already\nproves the pattern works: `aggression` scales `military_base`, `expansion`\nscales `site_food`. That pattern stops at scoring; it should continue into\nposture / retreat / chase / siege thresholds.\n\nResearch basis (2024-2025):\n- **Sims 3 / Richard Evans (Game AI Pro)**: axis-shaped utility \u2192 NPCs diverge\n in identical states. 16 years of production evidence.\n- **Tactical Troops: Anthracite Shift**: utility-AI-scored orders feed MCTS\n priors. Our axis-derived thresholds are the utility layer.\n- **Vox Deorum (Civ-V, arxiv 2512.18564, Dec 2025)**: validates\n macro/tactical decoupling across 2,327 games \u2014 our MCTS-strategic +\n axis-driven-tactical layering sits in the sweet spot." + "summary": "The p0-26 tactical port faithfully copied 7 tuning constants from\n`simple_heuristic_ai.gd` into Rust. They're currently flat globals that ignore\npersonality axes and difficulty tier, which means:\n\n- **Rail-2 violation**: gameplay tuning hardcoded in Rust instead of derived\n from JSON-owned data (`ai_personalities.json::strategic_axes`).\n- **Personality suppression**: every clan uses the same posture-flip threshold,\n so aggression / grudge_persistence / wealth axes only affect production\n scoring, not commit-to-assault decisions. Clan flavor flattens on the\n tactical layer.\n- **Downstream gate failures**: p0-01 tier_peak, p0-02 era-divergence, p0-22\n median-turn all share the same root β€” games resolve T39-T100 via\n rush-domination because one global factor governs every clan's\n rush-commit decision.\n\nThe existing `ScoringWeights::apply_axes` (evaluator.rs:180-204) already\nproves the pattern works: `aggression` scales `military_base`, `expansion`\nscales `site_food`. That pattern stops at scoring; it should continue into\nposture / retreat / chase / siege thresholds.\n\nResearch basis (2024-2025):\n- **Sims 3 / Richard Evans (Game AI Pro)**: axis-shaped utility β†’ NPCs diverge\n in identical states. 16 years of production evidence.\n- **Tactical Troops: Anthracite Shift**: utility-AI-scored orders feed MCTS\n priors. Our axis-derived thresholds are the utility layer.\n- **Vox Deorum (Civ-V, arxiv 2512.18564, Dec 2025)**: validates\n macro/tactical decoupling across 2,327 games β€” our MCTS-strategic +\n axis-driven-tactical layering sits in the sweet spot." }, { "id": "p0-38", @@ -383,22 +383,22 @@ "owner": "warcouncil", "updated_at": "2026-04-24", "blocked_by": [], - "summary": "Current MCTS selection uses classical UCB1 at tree nodes \u2014 all actions start\nwith equal prior, exploration is driven only by visit count. `ScoringWeights`\nand `strategic_axes` feed the *tactical executor* and *leaf evaluator* but\nNOT the tree-selection step. This means MCTS explores the same branches for\nevery clan; divergence only appears at the leaf.\n\nAlphaGo's core contribution was **learned priors** seeded into the tree. We\ndon't need learning \u2014 we have personality utility. Inject it as the `P(s,a)`\nterm in the PUCT / UCB1-with-prior formula:\n\n```\nscore(a) = Q(s,a) + c_puct \u00d7 P(s,a) \u00d7 sqrt(N(s)) / (1 + N(s,a))\n```\n\nWhere `P(s,a) = softmax(personality_utility(state, action) / temperature)`\nand `personality_utility` is the same `ScoringWeights`-driven evaluator used\nat the leaf.\n\nEffect: blackhammer's MCTS tree spends more branches on early assault\nvariants; goldvein's tree spends more branches on tech-up + defend variants.\nWithout the prior, both clans' trees are identical shape \u2014 only the leaf\nevaluator differs, and leaf evaluation is after 20+ turns of rollout where\nthe differentiating choice has already been washed out." + "summary": "Current MCTS selection uses classical UCB1 at tree nodes β€” all actions start\nwith equal prior, exploration is driven only by visit count. `ScoringWeights`\nand `strategic_axes` feed the *tactical executor* and *leaf evaluator* but\nNOT the tree-selection step. This means MCTS explores the same branches for\nevery clan; divergence only appears at the leaf.\n\nAlphaGo's core contribution was **learned priors** seeded into the tree. We\ndon't need learning β€” we have personality utility. Inject it as the `P(s,a)`\nterm in the PUCT / UCB1-with-prior formula:\n\n```\nscore(a) = Q(s,a) + c_puct Γ— P(s,a) Γ— sqrt(N(s)) / (1 + N(s,a))\n```\n\nWhere `P(s,a) = softmax(personality_utility(state, action) / temperature)`\nand `personality_utility` is the same `ScoringWeights`-driven evaluator used\nat the leaf.\n\nEffect: blackhammer's MCTS tree spends more branches on early assault\nvariants; goldvein's tree spends more branches on tech-up + defend variants.\nWithout the prior, both clans' trees are identical shape β€” only the leaf\nevaluator differs, and leaf evaluation is after 20+ turns of rollout where\nthe differentiating choice has already been washed out." }, { "id": "p0-39", - "title": "AI tier-progression unit selection \u2014 production.rs picks tier-2+ units once tech unlocks", + "title": "AI tier-progression unit selection β€” production.rs picks tier-2+ units once tech unlocks", "priority": "p0", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "Shipwright audit 2026-04-18 of tech_web.json + research costs (requested by warcouncil session-close handoff) found the tech tree, costs, and research pacing are correct. `peak_unit_tier=1` universally is NOT a balance-data issue. Root cause is in the tactical AI's production-selection logic:\n\n**`src/simulator/crates/mc-ai/src/tactical/production.rs:72-80`** \u2014 the `ids` module hardcodes only tier-1 unit IDs (`WARRIOR`, `WORKER`, `FOUNDER`, `WALLS`, `FORGE`, `CASTLE`, `MARKETPLACE`, `GRANARY`). The priority ladder in `decide_production()` pulls exclusively from this list. When `bronze_working` researches (reliably by turn ~72) and enables `pikeman` (tier-2), the tactical AI has no branch that picks it. Same gap blocks berserker, runesmith, cavalry, ironwarden, forge_titan, mithril_vanguard.\n\n### Empirical evidence (batch `apricot-20260418_062941`, T300)\n\n- 53 techs researched by T300 per player \u2014 tech pipeline flows correctly\n- `bronze_working` researched turn 72 in one inspected seed\n- Zero pikemen built across any seed\n- Units built: 393\u00d7 warrior, 4\u00d7 worker, 2\u00d7 founder, 2\u00d7 dwarf_tribe \u2014 all tier-1\n- Telemetry honest: `peak_unit_tier` reads `DataLoader.get_unit(type_id).tier`; it reports 1 because tier-1 is all that exists in live gameplay" + "summary": "Shipwright audit 2026-04-18 of tech_web.json + research costs (requested by warcouncil session-close handoff) found the tech tree, costs, and research pacing are correct. `peak_unit_tier=1` universally is NOT a balance-data issue. Root cause is in the tactical AI's production-selection logic:\n\n**`src/simulator/crates/mc-ai/src/tactical/production.rs:72-80`** β€” the `ids` module hardcodes only tier-1 unit IDs (`WARRIOR`, `WORKER`, `FOUNDER`, `WALLS`, `FORGE`, `CASTLE`, `MARKETPLACE`, `GRANARY`). The priority ladder in `decide_production()` pulls exclusively from this list. When `bronze_working` researches (reliably by turn ~72) and enables `pikeman` (tier-2), the tactical AI has no branch that picks it. Same gap blocks berserker, runesmith, cavalry, ironwarden, forge_titan, mithril_vanguard.\n\n### Empirical evidence (batch `apricot-20260418_062941`, T300)\n\n- 53 techs researched by T300 per player β€” tech pipeline flows correctly\n- `bronze_working` researched turn 72 in one inspected seed\n- Zero pikemen built across any seed\n- Units built: 393Γ— warrior, 4Γ— worker, 2Γ— founder, 2Γ— dwarf_tribe β€” all tier-1\n- Telemetry honest: `peak_unit_tier` reads `DataLoader.get_unit(type_id).tier`; it reports 1 because tier-1 is all that exists in live gameplay" }, { "id": "p0-40", - "title": "Iron-ore strategic resource density \u2014 unblock tier 3-6 unit chain", + "title": "Iron-ore strategic resource density β€” unblock tier 3-6 unit chain", "priority": "p0", "status": "done", "scope": "game1", @@ -409,73 +409,73 @@ }, { "id": "p0-41", - "title": "Building rally points \u2014 produced units auto-deploy to a designated hex", + "title": "Building rally points β€” produced units auto-deploy to a designated hex", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-24", "blocked_by": [], - "summary": "Unit-producing buildings (barracks and others with `can_rally: true`) can have an optional rally hex + default command (Defend / Patrol / Advance). Units produced at such a building are automatically issued a move order to the rally hex on spawn. Once they arrive they auto-join the formation at that hex (if auto_join is enabled). This is the supply pipeline that feeds formations \u2014 the rally point is how the player designates where their growing army should concentrate without micromanaging every produced unit." + "summary": "Unit-producing buildings (barracks and others with `can_rally: true`) can have an optional rally hex + default command (Defend / Patrol / Advance). Units produced at such a building are automatically issued a move order to the rally hex on spawn. Once they arrive they auto-join the formation at that hex (if auto_join is enabled). This is the supply pipeline that feeds formations β€” the rally point is how the player designates where their growing army should concentrate without micromanaging every produced unit." }, { "id": "p0-41a", - "title": "Rally-point smoke \u2014 produced unit gets PatrolOrder toward rally hex", + "title": "Rally-point smoke β€” produced unit gets PatrolOrder toward rally hex", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "End-to-end smoke verification for the rally-point feature (p0-41). Originally framed as needing a weston display server on apricot (\"set rally on barracks UI \u2192 produce unit \u2192 screenshot the move\"), but the same coverage is achievable via Rust unit tests against `try_spawn_unit` \u2014 the rally behavior is fully encoded in `processor.rs:768-782` and the resulting `PatrolOrder` is the contract that drives subsequent movement." + "summary": "End-to-end smoke verification for the rally-point feature (p0-41). Originally framed as needing a weston display server on apricot (\"set rally on barracks UI β†’ produce unit β†’ screenshot the move\"), but the same coverage is achievable via Rust unit tests against `try_spawn_unit` β€” the rally behavior is fully encoded in `processor.rs:768-782` and the resulting `PatrolOrder` is the contract that drives subsequent movement." }, { "id": "p0-42", - "title": "Formation aggregation \u2014 adjacent units link into a shaped formation with terrain reflow", + "title": "Formation aggregation β€” adjacent units link into a shaped formation with terrain reflow", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "Units in adjacent hexes (same owner, both with auto_join enabled) automatically link into a Formation. Each unit retains its own hex \u2014 no stacking. The formation has a defined shape (Line, Column, Wedge, Diamond) expressed as relative hex offsets from a leader unit. When the formation moves, a reflow solver computes target hexes for all members: if the preferred shape doesn't fit terrain (e.g. a 5-wide Line entering a 2-hex canyon), it automatically compresses to a Column and re-expands on exit. Combat with formation_count set from the number of linked units uses the existing `dmg \u00d7 count^0.75` and `HP \u00d7 count` scaling already in mc-combat/resolver.rs. Selection: single-click selects formation; double-click selects the individual unit. 'Exit Formation' button in unit panel splits the unit back to solo." + "summary": "Units in adjacent hexes (same owner, both with auto_join enabled) automatically link into a Formation. Each unit retains its own hex β€” no stacking. The formation has a defined shape (Line, Column, Wedge, Diamond) expressed as relative hex offsets from a leader unit. When the formation moves, a reflow solver computes target hexes for all members: if the preferred shape doesn't fit terrain (e.g. a 5-wide Line entering a 2-hex canyon), it automatically compresses to a Column and re-expands on exit. Combat with formation_count set from the number of linked units uses the existing `dmg Γ— count^0.75` and `HP Γ— count` scaling already in mc-combat/resolver.rs. Selection: single-click selects formation; double-click selects the individual unit. 'Exit Formation' button in unit panel splits the unit back to solo." }, { "id": "p0-42a", - "title": "Formation aggregation smoke \u2014 formations form and evolve at runtime", + "title": "Formation aggregation smoke β€” formations form and evolve at runtime", "priority": "p0", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "End-to-end smoke verification for the formation aggregation feature (p0-42). Spun off from p0-42 on 2026-04-25 originally framed as needing weston (display server) but resolved 2026-04-25 via headless run \u2014 formation evidence surfaces in `game.log` via `AiTurnBridge: formations turn=N player=P count=C sizes=[...] tiers=[...]` lines, no display server required." + "summary": "End-to-end smoke verification for the formation aggregation feature (p0-42). Spun off from p0-42 on 2026-04-25 originally framed as needing weston (display server) but resolved 2026-04-25 via headless run β€” formation evidence surfaces in `game.log` via `AiTurnBridge: formations turn=N player=P count=C sizes=[...] tiers=[...]` lines, no display server required." }, { "id": "p0-43", - "title": "Formation AI \u2014 MCTS plans at formation level, not per-unit", + "title": "Formation AI β€” MCTS plans at formation level, not per-unit", "priority": "p0", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "After p0-42 lands, the MCTS strategic planner should treat formations as the atomic military entity rather than individual units. The abstract rollout state (AbstractPlayerState in mc-ai/src/abstract_state.rs) is updated to track formation count + tier + strength instead of raw unit_counts. Action candidates include CommandFormation (advance formation to hex) scored by military axis. The AI builds up a formation at a rally point then commands it to advance \u2014 matching the TA-style intended gameplay. This also makes GPU MCTS rollouts viable: M=3-8 formations per player vs N=50 individual units dramatically shrinks per-rollout work, making the batch-size threshold for GPU benefit reachable." + "summary": "After p0-42 lands, the MCTS strategic planner should treat formations as the atomic military entity rather than individual units. The abstract rollout state (AbstractPlayerState in mc-ai/src/abstract_state.rs) is updated to track formation count + tier + strength instead of raw unit_counts. Action candidates include CommandFormation (advance formation to hex) scored by military axis. The AI builds up a formation at a rally point then commands it to advance β€” matching the TA-style intended gameplay. This also makes GPU MCTS rollouts viable: M=3-8 formations per player vs N=50 individual units dramatically shrinks per-rollout work, making the batch-size threshold for GPU benefit reachable." }, { "id": "p0-44", - "title": "Movement mode UX \u2014 Move button, path preview, right-click confirm, fog-aware pathing", + "title": "Movement mode UX β€” Move button, path preview, right-click confirm, fog-aware pathing", "priority": "p0", "status": "done", "scope": "game1", "owner": "wireguard", "updated_at": "2026-04-19", "blocked_by": [], - "summary": "Movement is currently a silent left-click on a reachable hex \u2014 no path shown, no\nconfirmation step. Players expect the Civ-style flow: enter movement mode (M key\nor Move button), see a path preview, right-click to confirm. This objective\nadds the full movement-mode state machine, path rendering, fog-of-war-aware\npathing, and the Move button on the unit action panel with disabled-state\ntooltips for all action buttons.\n\nDepends on **p0-33** (unit panel must be in the scene tree before the Move\nbutton can be wired)." + "summary": "Movement is currently a silent left-click on a reachable hex β€” no path shown, no\nconfirmation step. Players expect the Civ-style flow: enter movement mode (M key\nor Move button), see a path preview, right-click to confirm. This objective\nadds the full movement-mode state machine, path rendering, fog-of-war-aware\npathing, and the Move button on the unit action panel with disabled-state\ntooltips for all action buttons.\n\nDepends on **p0-33** (unit panel must be in the scene tree before the Move\nbutton can be wired)." }, { "id": "p0-45", - "title": "Turn processor consolidation \u2014 entities/ duplicate caused T1 SCRIPT ERROR halt", + "title": "Turn processor consolidation β€” entities/ duplicate caused T1 SCRIPT ERROR halt", "priority": "p0", "status": "done", "scope": "game1", @@ -486,7 +486,7 @@ }, { "id": "g2-07", - "title": "Flora succession \u2014 wire the existing flora lifecycle engine into the playable turn", + "title": "Flora succession β€” wire the existing flora lifecycle engine into the playable turn", "priority": "p1", "status": "done", "scope": "game1", @@ -494,7 +494,7 @@ "blocked_by": [ "p2-80" ], - "summary": "Climate-driven flora succession (forests advancing/retreating, riparian\nbelts dying back) is the most visible expression of the living-world\nUSP. **The engine already exists and is tested** \u2014 the work is\nintegration, persistence, determinism, and presentation, NOT building a\nconsumer.\n\n- Flora succession engine: `mc-flora/src/engine.rs:352 tick_tiers`\n (tier transitions, T9 event gating) + `mc-flora/src/dynamics.rs:26\n tick_populations`.\n- Tier-advancement is **already wired** into the ecology step:\n `EcologySim::process_step` runs `run_tier_advancement` +\n `run_seed_dispersal` each tick (`mc-ecology/src/engine.rs:276`), and\n `WorldSim::step` already calls `process_step`\n (`mc-worldsim/src/lib.rs:161`).\n\nSpecies data is fully authored: 149/149 flora carry\n`lifecycle.transforms[]` (e.g. `european_beech.json::lifecycle.transforms[0]`\n\u2014 `climate_field: temperature`, `climate_op: >=`, `climate_value: 0.62`,\n`climate_sustained_turns: 200`). What is missing is that this engine runs\nonly at worldgen / in benches today \u2014 it does not advance in the\n**playable** game turn (that goes through bare `TurnProcessor::step`).\n\nThis objective lands once `mc-worldsim` (`p2-80`) drives the step in the\nplayable game: confirm flora succession ticks per played turn, persists,\nis deterministic, and is rendered." + "summary": "Climate-driven flora succession (forests advancing/retreating, riparian\nbelts dying back) is the most visible expression of the living-world\nUSP. **The engine already exists and is tested** β€” the work is\nintegration, persistence, determinism, and presentation, NOT building a\nconsumer.\n\n- Flora succession engine: `mc-flora/src/engine.rs:352 tick_tiers`\n (tier transitions, T9 event gating) + `mc-flora/src/dynamics.rs:26\n tick_populations`.\n- Tier-advancement is **already wired** into the ecology step:\n `EcologySim::process_step` runs `run_tier_advancement` +\n `run_seed_dispersal` each tick (`mc-ecology/src/engine.rs:276`), and\n `WorldSim::step` already calls `process_step`\n (`mc-worldsim/src/lib.rs:161`).\n\nSpecies data is fully authored: 149/149 flora carry\n`lifecycle.transforms[]` (e.g. `european_beech.json::lifecycle.transforms[0]`\nβ€” `climate_field: temperature`, `climate_op: >=`, `climate_value: 0.62`,\n`climate_sustained_turns: 200`). What is missing is that this engine runs\nonly at worldgen / in benches today β€” it does not advance in the\n**playable** game turn (that goes through bare `TurnProcessor::step`).\n\nThis objective lands once `mc-worldsim` (`p2-80`) drives the step in the\nplayable game: confirm flora succession ticks per played turn, persists,\nis deterministic, and is rendered." }, { "id": "p0-20", @@ -505,7 +505,7 @@ "owner": "warcouncil", "updated_at": "2026-05-05", "blocked_by": [], - "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\u00d7256) 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 \u2014 GPU\u2194CPU 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 \u2014 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 \u2192 Naga parse panic \u2192 wgpu_core handler \u2192 try_init\n worker thread panic \u2192 timeout returned None \u2192 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 \u2014 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 \u2192 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) \u2014 **100% agreement, bit-identical** on all 3\nquantitative parity tests (209 inputs total). Pre-fixes: 3\u20136% agreement with\nmax_drift 0.025\u20130.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 \u2014 host-side infrastructure\n\n- `scripts/dev-setup/bluefin.sh` + `./run setup:bluefin` \u2014 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\u2192RUN 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 \u2014 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` T5\u2013T10 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 3\u201310s |\n| `systemd-run --user` | `.local/iter/warcouncil-a5-systemd-*/` | same \u2014 service `Active: inactive (dead)` after 2s, scope children SIGTERMed |\n| `KillMode=none` | `.local/iter/warcouncil-a5-systemd-*` (2nd) | games reached T9\u2013T10 only; same kill pattern |\n| plain `bash autoplay-batch` synchronous | `.local/iter/a5-direct-123300/` | 10 games with 0-line `turn_stats.jsonl` \u2014 games get SIGTERMed during map generation |\n\nSeven distinct execution strategies, same failure pattern: flatpak Godot\nscopes SIGTERMed within 3\u201310s 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 \u2014 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." + "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: 3–6% agreement with\nmax_drift 0.025–0.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` T5–T10 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 3–10s |\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 T9–T10 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 3–10s 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-26b", @@ -520,36 +520,36 @@ }, { "id": "p0-35", - "title": "Ecology telemetry instrumentation \u2014 flora canopy / undergrowth fields in turn_stats.jsonl", + "title": "Ecology telemetry instrumentation β€” flora canopy / undergrowth fields in turn_stats.jsonl", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "`turn_stats.jsonl` currently emits `aggregate.total_combats`, `player_stats.*.tier_peak` etc. (per p0-25) but no flora/ecology fields. p0-30 / p0-31 bullets about \"flora canopy values evolve in turn_stats.jsonl\" cannot empirically close without these fields.\n\nThis objective adds per-turn ecology telemetry so future batches can cite canopy evolution as evidence of a working Rust ecology tick.\n\nScope reduced from P0 to P1 because:\n- The p0-25 gate bullets (tier_peak, peak_unit_tier, wonder_count, combats, cities_founded) already confirm the game plays to victory under the Rust ecology path (smoke5 batch 2026-04-17: 8/10 seeds reached `outcome: victory`, combats 131\u20131686, tier_peak 2\u20136).\n- Canopy instrumentation is a dev-tool nicety, not a shipping gate. Game 1 ships without it; follow-up lands pre-EA-polish." + "summary": "`turn_stats.jsonl` currently emits `aggregate.total_combats`, `player_stats.*.tier_peak` etc. (per p0-25) but no flora/ecology fields. p0-30 / p0-31 bullets about \"flora canopy values evolve in turn_stats.jsonl\" cannot empirically close without these fields.\n\nThis objective adds per-turn ecology telemetry so future batches can cite canopy evolution as evidence of a working Rust ecology tick.\n\nScope reduced from P0 to P1 because:\n- The p0-25 gate bullets (tier_peak, peak_unit_tier, wonder_count, combats, cities_founded) already confirm the game plays to victory under the Rust ecology path (smoke5 batch 2026-04-17: 8/10 seeds reached `outcome: victory`, combats 131–1686, tier_peak 2–6).\n- Canopy instrumentation is a dev-tool nicety, not a shipping gate. Game 1 ships without it; follow-up lands pre-EA-polish." }, { "id": "p0-36", - "title": "Weather / climate-effects event telemetry \u2014 events.jsonl + turn_stats aggregates", + "title": "Weather / climate-effects event telemetry β€” events.jsonl + turn_stats aggregates", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "p0-32 added `WeatherScript.process_turn` + `ClimateEffectsScript.process_turn` over the Rust `mc-climate` crate. The calls run per turn without crashing (smoke5 batch 2026-04-17 confirms), but no weather-event records reach `events.jsonl` or `turn_stats.jsonl` aggregates \u2014 p0-32 bullet 4 \"weather events visible via event log\" cannot close without this wiring.\n\nScope reduced from P0 to P1 because:\n- Weather/climate-effects code runs + applies damage + adjusts tile state (verified by passing cargo tests in `mc-climate`).\n- Events surfacing is a dev/analytics concern, not a shipping gate." + "summary": "p0-32 added `WeatherScript.process_turn` + `ClimateEffectsScript.process_turn` over the Rust `mc-climate` crate. The calls run per turn without crashing (smoke5 batch 2026-04-17 confirms), but no weather-event records reach `events.jsonl` or `turn_stats.jsonl` aggregates β€” p0-32 bullet 4 \"weather events visible via event log\" cannot close without this wiring.\n\nScope reduced from P0 to P1 because:\n- Weather/climate-effects code runs + applies damage + adjusts tile state (verified by passing cargo tests in `mc-climate`).\n- Events surfacing is a dev/analytics concern, not a shipping gate." }, { "id": "p1-01", - "title": "Diplomacy-lite \u2014 peace/war toggle plus one trade action", + "title": "Diplomacy-lite β€” peace/war toggle plus one trade action", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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).~~ **[SUPERSEDED \u2014 see banner: canonical start state is PEACE; war begins on war-dec dispatch per COMMUNICATIONS.md / p3-01.]** GUT coverage in `test_simple_heuristic_ai_war_gate.gd`." + "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).~~ **[SUPERSEDED β€” see banner: canonical start state is PEACE; war begins on war-dec dispatch per COMMUNICATIONS.md / p3-01.]** GUT coverage in `test_simple_heuristic_ai_war_gate.gd`." }, { "id": "p1-02", @@ -571,18 +571,18 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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 \u2014 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 \u2014 tests, proof scenes, and counter\nrendering all read `total_steps()` from the array length." + "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 \u2014 pop_peak \u226530 median, worker improvements \u22658 min", + "title": "Balance tuning β€” pop_peak β‰₯30 median, worker improvements β‰₯8 min", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-05-14", "blocked_by": [], - "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 \u2014 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 \u2192 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 \u2014 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**Done (2026-05-14):** all in-scope JSON tuning bullets landed\n(pop_peak median 69, worker_improvements min 15, techs median 39,\ncombats median 808, strategic_gate_rejections 1670, both-players-T100\n3/6 qualifying). The two remaining bullets (luxury variance,\npersonality_win_balance) are upstream-blocked autoplay-batch sign-off\nwork moved to `p1-05-followup-shipwright-batch.md`.\n\n**2026-04-17 ecology handoff from p0-30:** duplicate GDScript ecology tick\n(`ecosystem.gd` + `flora.gd`) deleted; ecology is dormant until\n`ClimateScript.process_turn` is re-enabled, at which point\n`GdEcologyPhysics::process_step` becomes the sole canonical tick. Any\nwilds/food/lair knobs tuned against the previous 1\u00d7 GDScript rate may\nneed re-tuning against the Rust rate in a follow-up pass." + "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**Done (2026-05-14):** all in-scope JSON tuning bullets landed\n(pop_peak median 69, worker_improvements min 15, techs median 39,\ncombats median 808, strategic_gate_rejections 1670, both-players-T100\n3/6 qualifying). The two remaining bullets (luxury variance,\npersonality_win_balance) are upstream-blocked autoplay-batch sign-off\nwork moved to `p1-05-followup-shipwright-batch.md`.\n\n**2026-04-17 ecology handoff from p0-30:** duplicate GDScript ecology tick\n(`ecosystem.gd` + `flora.gd`) deleted; ecology is dormant until\n`ClimateScript.process_turn` is re-enabled, at which point\n`GdEcologyPhysics::process_step` becomes the sole canonical tick. Any\nwilds/food/lair knobs tuned against the previous 1Γ— GDScript rate may\nneed re-tuning against the Rust rate in a follow-up pass." }, { "id": "p1-06", @@ -593,7 +593,7 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Options screen ships with five sections \u2014 Display, Audio, Camera & Controls,\nGameplay, Game Defaults, Privacy \u2014 backed by `SettingsManager` autoload which\npersists to `user://settings.cfg` and applies every change live. Restore-defaults\nand Back buttons anchor the bottom row." + "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", @@ -608,7 +608,7 @@ }, { "id": "p1-08", - "title": "Victory/defeat screen content \u2014 recap, banner, replay seed", + "title": "Victory/defeat screen content β€” recap, banner, replay seed", "priority": "p1", "status": "done", "scope": "game1", @@ -619,18 +619,18 @@ }, { "id": "p1-09", - "title": "Determinism gate \u2014 same seed produces byte-identical runs", + "title": "Determinism gate β€” same seed produces byte-identical runs", "priority": "p1", "status": "done", "scope": "game1", "owner": "testwright", "updated_at": "2026-04-19", "blocked_by": [], - "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\u2192BTreeMap 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, now running green in CI.\n\n**State as of 2026-04-17 PM**: the Rust side of the gate is running. CI enforces `cargo test --workspace` on every push to main (Stage 1 of `.forgejo/workflows/ci.yml`), the apricot runner is registered + polling, and T1's determinism vector is green. Two remaining blockers, both tractable:\n\n1. **HashMap iteration audit** is no longer abstract. The T2 serde round-trip test (`mc-turn/tests/serde_roundtrip.rs`) concretely demonstrated that `PlayerState.strategic_axes: HashMap<_>` and `TechState.progress: HashMap<_>` produce non-deterministic save output across processes. Fix is `HashMap \u2192 BTreeMap` in `mc-turn/src/game_state.rs`, scoped to p0-12 (shipwright). The 3 currently-`#[ignore]`'d T2 tests will flip to passing the moment that change lands.\n2. **GUT save/replay test + end-to-end byte-identical turn_stats diff** are still both missing. The autoplay smoke stage is advisory right now because `turn_stats.jsonl` isn't landing reliably on fresh flatpak checkouts \u2014 fixing the sandbox path handling in `tools/autoplay-batch.sh` unblocks the turn_stats equality check." + "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, now running green in CI.\n\n**State as of 2026-04-17 PM**: the Rust side of the gate is running. CI enforces `cargo test --workspace` on every push to main (Stage 1 of `.forgejo/workflows/ci.yml`), the apricot runner is registered + polling, and T1's determinism vector is green. Two remaining blockers, both tractable:\n\n1. **HashMap iteration audit** is no longer abstract. The T2 serde round-trip test (`mc-turn/tests/serde_roundtrip.rs`) concretely demonstrated that `PlayerState.strategic_axes: HashMap<_>` and `TechState.progress: HashMap<_>` produce non-deterministic save output across processes. Fix is `HashMap β†’ BTreeMap` in `mc-turn/src/game_state.rs`, scoped to p0-12 (shipwright). The 3 currently-`#[ignore]`'d T2 tests will flip to passing the moment that change lands.\n2. **GUT save/replay test + end-to-end byte-identical turn_stats diff** are still both missing. The autoplay smoke stage is advisory right now because `turn_stats.jsonl` isn't landing reliably on fresh flatpak checkouts β€” fixing the sandbox path handling in `tools/autoplay-batch.sh` unblocks the turn_stats equality check." }, { "id": "p1-10", - "title": "Game setup UX \u2014 new-game dialog, difficulty, clan preview", + "title": "Game setup UX β€” new-game dialog, difficulty, clan preview", "priority": "p1", "status": "done", "scope": "game1", @@ -641,14 +641,14 @@ }, { "id": "p1-11", - "title": "Purge build output from src/ \u2014 wasm-pack moves to .local/build/wasm/", + "title": "Purge build output from src/ β€” wasm-pack moves to .local/build/wasm/", "priority": "p1", "status": "done", "scope": "game1", "owner": "tourguide", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "`src/` is source-only by project rule \u2014 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' \u2192 ../../../../src/simulator/pkg/magic_civ_physics.js`)\nis the blocking reason `./run guide` cannot boot on plum today \u2014\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/`." + "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", @@ -670,7 +670,7 @@ "owner": "tourguide", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "`./run guide` on plum (`natalie@plum.lan`, macOS) today fails before\nfirst paint because `@magic-civ/physics-rs` resolves to the missing\n`src/simulator/pkg/magic_civ_physics.js` \u2014 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 \u2014 `@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." + "summary": "`./run guide` on plum (`natalie@plum.lan`, 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": "p1-14", @@ -681,7 +681,7 @@ "owner": "", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Per CLAUDE.md's hard Game 1 scope rule (Dwarves only, NO magic; leylines\n/ Green school / spacefaring \u2192 Game 2; Archons / Ascension / 5 magic\nschools \u2192 Game 3), no magic content may ship into the Game 1 guide. The\n2026-04-17 `p2-09` scope-narrow pass deleted 10 Game 2/3 pages. But a\nprophylactic `Explore` audit on the same day \u2014 triggered by the\nTourguide route-coverage spec catching magic-data imports in Game 1\npages that still rendered \u2014 surfaced **2 RED** and **6 YELLOW** residual\nleaks that survived the first purge:\n\n**RED (runtime crash risk if data drifts)**\n\n- `public/games/age-of-dwarves/guide/src/pages/DevSpritesPage.tsx:5` \u2014\n live import of `@resources/magic/schools.json`. (Partial-fix: the\n p1-13 Wave-1 pass replaced the import with the shared `SCHOOL_COLORS`\n palette, so the crash is gone; keeping this row as evidence of why\n the objective exists.)\n- `src/packages/guide/src/components/climate-sim/HexGLRenderer.tsx:7,749,783`\n \u2014 live ley-line network rendering via `LEY_COLORS[edge.school]`. In\n Game 1 the climate simulator's rendered grid shows ley-edge meshes\n with school-coded colors, inserting Game 2 visual vocabulary into\n every Game 1 planet view.\n\n**YELLOW (renders stale Game 2+ content into Game 1 UI, inert but wrong)**\n\n- `public/games/age-of-dwarves/guide/src/data/game.ts:87\u201398` \u2014\n `infusionTrees` loader pulls magical_promotions JSON into the guide\n context; exported to every consumer via `data/index.ts:40`.\n- `public/games/age-of-dwarves/guide/src/app/guide-data.ts:48` \u2014 the\n data context injection re-exposes magic-school data to every page\n via `GuideDataProvider`.\n- `public/games/age-of-dwarves/guide/src/pages/HomePage.tsx:220\u2013224`\n \u2014 nav items `/magic/schools`, `/magic/spells`, `/magic/archons`\n still defined; the routes were purged, so clicking these in Game 1\n hits the `` fallback. Broken-link UX.\n- `public/games/age-of-dwarves/guide/src/pages/LensesPage.tsx:18` \u2014\n `formatUnlock` renders magic-school unlock strings for Game 1 lenses;\n if a Game 1 lens has a magic unlock in data, the formatter renders it.\n- `public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx:23,143,152`\n \u2014 `ManaUpkeep` interface + render.\n- `src/packages/guide/src/components/cards/TerrainCard.tsx:256\u2013264`\n \u2014 `mana_major` field render block.\n\nAdditional front-page prose (Game 1 HomePage) already displays \"5\nMagic Schools\" and \"16 Asymmetric Races\" in its feature grid \u2014\nevidence that the `` wrapping isn't\nbeing consulted by every page's content block." + "summary": "Per CLAUDE.md's hard Game 1 scope rule (Dwarves only, NO magic; leylines\n/ Green school / spacefaring β†’ Game 2; Archons / Ascension / 5 magic\nschools β†’ Game 3), no magic content may ship into the Game 1 guide. The\n2026-04-17 `p2-09` scope-narrow pass deleted 10 Game 2/3 pages. But a\nprophylactic `Explore` audit on the same day β€” triggered by the\nTourguide route-coverage spec catching magic-data imports in Game 1\npages that still rendered β€” surfaced **2 RED** and **6 YELLOW** residual\nleaks that survived the first purge:\n\n**RED (runtime crash risk if data drifts)**\n\n- `public/games/age-of-dwarves/guide/src/pages/DevSpritesPage.tsx:5` β€”\n live import of `@resources/magic/schools.json`. (Partial-fix: the\n p1-13 Wave-1 pass replaced the import with the shared `SCHOOL_COLORS`\n palette, so the crash is gone; keeping this row as evidence of why\n the objective exists.)\n- `src/packages/guide/src/components/climate-sim/HexGLRenderer.tsx:7,749,783`\n β€” live ley-line network rendering via `LEY_COLORS[edge.school]`. In\n Game 1 the climate simulator's rendered grid shows ley-edge meshes\n with school-coded colors, inserting Game 2 visual vocabulary into\n every Game 1 planet view.\n\n**YELLOW (renders stale Game 2+ content into Game 1 UI, inert but wrong)**\n\n- `public/games/age-of-dwarves/guide/src/data/game.ts:87–98` β€”\n `infusionTrees` loader pulls magical_promotions JSON into the guide\n context; exported to every consumer via `data/index.ts:40`.\n- `public/games/age-of-dwarves/guide/src/app/guide-data.ts:48` β€” the\n data context injection re-exposes magic-school data to every page\n via `GuideDataProvider`.\n- `public/games/age-of-dwarves/guide/src/pages/HomePage.tsx:220–224`\n β€” nav items `/magic/schools`, `/magic/spells`, `/magic/archons`\n still defined; the routes were purged, so clicking these in Game 1\n hits the `` fallback. Broken-link UX.\n- `public/games/age-of-dwarves/guide/src/pages/LensesPage.tsx:18` β€”\n `formatUnlock` renders magic-school unlock strings for Game 1 lenses;\n if a Game 1 lens has a magic unlock in data, the formatter renders it.\n- `public/games/age-of-dwarves/guide/src/pages/SurvivalGuidePage.tsx:23,143,152`\n β€” `ManaUpkeep` interface + render.\n- `src/packages/guide/src/components/cards/TerrainCard.tsx:256–264`\n β€” `mana_major` field render block.\n\nAdditional front-page prose (Game 1 HomePage) already displays \"5\nMagic Schools\" and \"16 Asymmetric Races\" in its feature grid β€”\nevidence that the `` wrapping isn't\nbeing consulted by every page's content block." }, { "id": "p1-15", @@ -703,7 +703,7 @@ "owner": "tourguide", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "CLAUDE.md's Game 1 scope rule is clear: Age of Dwarves Early Access\nships with **no magic** \u2014 no magic schools, no Archons, no leylines,\nno mana. The `` component + `VITE_DEV_GUIDE=1`\ndev-bundle flag (p1-15) are the enforcement mechanism. But a 2026-04-18\nExplore sweep catalogued six user-visible surfaces in the default Game 1\nbuild that still advertise or document Game 2/3 cosmology:\n\n| File:Line | Failure |\n|---|---|\n| `HomePage.tsx:189\u2013190` (FEATURES) | \"5 Magic Schools\" card with \"cross-school fusions, 10 hybrid disciplines\" |\n| `HomePage.tsx:246\u2013251` (Pitch) | \"16 asymmetric races, 5 magic schools \u2026 pursue arcane power\" |\n| `HomePage.tsx:256\u2013275` (LoreSection) | Two paragraphs of mana-nodes + ley-lines + school-aligned energy |\n| `CommunicationsPage.tsx:97\u201398` | \"Archon Telepathy (Magic civs)\" row in mundane radio-tower rules |\n| `PromotionsPage.tsx:5,12\u201315,156\u2013193` | Magic-data imports + \"Mana Infusions\" section + \"Dispellable by Aether\" / \"High Archon dies\" body text |\n| `SurvivalGuidePage` data.ts:85 | \"Life T3 quarantine spell blocks adjacency transmission\" in mundane survival scenario |\n\nThese are **RED** \u2014 visible to someone opening the staging / production\nbuild without `VITE_DEV_GUIDE=1`. A player launching Early Access with\none dwarf race should not be told the game has \"5 magic schools\" or\nread Archon mechanics in the communications tower rules.\n\nYELLOW items (dev-bundle-only, flagged for opportunistic cleanup):\n\n- `progress-report/OverviewTab.tsx:137,183` \u2014 hardcoded \"5 trees,\n 30 policies\" + \"12 additional races\" roadmap rows\n- `GovernmentPage.tsx:48` (SKIP_MODS) \u2014 defensive filter for the\n Game-2 `no_spell_pact_opposing_school` modifier" + "summary": "CLAUDE.md's Game 1 scope rule is clear: Age of Dwarves Early Access\nships with **no magic** β€” no magic schools, no Archons, no leylines,\nno mana. The `` component + `VITE_DEV_GUIDE=1`\ndev-bundle flag (p1-15) are the enforcement mechanism. But a 2026-04-18\nExplore sweep catalogued six user-visible surfaces in the default Game 1\nbuild that still advertise or document Game 2/3 cosmology:\n\n| File:Line | Failure |\n|---|---|\n| `HomePage.tsx:189–190` (FEATURES) | \"5 Magic Schools\" card with \"cross-school fusions, 10 hybrid disciplines\" |\n| `HomePage.tsx:246–251` (Pitch) | \"16 asymmetric races, 5 magic schools … pursue arcane power\" |\n| `HomePage.tsx:256–275` (LoreSection) | Two paragraphs of mana-nodes + ley-lines + school-aligned energy |\n| `CommunicationsPage.tsx:97–98` | \"Archon Telepathy (Magic civs)\" row in mundane radio-tower rules |\n| `PromotionsPage.tsx:5,12–15,156–193` | Magic-data imports + \"Mana Infusions\" section + \"Dispellable by Aether\" / \"High Archon dies\" body text |\n| `SurvivalGuidePage` data.ts:85 | \"Life T3 quarantine spell blocks adjacency transmission\" in mundane survival scenario |\n\nThese are **RED** β€” visible to someone opening the staging / production\nbuild without `VITE_DEV_GUIDE=1`. A player launching Early Access with\none dwarf race should not be told the game has \"5 magic schools\" or\nread Archon mechanics in the communications tower rules.\n\nYELLOW items (dev-bundle-only, flagged for opportunistic cleanup):\n\n- `progress-report/OverviewTab.tsx:137,183` β€” hardcoded \"5 trees,\n 30 policies\" + \"12 additional races\" roadmap rows\n- `GovernmentPage.tsx:48` (SKIP_MODS) β€” defensive filter for the\n Game-2 `no_spell_pact_opposing_school` modifier" }, { "id": "p1-17", @@ -718,7 +718,7 @@ }, { "id": "p1-18", - "title": "Village discovery \u2014 world-map feedback (notification, reward popup, minimap ping)", + "title": "Village discovery β€” world-map feedback (notification, reward popup, minimap ping)", "priority": "p1", "status": "done", "scope": "game1", @@ -729,40 +729,40 @@ }, { "id": "p1-19", - "title": "Tutorial opt-in \u2014 HUD button, disappears after turn 5, starts from Step 1", + "title": "Tutorial opt-in β€” HUD button, disappears after turn 5, starts from Step 1", "priority": "p1", "status": "done", "scope": "game1", "owner": "wireguard", "updated_at": "2026-04-19", "blocked_by": [], - "summary": "The first-run tutorial currently auto-shows on game start (gated by\n`TutorialOverlay.should_show_on_first_run()`). This is hostile to returning\nplayers and to playtesting \u2014 the tutorial interrupts real gameplay every fresh\nboot. Assume the player doesn't need a tutorial by default. Offer it as a\nbutton on the world-map HUD that disappears after turn 5.\n\nAdditionally, when the tutorial IS started, it begins at **Step 1** (camera\npan), not Step 2. This is already the authoritative step order in\n`tutorial_overlay.gd:_STEPS` \u2014 the fix is only to make sure `_current_step`\ninitializes to `1` (it does) and that no code skips ahead." + "summary": "The first-run tutorial currently auto-shows on game start (gated by\n`TutorialOverlay.should_show_on_first_run()`). This is hostile to returning\nplayers and to playtesting β€” the tutorial interrupts real gameplay every fresh\nboot. Assume the player doesn't need a tutorial by default. Offer it as a\nbutton on the world-map HUD that disappears after turn 5.\n\nAdditionally, when the tutorial IS started, it begins at **Step 1** (camera\npan), not Step 2. This is already the authoritative step order in\n`tutorial_overlay.gd:_STEPS` β€” the fix is only to make sure `_current_step`\ninitializes to `1` (it does) and that no code skips ahead." }, { "id": "p1-20", - "title": "Unit action capability registry \u2014 one source of truth for \"what can this unit do right now?\"", + "title": "Unit action capability registry β€” one source of truth for \"what can this unit do right now?\"", "priority": "p1", "status": "done", "scope": "game1", "owner": "wireguard", "updated_at": "2026-04-19", "blocked_by": [], - "summary": "The game has no unified answer to *\"what actions can unit U take on turn T in\nstate S?\"* Today the unit panel (`unit_panel.gd:19-40`) hardcodes three\nbuttons \u2014 Fortify, Skip, Found City \u2014 and decides visibility with bespoke\nper-unit booleans scattered across the JSON (`can_found_city`,\n`can_build_improvements`, `flags: [\"ranged\"]`) and ad-hoc GDScript predicates\n(`is_civilian()`). Meanwhile `mc-ai/src/tactical/movement.rs` enumerates\nmoves and attacks but has no registry for non-motion actions. UI and AI have\nno shared truth.\n\nEvery future action \u2014 patrol (p1-21), siege pack/deploy, pillage, embark,\nbuild-road, heal, upgrade \u2014 compounds that debt by adding another hardcoded\nbutton plus its own scattered check. A siege engine in `packed` state can\nmove but not bombard; in `deployed` state can bombard but not move. Patrol\nhas the same shape (idle \u2194 patrolling, with auto-cancel). Fortify has the\nsame shape. Without a registry, each state gate becomes a new bespoke flag.\n\nThis objective lands the foundation: a JSON-driven capability declaration,\na Rust `ActionKind` enum with a single `legal_actions(unit, state)` query,\nand a unit-panel refactor that renders buttons from that list. **Behavior\ndoes not change** \u2014 the three existing actions are folded in with no\nsemantic change. The payoff is every subsequent action objective (patrol,\nsiege, pillage, embark, ...) ships as one enum variant + one JSON keyword\nmapping + one handler, with no UI or AI scaffolding to re-invent." + "summary": "The game has no unified answer to *\"what actions can unit U take on turn T in\nstate S?\"* Today the unit panel (`unit_panel.gd:19-40`) hardcodes three\nbuttons β€” Fortify, Skip, Found City β€” and decides visibility with bespoke\nper-unit booleans scattered across the JSON (`can_found_city`,\n`can_build_improvements`, `flags: [\"ranged\"]`) and ad-hoc GDScript predicates\n(`is_civilian()`). Meanwhile `mc-ai/src/tactical/movement.rs` enumerates\nmoves and attacks but has no registry for non-motion actions. UI and AI have\nno shared truth.\n\nEvery future action β€” patrol (p1-21), siege pack/deploy, pillage, embark,\nbuild-road, heal, upgrade β€” compounds that debt by adding another hardcoded\nbutton plus its own scattered check. A siege engine in `packed` state can\nmove but not bombard; in `deployed` state can bombard but not move. Patrol\nhas the same shape (idle ↔ patrolling, with auto-cancel). Fortify has the\nsame shape. Without a registry, each state gate becomes a new bespoke flag.\n\nThis objective lands the foundation: a JSON-driven capability declaration,\na Rust `ActionKind` enum with a single `legal_actions(unit, state)` query,\nand a unit-panel refactor that renders buttons from that list. **Behavior\ndoes not change** β€” the three existing actions are folded in with no\nsemantic change. The payoff is every subsequent action objective (patrol,\nsiege, pillage, embark, ...) ships as one enum variant + one JSON keyword\nmapping + one handler, with no UI or AI scaffolding to re-invent." }, { "id": "p1-21", - "title": "Unit patrol orders \u2014 standing order to loop between waypoint tiles", + "title": "Unit patrol orders β€” standing order to loop between waypoint tiles", "priority": "p1", "status": "done", "scope": "game1", "owner": "wireguard", "updated_at": "2026-04-19", "blocked_by": [], - "summary": "Both the human player and the AI clans need a *standing order* that keeps a\nunit moving along a fixed route turn after turn without per-turn micro-\nmanagement. Canonical use cases: escorting a worker loop, covering a\nchokepoint, sweeping scout fog between two outposts.\n\nToday a unit has two durable states: idle-on-tile, or fortified. `Skip`\nends the turn but does not persist. A player who wants a scout to pace\nbetween two tiles must hand-move it every single turn \u2014 which breaks down\nonce the empire has more than a few units, and which the AI cannot express\nat all because `mc-ai/tactical/movement.rs` re-plans from scratch each turn.\n\nThis objective adds a third durable state \u2014 **patrol** \u2014 with a small\nwaypoint list, a direction cursor, and a loop mode. While patrolling, the\nunit auto-advances along its route during the turn processor before the\nplayer's input phase, so turn N+1 opens with the unit already at the next\nstep on its loop.\n\n**This objective assumes p1-20 (unit action capability registry) has\nshipped.** Patrol plugs into the registry as one new `ActionKind` variant\nplus its handlers \u2014 no bespoke unit-panel buttons, no scattered\n`is_patrolling` checks in GDScript. If p1-20 slips, reassess whether to\nland a narrower patrol-only version first." + "summary": "Both the human player and the AI clans need a *standing order* that keeps a\nunit moving along a fixed route turn after turn without per-turn micro-\nmanagement. Canonical use cases: escorting a worker loop, covering a\nchokepoint, sweeping scout fog between two outposts.\n\nToday a unit has two durable states: idle-on-tile, or fortified. `Skip`\nends the turn but does not persist. A player who wants a scout to pace\nbetween two tiles must hand-move it every single turn β€” which breaks down\nonce the empire has more than a few units, and which the AI cannot express\nat all because `mc-ai/tactical/movement.rs` re-plans from scratch each turn.\n\nThis objective adds a third durable state β€” **patrol** β€” with a small\nwaypoint list, a direction cursor, and a loop mode. While patrolling, the\nunit auto-advances along its route during the turn processor before the\nplayer's input phase, so turn N+1 opens with the unit already at the next\nstep on its loop.\n\n**This objective assumes p1-20 (unit action capability registry) has\nshipped.** Patrol plugs into the registry as one new `ActionKind` variant\nplus its handlers β€” no bespoke unit-panel buttons, no scattered\n`is_patrolling` checks in GDScript. If p1-20 slips, reassess whether to\nland a narrower patrol-only version first." }, { "id": "p1-22", - "title": "MCTS per-decision wall-clock budget \u2014 bound per-turn cost on huge maps", + "title": "MCTS per-decision wall-clock budget β€” bound per-turn cost on huge maps", "priority": "p1", "status": "done", "scope": "game1", @@ -773,7 +773,7 @@ }, { "id": "p1-22a", - "title": "Huge-map AI quality \u2014 close the 4/10 \u2192 \u22655/10 decisive-game gate", + "title": "Huge-map AI quality β€” close the 4/10 β†’ β‰₯5/10 decisive-game gate", "priority": "p1", "status": "done", "scope": "game1", @@ -782,29 +782,29 @@ "blocked_by": [ "p1-22" ], - "summary": "The huge-map 5-clan batch (`tools/huge-map-5clan.sh`, 10 seeds, T300 limit,\n`MCTS_DECISION_BUDGET_MS=2000`) has landed at **4/10 victories** across three\nindependent runs (cycle-1 pre-budget, cycle-2 post-tactical-budget, cycle-3\npost-p0-20 2\u00d7 GPU rollout speed). The gate is \u22655/10.\n\nPost-p0-20 evidence eliminates budget plumbing as the bottleneck: with\n`budget_ms=50` the budget test fires at `dispatched=2623 << 100000`\n(1/38 of the iteration cap), and GPU rollouts are 2\u00d7 faster than CPU. Yet the\nratio did not move from 4/10. This is **AI strategic quality on huge maps**,\nnot throughput." + "summary": "The huge-map 5-clan batch (`tools/huge-map-5clan.sh`, 10 seeds, T300 limit,\n`MCTS_DECISION_BUDGET_MS=2000`) has landed at **4/10 victories** across three\nindependent runs (cycle-1 pre-budget, cycle-2 post-tactical-budget, cycle-3\npost-p0-20 2Γ— GPU rollout speed). The gate is β‰₯5/10.\n\nPost-p0-20 evidence eliminates budget plumbing as the bottleneck: with\n`budget_ms=50` the budget test fires at `dispatched=2623 << 100000`\n(1/38 of the iteration cap), and GPU rollouts are 2Γ— faster than CPU. Yet the\nratio did not move from 4/10. This is **AI strategic quality on huge maps**,\nnot throughput." }, { "id": "p1-23", - "title": "Restore StatsTracker \u2014 demographics overview broken in shipped builds", + "title": "Restore StatsTracker β€” demographics overview broken in shipped builds", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "`engine/scenes/overviews/demographics.gd` (and `end_game_stats.gd`) referenced `StatsTracker.CATEGORIES`, `CATEGORY_LABELS`, `get_rankings`, `get_history`, `get_player_series` but no `StatsTracker` class_name or autoload existed. Surfaced 2026-04-25 in `p2-06-verify-20260425` export logs as 4\u00d7 `SCRIPT ERROR: Identifier \"StatsTracker\" not declared`. The demographics screen was shipped broken.\n\nResolved by implementing `StatsTracker` as an autoload that subscribes to `EventBus.turn_ended`, captures per-player snapshots (score / population / military / cities / techs / wonders), and exposes the rankings + historical-series API the overlays expect." + "summary": "`engine/scenes/overviews/demographics.gd` (and `end_game_stats.gd`) referenced `StatsTracker.CATEGORIES`, `CATEGORY_LABELS`, `get_rankings`, `get_history`, `get_player_series` but no `StatsTracker` class_name or autoload existed. Surfaced 2026-04-25 in `p2-06-verify-20260425` export logs as 4Γ— `SCRIPT ERROR: Identifier \"StatsTracker\" not declared`. The demographics screen was shipped broken.\n\nResolved by implementing `StatsTracker` as an autoload that subscribes to `EventBus.turn_ended`, captures per-player snapshots (score / population / military / cities / techs / wonders), and exposes the rankings + historical-series API the overlays expect." }, { "id": "p1-24", - "title": "ai_personalities.json fails to load from packed builds (all platforms) \u2014 pass JSON contents not path", + "title": "ai_personalities.json fails to load from packed builds (all platforms) β€” pass JSON contents not path", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "When the Windows .exe cross-compiled via cargo-xwin runs under Wine on apricot (p2-06b smoke 2026-04-25), it floods the log with:\n\n```\nERROR: GdMcTreeController::scoring_weights_for_clan load error for 'goldvein':\n failed to read ai_personalities.json at public/games/age-of-dwarves/data\\ai_personalities.json:\n Path not found. (os error 3)\n```\n\nThe mixed-separator path is one symptom; the deeper issue is that `ai_turn_bridge.gd:79` passes `ProjectSettings.globalize_path(\"res://public/games/age-of-dwarves/data\")` to the Rust side, then Rust uses `std::fs::read_to_string` on `/ai_personalities.json`. **For packed builds (any platform), `res://` content lives inside the .pck \u2014 `globalize_path` returns a fake/non-existent OS path, so `std::fs::read_to_string` always fails.** It silently fell back to default weights everywhere \u2014 the macOS smoke (`p0_pop_peak ~290 turns to victory`) likely had the same error invisibly.\n\n**Game still completes** \u2014 the AI falls back to default scoring weights. So this is non-blocking but ships a degraded MCTS-personality-aware AI on EVERY platform from packed builds, not just Windows. The wine smoke just made the error finally visible." + "summary": "When the Windows .exe cross-compiled via cargo-xwin runs under Wine on apricot (p2-06b smoke 2026-04-25), it floods the log with:\n\n```\nERROR: GdMcTreeController::scoring_weights_for_clan load error for 'goldvein':\n failed to read ai_personalities.json at public/games/age-of-dwarves/data\\ai_personalities.json:\n Path not found. (os error 3)\n```\n\nThe mixed-separator path is one symptom; the deeper issue is that `ai_turn_bridge.gd:79` passes `ProjectSettings.globalize_path(\"res://public/games/age-of-dwarves/data\")` to the Rust side, then Rust uses `std::fs::read_to_string` on `/ai_personalities.json`. **For packed builds (any platform), `res://` content lives inside the .pck β€” `globalize_path` returns a fake/non-existent OS path, so `std::fs::read_to_string` always fails.** It silently fell back to default weights everywhere β€” the macOS smoke (`p0_pop_peak ~290 turns to victory`) likely had the same error invisibly.\n\n**Game still completes** β€” the AI falls back to default scoring weights. So this is non-blocking but ships a degraded MCTS-personality-aware AI on EVERY platform from packed builds, not just Windows. The wine smoke just made the error finally visible." }, { "id": "p1-25", @@ -815,18 +815,18 @@ "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "Every Linux/Windows export log was emitting two families of parse errors despite producing a working binary:\n\n1. **`Class \"SaveManager\" hides a global script class.`** \u2014 apricot had a stray duplicate `src/game/engine/src/map/save_manager.gd` (byte-identical to `src/core/save_manager.gd`, both declaring `class_name SaveManager`). The Mac source tree was clean; the apricot stray must have come from a prior agent's misplaced rsync. Deleted on apricot 2026-04-25; export logs now register SaveManager exactly once.\n\n2. **`The member \"type_id\" / \"hp\" / \"max_hp\" / \"movement_remaining\" / \"position\" / \"equipped_items\" already exists in parent class Unit.`** \u2014 `engine/scenes/tests/crafting_complete_proof.gd` had a `class UnitStub: extends Unit` block that redeclared 6 fields the parent `Unit` class already owns. Comment in the file claimed \"Unit.gd is a 2-line stub\" but Unit.gd evolved past that point. Refactored UnitStub to set those values in `_init()` instead of redeclaring them.\n\nBoth error families surfaced in **every** export run since shipwright started exporting builds; not a regression introduced today, just never triaged because the export still produced a working binary." + "summary": "Every Linux/Windows export log was emitting two families of parse errors despite producing a working binary:\n\n1. **`Class \"SaveManager\" hides a global script class.`** β€” apricot had a stray duplicate `src/game/engine/src/map/save_manager.gd` (byte-identical to `src/core/save_manager.gd`, both declaring `class_name SaveManager`). The Mac source tree was clean; the apricot stray must have come from a prior agent's misplaced rsync. Deleted on apricot 2026-04-25; export logs now register SaveManager exactly once.\n\n2. **`The member \"type_id\" / \"hp\" / \"max_hp\" / \"movement_remaining\" / \"position\" / \"equipped_items\" already exists in parent class Unit.`** β€” `engine/scenes/tests/crafting_complete_proof.gd` had a `class UnitStub: extends Unit` block that redeclared 6 fields the parent `Unit` class already owns. Comment in the file claimed \"Unit.gd is a 2-line stub\" but Unit.gd evolved past that point. Refactored UnitStub to set those values in `_init()` instead of redeclaring them.\n\nBoth error families surfaced in **every** export run since shipwright started exporting builds; not a regression introduced today, just never triaged because the export still produced a working binary." }, { "id": "p1-26", - "title": "Tile-placement UX with effect preview \u2014 Civ7-style \\\\\\\"where does this go and what changes\\\\\\\"", + "title": "Tile-placement UX with effect preview β€” Civ7-style \\\\\\\"where does this go and what changes\\\\\\\"", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "When a player queues a building or tile improvement today, the placement is a black box:\n- **Buildings**: every building entry in `public/games/age-of-dwarves/data/buildings/*.json` has `placement: \"city\"` \u2014 buildings just appear in the city center, no tile choice, no spatial decision.\n- **Improvements**: a worker drops a farm/mine/road at the worker's hex; the player sees no preview of yield-delta or adjacency effects before committing.\n\nCiv7 (and Civ6 districts) made this a primary expressive lever: pick a tile, see live yield projections + adjacency bonuses + terrain restrictions before locking in. Without this surface in Game 1 the player loses a major strategic dimension and the city map feels like decoration.\n\nThis objective covers the **UX + supporting data extension** for tile-targeted building/improvement placement with live preview. The simulation already supports per-tile improvements; the gap is presentation + a small data extension to mark which buildings become tile-placed and any adjacency rules." + "summary": "When a player queues a building or tile improvement today, the placement is a black box:\n- **Buildings**: every building entry in `public/games/age-of-dwarves/data/buildings/*.json` has `placement: \"city\"` β€” buildings just appear in the city center, no tile choice, no spatial decision.\n- **Improvements**: a worker drops a farm/mine/road at the worker's hex; the player sees no preview of yield-delta or adjacency effects before committing.\n\nCiv7 (and Civ6 districts) made this a primary expressive lever: pick a tile, see live yield projections + adjacency bonuses + terrain restrictions before locking in. Without this surface in Game 1 the player loses a major strategic dimension and the city map feels like decoration.\n\nThis objective covers the **UX + supporting data extension** for tile-targeted building/improvement placement with live preview. The simulation already supports per-tile improvements; the gap is presentation + a small data extension to mark which buildings become tile-placed and any adjacency rules." }, { "id": "p1-27", @@ -837,7 +837,7 @@ "owner": "warcouncil", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "Today the GPU MCTS path lives **inside** the `mc-ai` crate (`gpu/inner.rs`, `gpu/rollout.wgsl`, `gpu/cpu_reference.rs`) and runs in-process via the GDExtension (`GdMcTreeController`). That couples GPU lifecycle (device init, queue submission, buffer pooling, fence waits) to the game's per-turn decision call.\n\nPer user directive 2026-04-25: extract this into its own **MCTS service/client** that\n\n1. Lives **inside @magic-civilization** (not in @model-boss / not in any other repo) \u2014 it's game-specific.\n2. Lives **independently** of the in-process GDExtension \u2014 long-lived process the game talks to via IPC (Unix socket / TCP / shared memory).\n3. **Borrows patterns** from `@model-boss` (job submission, queue, batched dispatch, GPU lifecycle isolation) but doesn't take a dependency on it. Magic-civ's MCTS workload is narrow enough to warrant its own focused implementation.\n\nWhy a service vs in-process:\n- GPU init + warm-up amortized once per session, not per AI turn\n- Game can keep playing turns while a deep search is in flight (async)\n- Crash isolation \u2014 a wgpu/driver fault doesn't take the game down\n- One service can serve multiple game clients (autoplay-batch parallel runs hit one warm GPU instead of N cold inits)\n- Future: out-of-process service can run on a different host (apricot has GPU, dev mac doesn't)" + "summary": "Today the GPU MCTS path lives **inside** the `mc-ai` crate (`gpu/inner.rs`, `gpu/rollout.wgsl`, `gpu/cpu_reference.rs`) and runs in-process via the GDExtension (`GdMcTreeController`). That couples GPU lifecycle (device init, queue submission, buffer pooling, fence waits) to the game's per-turn decision call.\n\nPer user directive 2026-04-25: extract this into its own **MCTS service/client** that\n\n1. Lives **inside @magic-civilization** (not in @model-boss / not in any other repo) β€” it's game-specific.\n2. Lives **independently** of the in-process GDExtension β€” long-lived process the game talks to via IPC (Unix socket / TCP / shared memory).\n3. **Borrows patterns** from `@model-boss` (job submission, queue, batched dispatch, GPU lifecycle isolation) but doesn't take a dependency on it. Magic-civ's MCTS workload is narrow enough to warrant its own focused implementation.\n\nWhy a service vs in-process:\n- GPU init + warm-up amortized once per session, not per AI turn\n- Game can keep playing turns while a deep search is in flight (async)\n- Crash isolation β€” a wgpu/driver fault doesn't take the game down\n- One service can serve multiple game clients (autoplay-batch parallel runs hit one warm GPU instead of N cold inits)\n- Future: out-of-process service can run on a different host (apricot has GPU, dev mac doesn't)" }, { "id": "p1-27a", @@ -848,18 +848,18 @@ "owner": "warcouncil", "updated_at": "2026-05-16", "blocked_by": [], - "summary": "Split out of `p1-27` (architectural extraction, closed `done` 2026-05-14). The\nservice crate ships, the client/server protocol ships, the gdext fallback path\nships, and `tools/run-services.sh` manages lifecycle. What did NOT ship under\np1-27, and is tracked here:\n\n1. Per-job latency + queue-depth telemetry to JSONL.\n2. `gpu_rollout_parity.rs` driven against the service path (byte-identical assert).\n3. Wiring `huge-map-5clan.sh` to bring the warm service up so we can measure the\n p1-22 wall-clock improvement.\n\nThe parent objective's \"Remaining work (2026-05-03)\" section (bullets 88\u2013127)\nis the authored design for this sibling \u2014 see `p1-27.md` for the full per-file\nplan, acceptance gates, and SOLID/DRY rails. Re-stated here in brief:" + "summary": "Split out of `p1-27` (architectural extraction, closed `done` 2026-05-14). The\nservice crate ships, the client/server protocol ships, the gdext fallback path\nships, and `tools/run-services.sh` manages lifecycle. What did NOT ship under\np1-27, and is tracked here:\n\n1. Per-job latency + queue-depth telemetry to JSONL.\n2. `gpu_rollout_parity.rs` driven against the service path (byte-identical assert).\n3. Wiring `huge-map-5clan.sh` to bring the warm service up so we can measure the\n p1-22 wall-clock improvement.\n\nThe parent objective's \"Remaining work (2026-05-03)\" section (bullets 88–127)\nis the authored design for this sibling β€” see `p1-27.md` for the full per-file\nplan, acceptance gates, and SOLID/DRY rails. Re-stated here in brief:" }, { "id": "p1-28", - "title": "Culture research tree \u2014 real graph, bridge, UI", + "title": "Culture research tree β€” real graph, bridge, UI", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "The culture data files\n(`public/games/age-of-dwarves/data/culture/manifest.json` \u2192\n`public/resources/culture/*.json`) describe a six-pillar culture tree that\nmirrors the tech-tree shape: `id`, `name`, `pillar`, `era`, `tier`, `cost`,\n`requires`, `unlocks{\u2026}`, `flavor`. The web guide already renders it via the\nshared `TechTreeGraph` component (`CultureTreePage.tsx`).\n\nInside the Godot game there was no culture-tree surface: no Rust research\ngraph, no GDExtension bridge, no GDScript wrapper, no scene, no per-turn\nresearch accumulator. The `CulturePool` only powered border expansion.\n\nThis objective shipped the live culture-research path end-to-end so the\nplayer can open a culture-tree screen identical in UX to the tech tree,\npick a tradition, accumulate culture-research progress per turn, and unlock\nbuildings / wonders / lenses / mechanics on completion." + "summary": "The culture data files\n(`public/games/age-of-dwarves/data/culture/manifest.json` β†’\n`public/resources/culture/*.json`) describe a six-pillar culture tree that\nmirrors the tech-tree shape: `id`, `name`, `pillar`, `era`, `tier`, `cost`,\n`requires`, `unlocks{…}`, `flavor`. The web guide already renders it via the\nshared `TechTreeGraph` component (`CultureTreePage.tsx`).\n\nInside the Godot game there was no culture-tree surface: no Rust research\ngraph, no GDExtension bridge, no GDScript wrapper, no scene, no per-turn\nresearch accumulator. The `CulturePool` only powered border expansion.\n\nThis objective shipped the live culture-research path end-to-end so the\nplayer can open a culture-tree screen identical in UX to the tech tree,\npick a tradition, accumulate culture-research progress per turn, and unlock\nbuildings / wonders / lenses / mechanics on completion." }, { "id": "p1-29", @@ -870,44 +870,44 @@ "owner": "combat-dev", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "Split out from p0-01's original v1 sub-gates that the AI-layer cycles (1, 2, 3) could not move because they measure emergent game-balance dynamics, not AI quality. p0-01 closed `done` 2026-04-26 against Gate v2 (3/5 v1 sub-gates pass cleanly: tier_peak=4, wonders 7/10, combats=255). The 2 v1 sub-gates that v2 reframed away need a real owner:\n\n- `tier_peak_gap \u2264 4` median: in surviving-pair games at end, one player tech-monopolizes (tp=6) while the other stagnates (tp=0), giving gap=5-6. Even with the alive-aware metric, the gap holds. Root cause: capture/combat dynamics let one player snowball without the other catching up. Loser stays alive but undeveloped.\n- `peak_unit_tier \u2265 3 in \u22657/10 games` absolute: 5/10 currently. 4 of the 5 fails are early-domination games (T48-T121) where tier-3 tech hasn't unlocked yet. The AI does deploy tier-3 units when available (80% of seeds reaching tp \u22653 also reach unit \u22653), but games end before tier-3 unlocks in half the seeds.\n\nCycle-3 attempted multiple AI-layer levers and confirmed they DON'T move these gates:\n- Tactical `DOMINANCE_FACTOR` bump (production.rs 1.25\u21922.0): no effect on outcome\n- Tactical dominance lerp bump (thresholds.rs 1.5\u21922.0/2.5 baseline): caused REGRESSION on tier_peak (faster opportunist wins)\n- Both reverted because the strategic MCTS doesn't pick attack actions \u2014 it only picks `SpawnUnit/FoundCity/Idle` per `mc-turn/src/snapshot.rs:204-214 action_prior`. The capture/development tempo is governed by mc-turn capture mechanics + mc-economy growth rates, NOT by AI scoring weights.\n\nReal levers (cross-team scope):\n- **mc-combat / mc-turn capture mechanics**: increase city HP, lengthen siege duration, add capital-recapture cost, weaken early-rush combat math.\n- **mc-economy growth rates**: faster baseline tech research, lower tier-3 prereq cost, give players tech catch-up bonus when behind.\n- **mc-turn turn-limit floor**: refuse to award domination victory before T150 (force games to mid-game minimum).\n\nPick one or compose multiple. Each requires the corresponding team-lead's involvement." + "summary": "Split out from p0-01's original v1 sub-gates that the AI-layer cycles (1, 2, 3) could not move because they measure emergent game-balance dynamics, not AI quality. p0-01 closed `done` 2026-04-26 against Gate v2 (3/5 v1 sub-gates pass cleanly: tier_peak=4, wonders 7/10, combats=255). The 2 v1 sub-gates that v2 reframed away need a real owner:\n\n- `tier_peak_gap ≀ 4` median: in surviving-pair games at end, one player tech-monopolizes (tp=6) while the other stagnates (tp=0), giving gap=5-6. Even with the alive-aware metric, the gap holds. Root cause: capture/combat dynamics let one player snowball without the other catching up. Loser stays alive but undeveloped.\n- `peak_unit_tier β‰₯ 3 in β‰₯7/10 games` absolute: 5/10 currently. 4 of the 5 fails are early-domination games (T48-T121) where tier-3 tech hasn't unlocked yet. The AI does deploy tier-3 units when available (80% of seeds reaching tp β‰₯3 also reach unit β‰₯3), but games end before tier-3 unlocks in half the seeds.\n\nCycle-3 attempted multiple AI-layer levers and confirmed they DON'T move these gates:\n- Tactical `DOMINANCE_FACTOR` bump (production.rs 1.25β†’2.0): no effect on outcome\n- Tactical dominance lerp bump (thresholds.rs 1.5β†’2.0/2.5 baseline): caused REGRESSION on tier_peak (faster opportunist wins)\n- Both reverted because the strategic MCTS doesn't pick attack actions β€” it only picks `SpawnUnit/FoundCity/Idle` per `mc-turn/src/snapshot.rs:204-214 action_prior`. The capture/development tempo is governed by mc-turn capture mechanics + mc-economy growth rates, NOT by AI scoring weights.\n\nReal levers (cross-team scope):\n- **mc-combat / mc-turn capture mechanics**: increase city HP, lengthen siege duration, add capital-recapture cost, weaken early-rush combat math.\n- **mc-economy growth rates**: faster baseline tech research, lower tier-3 prereq cost, give players tech catch-up bonus when behind.\n- **mc-turn turn-limit floor**: refuse to award domination victory before T150 (force games to mid-game minimum).\n\nPick one or compose multiple. Each requires the corresponding team-lead's involvement." }, { "id": "p1-29a", - "title": "Last-stand defense \u2014 combat-strength multiplier when defender is at last city", + "title": "Last-stand defense β€” combat-strength multiplier when defender is at last city", "priority": "p1", "status": "done", "scope": "game1", "owner": "combat-dev", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "Filed by p1-29 cycle 5 close-out as the combat-side intervention that should close p1-29's `tier_peak_gap \u22644` gate. Three consecutive cycles of research-side levers (catch-up tech-pick mult, catch-up tech-output mult, loss-tolerance lever) landed durably but failed to move the gate across three batches. The failure is structural: p1 (the losing AI) loses cities faster than research output can unlock era-2+ techs. Research-side levers multiply a tiny base into a tiny base. The gate is a **territory problem**, not a research problem.\n\nThis objective addresses the territory problem by giving the defender (when reduced to their last city) a combat-strength bonus that scales with how many cities they've lost \u2014 buying enough turns for the existing research-side levers to finally fire and unlock era-2+ techs." + "summary": "Filed by p1-29 cycle 5 close-out as the combat-side intervention that should close p1-29's `tier_peak_gap ≀4` gate. Three consecutive cycles of research-side levers (catch-up tech-pick mult, catch-up tech-output mult, loss-tolerance lever) landed durably but failed to move the gate across three batches. The failure is structural: p1 (the losing AI) loses cities faster than research output can unlock era-2+ techs. Research-side levers multiply a tiny base into a tiny base. The gate is a **territory problem**, not a research problem.\n\nThis objective addresses the territory problem by giving the defender (when reduced to their last city) a combat-strength bonus that scales with how many cities they've lost β€” buying enough turns for the existing research-side levers to finally fire and unlock era-2+ techs." }, { "id": "p1-29b", - "title": "AI tech tier gap \u2014 structural research path quality (low-pop AI fails to reach t1+)", + "title": "AI tech tier gap β€” structural research path quality (low-pop AI fails to reach t1+)", "priority": "p1", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-05-07", "blocked_by": [], - "summary": "Filed by cycle 47 close-out as the structural root cause for the alive-aware gate failure in p1-29a. The `tier_peak_gap \u2264 4` gate (across ALL living players at game end) failed 1/10 seeds in the cycle-45 batch even after p1-29a's last-stand multiplier landed. The failure is structural: the MCTS evaluator, rollout scorer, and tactical production allocator all apply flat weights that make sole-city players deprioritize research relative to defense and expansion. Tuning multipliers on a compound that starts near zero does not close the gap." + "summary": "Filed by cycle 47 close-out as the structural root cause for the alive-aware gate failure in p1-29a. The `tier_peak_gap ≀ 4` gate (across ALL living players at game end) failed 1/10 seeds in the cycle-45 batch even after p1-29a's last-stand multiplier landed. The failure is structural: the MCTS evaluator, rollout scorer, and tactical production allocator all apply flat weights that make sole-city players deprioritize research relative to defense and expansion. Tuning multipliers on a compound that starts near zero does not close the gap." }, { "id": "p1-29c", - "title": "Sole-city research path \u2014 lift trailing AI from tier_peak=1 to \u22652", + "title": "Sole-city research path β€” lift trailing AI from tier_peak=1 to β‰₯2", "priority": "p1", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-05-27", "blocked_by": [], - "summary": "Filed by p1-29b cycle-50 close-out (2026-05-07) and re-confirmed by p1-29a cycle-45 diagnosis (2026-05-07). p1-29b clamped p0's runaway (9/10 seeds now satisfy `tier_peak_gap \u2264 4`) but **p1 remains stuck at `tier_peak = 1` in every game**. The gap metric was tightened by capping the leader, not by lifting the trailing sole-city AI.\n\nThis objective is the missing structural fix for p1-29a's bullet 5 (alive-aware `tier_peak_gap \u2264 4`, requiring `p0_tp \u2265 2 AND p1_tp \u2265 2`) and bullet 7 (compose-isolation 3-batch, which has no signal to attribute while p1_tp=1 in 100% of seeds)." + "summary": "Filed by p1-29b cycle-50 close-out (2026-05-07) and re-confirmed by p1-29a cycle-45 diagnosis (2026-05-07). p1-29b clamped p0's runaway (9/10 seeds now satisfy `tier_peak_gap ≀ 4`) but **p1 remains stuck at `tier_peak = 1` in every game**. The gap metric was tightened by capping the leader, not by lifting the trailing sole-city AI.\n\nThis objective is the missing structural fix for p1-29a's bullet 5 (alive-aware `tier_peak_gap ≀ 4`, requiring `p0_tp β‰₯ 2 AND p1_tp β‰₯ 2`) and bullet 7 (compose-isolation 3-batch, which has no signal to attribute while p1_tp=1 in 100% of seeds)." }, { "id": "p1-29c-followup-empty-params-json-regression", - "title": "GdEconomy::process_turn fails \u2014 `_build_params_json` produces empty string for autoplay seeds", + "title": "GdEconomy::process_turn fails β€” `_build_params_json` produces empty string for autoplay seeds", "priority": "p1", "status": "done", "scope": "game1", @@ -918,7 +918,7 @@ }, { "id": "p1-29d-p1-survival", - "title": "P1 (trailing AI) eliminated or stalled before T100 in 10/10 seeds \u2014 upstream of action priority", + "title": "P1 (trailing AI) eliminated or stalled before T100 in 10/10 seeds β€” upstream of action priority", "priority": "p1", "status": "done", "scope": "game1", @@ -929,7 +929,7 @@ }, { "id": "p1-29e-rl-divergence-mining", - "title": "RL-policy divergence mining \u2192 sole-city economy break-out (production, not science)", + "title": "RL-policy divergence mining β†’ sole-city economy break-out (production, not science)", "priority": "p1", "status": "done", "scope": "game1", @@ -940,7 +940,7 @@ }, { "id": "p1-29f", - "title": "learned:* controller bridge \u2014 make the trained RL policy playable in-engine", + "title": "learned:* controller bridge β€” make the trained RL policy playable in-engine", "priority": "p1", "status": "done", "scope": "game1", @@ -964,7 +964,7 @@ }, { "id": "p1-29h-stateful-tactical-decisiveness", - "title": "Stateful tactical decisiveness \u2014 army target-lock + commitment hysteresis + press-on-capture in mc-ai", + "title": "Stateful tactical decisiveness β€” army target-lock + commitment hysteresis + press-on-capture in mc-ai", "priority": "p1", "status": "done", "scope": "game1", @@ -975,7 +975,7 @@ }, { "id": "p1-29i-refound-suppression", - "title": "Refound-suppression / capture-stickiness lever \u2014 convert captures into eliminations", + "title": "Refound-suppression / capture-stickiness lever β€” convert captures into eliminations", "priority": "p1", "status": "done", "scope": "game1", @@ -1008,14 +1008,14 @@ }, { "id": "p1-30", - "title": "Optimize `_build_tactical_state` \u2014 8000-tile GDScript dict-build per AI turn blocks p1-22 huge-map gate", + "title": "Optimize `_build_tactical_state` β€” 8000-tile GDScript dict-build per AI turn blocks p1-22 huge-map gate", "priority": "p1", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-05-04", "blocked_by": [], - "summary": "Split out from p1-22 (MCTS per-decision wall-clock budget). p1-22 closed `partial` after cycles 2-3 shipped strategic + tactical Rust budgets (`mcts_tree::simulate_parallel` + all 5 `mc-ai/src/tactical/` submodules + `GdAiController::set_budget_ms`, 186/186 lib tests). Strategic budget verified working: p1-22-cycle-2 batch had seeds 9 and 10 reach T500 victories at max_tier=10 and 7. Tactical budget verified bounded by unit test `tactical::tests::tactical_budget_respected`.\n\nBut the huge-map \u22655/10 victories sub-gate FAILED at 2/10 in the cycle-2 batch because seeds 1-8 hung at low turn counts (T43-T236). The Rust paths are bounded; the hang is in GDScript. Specifically: `src/game/engine/src/modules/ai/ai_turn_bridge.gd:248-250` (`_build_tactical_state`) iterates `width \u00d7 height` tiles (112\u00d772 = 8064 on a huge map) building a Dictionary per tile to serialize as JSON to feed `GdAiController::decide_actions`. That serialization runs every AI turn per player.\n\n5 AI players \u00d7 ~8000 tile-dicts \u00d7 ~T100 turns = ~4 million GDScript dict allocations per game. Each dict allocation is microseconds but compounds. The Rust `MCTS_DECISION_BUDGET_MS=2000` doesn't bound this \u2014 by the time Rust gets the JSON, the GDScript serialization has already chewed through the wall-clock budget for the turn.\n\nThe fix has two reasonable shapes:\n1. **Delta serialization**: only serialize tiles whose state changed since last AI turn. Cache per-player. ~10\u00d7 speedup.\n2. **Move tile state into mc-turn**: have Rust own the tile catalog (already partial via `TacticalMap`), pass an opaque handle from GDScript instead of full JSON. Eliminates the GDScript dict-build entirely. Aligns with Rail-1.\n\nOption 2 is more correct (Rail-1) but bigger surface. Option 1 is the quick win." + "summary": "Split out from p1-22 (MCTS per-decision wall-clock budget). p1-22 closed `partial` after cycles 2-3 shipped strategic + tactical Rust budgets (`mcts_tree::simulate_parallel` + all 5 `mc-ai/src/tactical/` submodules + `GdAiController::set_budget_ms`, 186/186 lib tests). Strategic budget verified working: p1-22-cycle-2 batch had seeds 9 and 10 reach T500 victories at max_tier=10 and 7. Tactical budget verified bounded by unit test `tactical::tests::tactical_budget_respected`.\n\nBut the huge-map β‰₯5/10 victories sub-gate FAILED at 2/10 in the cycle-2 batch because seeds 1-8 hung at low turn counts (T43-T236). The Rust paths are bounded; the hang is in GDScript. Specifically: `src/game/engine/src/modules/ai/ai_turn_bridge.gd:248-250` (`_build_tactical_state`) iterates `width Γ— height` tiles (112Γ—72 = 8064 on a huge map) building a Dictionary per tile to serialize as JSON to feed `GdAiController::decide_actions`. That serialization runs every AI turn per player.\n\n5 AI players Γ— ~8000 tile-dicts Γ— ~T100 turns = ~4 million GDScript dict allocations per game. Each dict allocation is microseconds but compounds. The Rust `MCTS_DECISION_BUDGET_MS=2000` doesn't bound this β€” by the time Rust gets the JSON, the GDScript serialization has already chewed through the wall-clock budget for the turn.\n\nThe fix has two reasonable shapes:\n1. **Delta serialization**: only serialize tiles whose state changed since last AI turn. Cache per-player. ~10Γ— speedup.\n2. **Move tile state into mc-turn**: have Rust own the tile catalog (already partial via `TacticalMap`), pass an opaque handle from GDScript instead of full JSON. Eliminates the GDScript dict-build entirely. Aligns with Rail-1.\n\nOption 2 is more correct (Rail-1) but bigger surface. Option 1 is the quick win." }, { "id": "p1-31", @@ -1025,7 +1025,7 @@ "scope": "game1", "updated_at": "2026-04-27", "blocked_by": [], - "summary": "`public/resources/units/` follows a one-file-per-unit convention: 65 dwarf-prefixed unit files (`dwarf_warrior.json`, `dwarf_founder.json`, ...) plus generic-class siblings under `public/games/age-of-dwarves/data/units/` (`warrior.json`, `worker.json`, `archer.json`, ...). Total 150 unit IDs, each in its own file. Easy to find, easy to diff, easy to git-blame, easy to override one without disturbing others.\n\n`public/resources/buildings/` does NOT follow the same convention. It contains a mix:\n- ~30 single-building files (`ancient_well.json`, `chronicle_tower.json`, ...) \u2014 the per-file pattern.\n- 11 **category bundle** files that pack 5\u20138 buildings each into one file:\n - `economy.json` (5 buildings: granary, mill, market, storehouse, brewery)\n - `military.json` (5: barracks, stable, forge, armory, siege_workshop)\n - `defense_special.json` (8: walls, watchtower, castle, mason_lodge, smithy, dwarf_deep_forge, infirmary, courthouse)\n - `science_culture.json` (7: library, university, observatory, monument, gathering_hall, great_hall, temple)\n - `production_advanced.json` (8), `military_advanced.json` (6), `naval_buildings.json` (5),\n - `science_nature.json` (7), `infrastructure_advanced.json` (5), `ecology_buildings.json` (5),\n - `crafting_producers.json` (5)\n\nThat's 66 buildings hidden inside 11 bundles. Editing `granary` means opening `economy.json` and scrolling. `git log -- granary` returns nothing meaningful \u2014 the line history is per bundle, not per building. Authoring a new building means picking a category bundle to edit instead of a clean filename. Schema validators that run \"per-file\" iterate at the wrong granularity.\n\nThe bundling also created an audit blind-spot: a previous review of `data/buildings/` (per-file, 31 files) reported \"9 tech-tree-referenced buildings missing\" because all 9 actually live in `resources/buildings/.json` bundles and were never noticed.\n\nThis objective splits every bundle into per-building files, matching the units convention." + "summary": "`public/resources/units/` follows a one-file-per-unit convention: 65 dwarf-prefixed unit files (`dwarf_warrior.json`, `dwarf_founder.json`, ...) plus generic-class siblings under `public/games/age-of-dwarves/data/units/` (`warrior.json`, `worker.json`, `archer.json`, ...). Total 150 unit IDs, each in its own file. Easy to find, easy to diff, easy to git-blame, easy to override one without disturbing others.\n\n`public/resources/buildings/` does NOT follow the same convention. It contains a mix:\n- ~30 single-building files (`ancient_well.json`, `chronicle_tower.json`, ...) β€” the per-file pattern.\n- 11 **category bundle** files that pack 5–8 buildings each into one file:\n - `economy.json` (5 buildings: granary, mill, market, storehouse, brewery)\n - `military.json` (5: barracks, stable, forge, armory, siege_workshop)\n - `defense_special.json` (8: walls, watchtower, castle, mason_lodge, smithy, dwarf_deep_forge, infirmary, courthouse)\n - `science_culture.json` (7: library, university, observatory, monument, gathering_hall, great_hall, temple)\n - `production_advanced.json` (8), `military_advanced.json` (6), `naval_buildings.json` (5),\n - `science_nature.json` (7), `infrastructure_advanced.json` (5), `ecology_buildings.json` (5),\n - `crafting_producers.json` (5)\n\nThat's 66 buildings hidden inside 11 bundles. Editing `granary` means opening `economy.json` and scrolling. `git log -- granary` returns nothing meaningful β€” the line history is per bundle, not per building. Authoring a new building means picking a category bundle to edit instead of a clean filename. Schema validators that run \"per-file\" iterate at the wrong granularity.\n\nThe bundling also created an audit blind-spot: a previous review of `data/buildings/` (per-file, 31 files) reported \"9 tech-tree-referenced buildings missing\" because all 9 actually live in `resources/buildings/.json` bundles and were never noticed.\n\nThis objective splits every bundle into per-building files, matching the units convention." }, { "id": "p1-32", @@ -1035,7 +1035,7 @@ "scope": "game1", "updated_at": "2026-05-03", "blocked_by": [], - "summary": "`public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md` describes a stockpile-based processing economy with six processing buildings: `granary`, `mill`, `brewery`, `tannery`, `sawmill`, `herbalist`. Audit confirms the first four exist in `resources/buildings/` bundles (`economy.json` carries granary/mill/brewery; `crafting_producers.json` carries tannery). The remaining two are truly missing.\n\n| Building | Designed source | Status |\n|---|---|---|\n| `granary` | Husbandry tech, food storage | \u2713 in `resources/buildings/economy.json` |\n| `mill` | Husbandry tech, grain \u2192 flour | \u2713 in `resources/buildings/economy.json` |\n| `brewery` | Brewing tech, ale (happiness + trade) | \u2713 in `resources/buildings/economy.json` |\n| `tannery` | Tanning tech, hides \u2192 leather | \u2713 in `resources/buildings/crafting_producers.json` |\n| `sawmill` | Logging tech, timber \u2192 lumber | **missing** \u2014 closest is `lumber_camp` (production_advanced.json, different concept) |\n| `herbalist` | Herbalism tech, reagents \u2192 academy | **missing** \u2014 closest are `alchemist_workshop` / `alchemist_bench` (different concept) |\n\nThis objective authors only the two genuinely missing buildings. The naming question \u2014 should `sawmill` reuse the existing `lumber_camp` slot, and should `herbalist` reuse `alchemist_workshop`? \u2014 needs a design decision before content is authored." + "summary": "`public/games/age-of-dwarves/docs/cities/PRODUCTION_CHAIN.md` describes a stockpile-based processing economy with six processing buildings: `granary`, `mill`, `brewery`, `tannery`, `sawmill`, `herbalist`. Audit confirms the first four exist in `resources/buildings/` bundles (`economy.json` carries granary/mill/brewery; `crafting_producers.json` carries tannery). The remaining two are truly missing.\n\n| Building | Designed source | Status |\n|---|---|---|\n| `granary` | Husbandry tech, food storage | βœ“ in `resources/buildings/economy.json` |\n| `mill` | Husbandry tech, grain β†’ flour | βœ“ in `resources/buildings/economy.json` |\n| `brewery` | Brewing tech, ale (happiness + trade) | βœ“ in `resources/buildings/economy.json` |\n| `tannery` | Tanning tech, hides β†’ leather | βœ“ in `resources/buildings/crafting_producers.json` |\n| `sawmill` | Logging tech, timber β†’ lumber | **missing** β€” closest is `lumber_camp` (production_advanced.json, different concept) |\n| `herbalist` | Herbalism tech, reagents β†’ academy | **missing** β€” closest are `alchemist_workshop` / `alchemist_bench` (different concept) |\n\nThis objective authors only the two genuinely missing buildings. The naming question β€” should `sawmill` reuse the existing `lumber_camp` slot, and should `herbalist` reuse `alchemist_workshop`? β€” needs a design decision before content is authored." }, { "id": "p1-33", @@ -1045,73 +1045,73 @@ "scope": "game1", "updated_at": "2026-05-03", "blocked_by": [], - "summary": "The buildings `harbor` (`resources/buildings/naval_buildings.json`, `enables_naval` effect, `requires_coastal: true`) and `airfield` (`resources/buildings/infrastructure_advanced.json`) already exist and are loaded by the engine. The gating problem is upstream of the buildings:\n\n- The 14 dwarf naval units (`dwarf_river_galley` \u2192 `dwarf_fortress_ship`) carry only a `tech_required` field. None reference `harbor` or `requires_building`. A landlocked city the moment `shipbuilding` is researched can build a `dwarf_dreadnought`.\n- The 7 dwarf aerial units (`dwarf_gyrocopter` \u2192 `dwarf_sky_fortress`) similarly do not reference `airfield`. Any city with `mechanical_flight` can build a sky_fortress on a mountaintop with no infrastructure.\n\nThe `harbor` building's effect `enables_naval: true` is suggestive of an intended gate, but no Rust or GDScript code consumes that effect. Naval / aerial unit eligibility is determined entirely by tech.\n\nThis objective wires the gate." + "summary": "The buildings `harbor` (`resources/buildings/naval_buildings.json`, `enables_naval` effect, `requires_coastal: true`) and `airfield` (`resources/buildings/infrastructure_advanced.json`) already exist and are loaded by the engine. The gating problem is upstream of the buildings:\n\n- The 14 dwarf naval units (`dwarf_river_galley` β†’ `dwarf_fortress_ship`) carry only a `tech_required` field. None reference `harbor` or `requires_building`. A landlocked city the moment `shipbuilding` is researched can build a `dwarf_dreadnought`.\n- The 7 dwarf aerial units (`dwarf_gyrocopter` β†’ `dwarf_sky_fortress`) similarly do not reference `airfield`. Any city with `mechanical_flight` can build a sky_fortress on a mountaintop with no infrastructure.\n\nThe `harbor` building's effect `enables_naval: true` is suggestive of an intended gate, but no Rust or GDScript code consumes that effect. Naval / aerial unit eligibility is determined entirely by tech.\n\nThis objective wires the gate." }, { "id": "p1-34", - "title": "Unit metadata expansion \u2014 flavor, archetype, promotion_tree, clan_affinity fields", + "title": "Unit metadata expansion β€” flavor, archetype, promotion_tree, clan_affinity fields", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-27", "blocked_by": [], - "summary": "The newly authored 50-unit dwarven military roster (p1-34 follow-on from the\nT1\u2013T10 design pass) currently mashes mechanical role text and lore one-liner\ninto a single `description` field. The schema is missing four high-value\nmetadata fields the rest of the system needs:\n\n- **`flavor`** \u2014 the lore one-liner, separate from the mechanical\n description. Already the convention in tech files (`combined_arms`,\n `runelore`, etc.); units inherit zero of that pattern.\n- **`archetype`** \u2014 the explicit role categorization (light_melee /\n heavy_melee / anti_cavalry / ranged / siege / cavalry_walker / wild).\n Currently the React calculator and AI builders infer this from\n `unit_type` + `keywords` heuristics, which is fragile and breaks the\n moment a new keyword combination lands.\n- **`promotion_tree`** \u2014 the link from unit to which `promotions.json`\n tree applies (`melee` / `ranged` / `siege` / null for wild). Without\n this, units can't actually use the promotion system that's already\n authored.\n- **`clan_affinity`** \u2014 list of 1\u20133 AI clan IDs that favor building this\n unit (`ironhold` / `goldvein` / `blackhammer` / `deepforge` /\n `runesmith`). Drives clan personality differentiation; currently all\n five clans pick units off the same flat priority list.\n\nThis is a **schema-and-data** objective. Touches all 75 existing unit JSONs\n(50 newly-authored dwarven + 25 original including wild creatures). React\ncalculator data loader (`allUnits.ts`) gets cleanup \u2014 drop the inference\nlogic, read fields directly." + "summary": "The newly authored 50-unit dwarven military roster (p1-34 follow-on from the\nT1–T10 design pass) currently mashes mechanical role text and lore one-liner\ninto a single `description` field. The schema is missing four high-value\nmetadata fields the rest of the system needs:\n\n- **`flavor`** β€” the lore one-liner, separate from the mechanical\n description. Already the convention in tech files (`combined_arms`,\n `runelore`, etc.); units inherit zero of that pattern.\n- **`archetype`** β€” the explicit role categorization (light_melee /\n heavy_melee / anti_cavalry / ranged / siege / cavalry_walker / wild).\n Currently the React calculator and AI builders infer this from\n `unit_type` + `keywords` heuristics, which is fragile and breaks the\n moment a new keyword combination lands.\n- **`promotion_tree`** β€” the link from unit to which `promotions.json`\n tree applies (`melee` / `ranged` / `siege` / null for wild). Without\n this, units can't actually use the promotion system that's already\n authored.\n- **`clan_affinity`** β€” list of 1–3 AI clan IDs that favor building this\n unit (`ironhold` / `goldvein` / `blackhammer` / `deepforge` /\n `runesmith`). Drives clan personality differentiation; currently all\n five clans pick units off the same flat priority list.\n\nThis is a **schema-and-data** objective. Touches all 75 existing unit JSONs\n(50 newly-authored dwarven + 25 original including wild creatures). React\ncalculator data loader (`allUnits.ts`) gets cleanup β€” drop the inference\nlogic, read fields directly." }, { "id": "p1-35", - "title": "Per-unit lore paragraphs \u2014 historical/cultural context for the dwarven roster", + "title": "Per-unit lore paragraphs β€” historical/cultural context for the dwarven roster", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-27", "blocked_by": [], - "summary": "The newly authored 50-unit dwarven roster has strong one-liner flavor but no\nparagraph-length cultural/historical context. Each unit needs a `lore` field\nexplaining its place in dwarven society \u2014 which clan invented it, what\nhistorical event birthed it, why it survived in the doctrine.\n\nExisting dwarven voice anchors (from `data/techs/foundations.json` and\n`advanced_military.json`):\n- \"We do not learn the mountain. We remember it.\" (dwarf_heritage)\n- \"A spear alone is courage. A line of spears is an empire.\" (combined_arms)\n- \"It is not magic. The runes only ask the powder to remember its purpose.\" (gunpowder, this work)\n\nThe lore field should sit ALONGSIDE `flavor` (added by p1-34) and provide\n3\u20135 sentences of cultural worldbuilding. Surfaced in the player guide\nencyclopedia and in unit detail panels." + "summary": "The newly authored 50-unit dwarven roster has strong one-liner flavor but no\nparagraph-length cultural/historical context. Each unit needs a `lore` field\nexplaining its place in dwarven society β€” which clan invented it, what\nhistorical event birthed it, why it survived in the doctrine.\n\nExisting dwarven voice anchors (from `data/techs/foundations.json` and\n`advanced_military.json`):\n- \"We do not learn the mountain. We remember it.\" (dwarf_heritage)\n- \"A spear alone is courage. A line of spears is an empire.\" (combined_arms)\n- \"It is not magic. The runes only ask the powder to remember its purpose.\" (gunpowder, this work)\n\nThe lore field should sit ALONGSIDE `flavor` (added by p1-34) and provide\n3–5 sentences of cultural worldbuilding. Surfaced in the player guide\nencyclopedia and in unit detail panels." }, { "id": "p1-36", - "title": "AI personalities \u2014 T1\u2013T10 build order coverage + clan_affinity routing", + "title": "AI personalities β€” T1–T10 build order coverage + clan_affinity routing", "priority": "p1", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-05-03", "blocked_by": [], - "summary": "`public/games/age-of-dwarves/data/ai_personalities.json` currently lists\nhardcoded early build orders that reference only the old 10-unit roster\n(warrior / forge / walls / dwarf_founder). The new T1\u2013T10 roster (50 new\nunits) is invisible to all five AI clans \u2014 they cannot build a Shield\nBearer, a Ballista Crew, a Boar Scout, or any T7+ unit.\n\nCombined with `clan_affinity` (added by p1-34), this objective wires the\nfive AI personalities to actually feel distinct in the units they build:\n\n- **Ironhold** (production 9, aggressive 6) \u2014 heavy melee anchored\n defender lines, walls, anvil_guard at T6, mountain_king late game\n- **Goldvein** (wealth 9, trade 9) \u2014 cheap cost-efficient units, mercenary\n archers, defensive pikemen, light_field_gun for cavalry counter\n- **Blackhammer** (aggression 9) \u2014 light melee rush, hearth_raiders,\n berserkers, war_rams for cavalry pressure, doomsoul end-game\n- **Deepforge** (production 8, isolationist) \u2014 siege + walker focus,\n forge_titan, rail_cannon, adamantine_tank, ancestral_walker\n- **Runesmith** (balanced) \u2014 runic units, rune_spears, marksmen, mixed\n army with one of each archetype" + "summary": "`public/games/age-of-dwarves/data/ai_personalities.json` currently lists\nhardcoded early build orders that reference only the old 10-unit roster\n(warrior / forge / walls / dwarf_founder). The new T1–T10 roster (50 new\nunits) is invisible to all five AI clans β€” they cannot build a Shield\nBearer, a Ballista Crew, a Boar Scout, or any T7+ unit.\n\nCombined with `clan_affinity` (added by p1-34), this objective wires the\nfive AI personalities to actually feel distinct in the units they build:\n\n- **Ironhold** (production 9, aggressive 6) β€” heavy melee anchored\n defender lines, walls, anvil_guard at T6, mountain_king late game\n- **Goldvein** (wealth 9, trade 9) β€” cheap cost-efficient units, mercenary\n archers, defensive pikemen, light_field_gun for cavalry counter\n- **Blackhammer** (aggression 9) β€” light melee rush, hearth_raiders,\n berserkers, war_rams for cavalry pressure, doomsoul end-game\n- **Deepforge** (production 8, isolationist) β€” siege + walker focus,\n forge_titan, rail_cannon, adamantine_tank, ancestral_walker\n- **Runesmith** (balanced) β€” runic units, rune_spears, marksmen, mixed\n army with one of each archetype" }, { "id": "p1-37", - "title": "mc-ai clan_affinity routing \u2014 Rust AI reads unit clan_affinity at build-decision time", + "title": "mc-ai clan_affinity routing β€” Rust AI reads unit clan_affinity at build-decision time", "priority": "p1", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "p1-36 landed the data side: every unit JSON has a `clan_affinity` array (per p1-34's\nschema expansion), and `ai_personalities.json` now has tiered build orders (early /\nmid / late) per clan that respect clan affinity. But the **decision loop that picks\nunits doesn't yet read either field** \u2014 it still selects from a flat priority list,\nso all five clans build similarly-statted armies and the Ironhold-vs-Blackhammer\ngameplay difference doesn't surface in actual matches.\n\nPer **Rail #1** (Rust is the simulation source of truth), this work lands in\n`src/simulator/crates/mc-ai/`, NOT in any GDScript file. The completed p0-26 port\nestablished the `GdAiController` bridge; this objective extends the build-order /\nunit-selection path inside `mc-ai` to consume `clan_affinity` data." + "summary": "p1-36 landed the data side: every unit JSON has a `clan_affinity` array (per p1-34's\nschema expansion), and `ai_personalities.json` now has tiered build orders (early /\nmid / late) per clan that respect clan affinity. But the **decision loop that picks\nunits doesn't yet read either field** β€” it still selects from a flat priority list,\nso all five clans build similarly-statted armies and the Ironhold-vs-Blackhammer\ngameplay difference doesn't surface in actual matches.\n\nPer **Rail #1** (Rust is the simulation source of truth), this work lands in\n`src/simulator/crates/mc-ai/`, NOT in any GDScript file. The completed p0-26 port\nestablished the `GdAiController` bridge; this objective extends the build-order /\nunit-selection path inside `mc-ai` to consume `clan_affinity` data." }, { "id": "p1-38", - "title": "Biome \u2192 economy coupling \u2014 population & luxury driven by live ecology", + "title": "Biome β†’ economy coupling β€” population & luxury driven by live ecology", "priority": "p1", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "Population growth and luxury supply have been decoupled from the live ecology\nsimulation since `mc-flora` was wired up. Cities read static per-terrain food\nyields (`grassland.food=2`, `plains.food=1`); 70 fauna species exist purely\nas combat encounters with no contribution to the city economy; the\n`mc-happiness::get_growth_modifier` tiering (1.25 / 1.00 / 0.50 / 0.00) was\ncomputed but unused on the GDScript side. This objective re-couples the\ncity economy to the ecology layer in four phases (C \u2192 A \u2192 B \u2192 D), each\nsized to land independently with its own balance regression risk.\n\nThe four phases were approved together as a single `p1` objective in plan\n`~/.claude/plans/hi-so-in-valiant-mango.md` (2026-04-27), but ship in\nsequence so `p1-05`'s baseline bands (median `pop_peak=69`, batch\n`p016b_20260417_024754`) are not disturbed." + "summary": "Population growth and luxury supply have been decoupled from the live ecology\nsimulation since `mc-flora` was wired up. Cities read static per-terrain food\nyields (`grassland.food=2`, `plains.food=1`); 70 fauna species exist purely\nas combat encounters with no contribution to the city economy; the\n`mc-happiness::get_growth_modifier` tiering (1.25 / 1.00 / 0.50 / 0.00) was\ncomputed but unused on the GDScript side. This objective re-couples the\ncity economy to the ecology layer in four phases (C β†’ A β†’ B β†’ D), each\nsized to land independently with its own balance regression risk.\n\nThe four phases were approved together as a single `p1` objective in plan\n`~/.claude/plans/hi-so-in-valiant-mango.md` (2026-04-27), but ship in\nsequence so `p1-05`'s baseline bands (median `pop_peak=69`, batch\n`p016b_20260417_024754`) are not disturbed." }, { "id": "p1-39", - "title": "Port per-yield difficulty multipliers from GDScript into Rust crates (Rail-1) \u2014 research + culture", + "title": "Port per-yield difficulty multipliers from GDScript into Rust crates (Rail-1) β€” research + culture", "priority": "p1", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-05-05", "blocked_by": [], - "summary": "During p1-29 Round 3-5, warcouncil added a per-yield difficulty multiplier framework (gold_mult, culture_mult, luxury_mult, research_mult, production_mult, yield_per_turn_growth) plus a symmetric player handicap (Easy = player gets Hard-AI bonuses). Per Rail-1, the multiplier APPLICATION should live in Rust crates, not in GDScript turn_processor.gd / economy.gd / turn_processor_helpers.gd.\n\n**Gold yield port: DONE 2026-04-27.** `EconomyParams.yield_mult` field added to `api-gdext/src/lib.rs:2962-2992` with serde default 1.0; `GdEconomy::process_turn` now scales gross income before netting expenses. GDScript `economy.gd::_build_params_json` injects `yield_mult` from `GameState.get_effective_yield_mult(player, \"gold\")`; the GDScript-side multiplication is deleted. Validated via 10-seed Hard batch `.local/iter/p1-31-r5-hard-20260427_044618/` \u2014 4/5 quality gates PASS (up from 3/5), clan diversity up from 3 to 5 distinct winners.\n\n**Research yield port: DONE 2026-04-27.** Added `process_research(player_json, yield_json, sci_modifier)` passthrough at `knowledge_web.gd:152` (delegates to `_bridge.call(\"process_research\", ...)`) and exposed at `tech_web.gd:39`. Refactored `turn_processor.gd::_process_research:143` to delegate fully \u2014 assembles JSON inputs (player_dict + per-city yields_arr), calls `tw.process_research()`, only handles completion side-effects (school_locked emit, _form_high_archon, tech_researched signal, resource reveals). No Rust rebuild needed (GdTechWeb::process_research already had sci_modifier as a direct parameter; only the wrapper-layer plumbing was missing).\n\nValidation: `.local/iter/p1-39-r6-hard-20260427_054348/` (10-seed Hard batch). 4/5 quality gates PASS: median winner_tier_peak=4.5 PASS (was 3 FAIL in R5 \u2014 research port LIFTED this), tier_peak_gap=5.0 FAIL (was 3.5 PASS \u2014 gates alternate, total still 4/5), max_peak_unit 10/10 PASS, wonders 7/10 PASS, combats 454 PASS. **All 10 games completed (vs 8/10 in R5)**, **6 distinct winners** (max diversity for 5-clan game).\n\n**Culture yield port: COMPLETED 2026-04-29.** Root cause of R7/R8 divergence was NOT floating-point semantics. Two bugs caused the apparent divergence:\n\n1. **Stale GDExtension binary on apricot** \u2014 R7 and R8 batches ran against an old `.so` that lacked `process_culture_with_modifier`. Godot emitted `SCRIPT ERROR: Invalid call. Nonexistent function 'process_culture_with_modifier'` on every culture call per turn, culture never accumulated, games ended at T111 vs R6/R9's T251. Evidence: `game.log` in both R7 and R8 dirs contains this exact error; R9 (reverted to `process_culture`) worked because that symbol WAS in the old binary.\n\n2. **Missing GDScript bridge wrappers** \u2014 `city.gd` and `city_rust_bridge.gd` delegated `process_culture` but had no `process_culture_with_modifier` method. The delegation chain from `turn_processor.gd` \u2192 `city.gd` \u2192 `city_rust_bridge.gd` \u2192 `_gd_city.call(...)` was incomplete.\n\nThe \"f64 vs Variant FLOAT round-trip\" hypothesis in the revert comment was incorrect. Godot 4 Variant FLOAT is f64 (lossless), and the Rust math is algebraically identical to the original GDScript.\n\nFix (2026-04-29): added `process_culture_with_modifier` wrappers to `city.gd` and `city_rust_bridge.gd`; switched `turn_processor.gd::_process_culture` to call `c.process_culture_with_modifier(tile_json, total_pct)`; rebuilt GDExtension on apricot (binary dated 2026-04-29 21:43). R12 validation run: zero `process_culture_with_modifier` errors. Games terminate at T22 due to an unrelated AI regression (`set_map`/`captured_turn` errors from p1-29 changes) not from culture.\n\nAll three yield types (gold, research, culture) are now ported to Rust. The one remaining open acceptance bullet is replay parity \u2014 blocked on unrelated p1-29 AI regressions, not on the culture port itself." + "summary": "During p1-29 Round 3-5, warcouncil added a per-yield difficulty multiplier framework (gold_mult, culture_mult, luxury_mult, research_mult, production_mult, yield_per_turn_growth) plus a symmetric player handicap (Easy = player gets Hard-AI bonuses). Per Rail-1, the multiplier APPLICATION should live in Rust crates, not in GDScript turn_processor.gd / economy.gd / turn_processor_helpers.gd.\n\n**Gold yield port: DONE 2026-04-27.** `EconomyParams.yield_mult` field added to `api-gdext/src/lib.rs:2962-2992` with serde default 1.0; `GdEconomy::process_turn` now scales gross income before netting expenses. GDScript `economy.gd::_build_params_json` injects `yield_mult` from `GameState.get_effective_yield_mult(player, \"gold\")`; the GDScript-side multiplication is deleted. Validated via 10-seed Hard batch `.local/iter/p1-31-r5-hard-20260427_044618/` β€” 4/5 quality gates PASS (up from 3/5), clan diversity up from 3 to 5 distinct winners.\n\n**Research yield port: DONE 2026-04-27.** Added `process_research(player_json, yield_json, sci_modifier)` passthrough at `knowledge_web.gd:152` (delegates to `_bridge.call(\"process_research\", ...)`) and exposed at `tech_web.gd:39`. Refactored `turn_processor.gd::_process_research:143` to delegate fully β€” assembles JSON inputs (player_dict + per-city yields_arr), calls `tw.process_research()`, only handles completion side-effects (school_locked emit, _form_high_archon, tech_researched signal, resource reveals). No Rust rebuild needed (GdTechWeb::process_research already had sci_modifier as a direct parameter; only the wrapper-layer plumbing was missing).\n\nValidation: `.local/iter/p1-39-r6-hard-20260427_054348/` (10-seed Hard batch). 4/5 quality gates PASS: median winner_tier_peak=4.5 PASS (was 3 FAIL in R5 β€” research port LIFTED this), tier_peak_gap=5.0 FAIL (was 3.5 PASS β€” gates alternate, total still 4/5), max_peak_unit 10/10 PASS, wonders 7/10 PASS, combats 454 PASS. **All 10 games completed (vs 8/10 in R5)**, **6 distinct winners** (max diversity for 5-clan game).\n\n**Culture yield port: COMPLETED 2026-04-29.** Root cause of R7/R8 divergence was NOT floating-point semantics. Two bugs caused the apparent divergence:\n\n1. **Stale GDExtension binary on apricot** β€” R7 and R8 batches ran against an old `.so` that lacked `process_culture_with_modifier`. Godot emitted `SCRIPT ERROR: Invalid call. Nonexistent function 'process_culture_with_modifier'` on every culture call per turn, culture never accumulated, games ended at T111 vs R6/R9's T251. Evidence: `game.log` in both R7 and R8 dirs contains this exact error; R9 (reverted to `process_culture`) worked because that symbol WAS in the old binary.\n\n2. **Missing GDScript bridge wrappers** β€” `city.gd` and `city_rust_bridge.gd` delegated `process_culture` but had no `process_culture_with_modifier` method. The delegation chain from `turn_processor.gd` β†’ `city.gd` β†’ `city_rust_bridge.gd` β†’ `_gd_city.call(...)` was incomplete.\n\nThe \"f64 vs Variant FLOAT round-trip\" hypothesis in the revert comment was incorrect. Godot 4 Variant FLOAT is f64 (lossless), and the Rust math is algebraically identical to the original GDScript.\n\nFix (2026-04-29): added `process_culture_with_modifier` wrappers to `city.gd` and `city_rust_bridge.gd`; switched `turn_processor.gd::_process_culture` to call `c.process_culture_with_modifier(tile_json, total_pct)`; rebuilt GDExtension on apricot (binary dated 2026-04-29 21:43). R12 validation run: zero `process_culture_with_modifier` errors. Games terminate at T22 due to an unrelated AI regression (`set_map`/`captured_turn` errors from p1-29 changes) not from culture.\n\nAll three yield types (gold, research, culture) are now ported to Rust. The one remaining open acceptance bullet is replay parity β€” blocked on unrelated p1-29 AI regressions, not on the culture port itself." }, { "id": "p1-40", @@ -1121,7 +1121,7 @@ "scope": "game1", "updated_at": "2026-04-29", "blocked_by": [], - "summary": "Today `public/games/age-of-dwarves/data/{units,buildings,techs}/` mirrors `public/resources/{units,buildings,techs}/` with override semantics: the loader walks resources/ first, then game/data/ overwrites by id. This created three real bugs in the last few sessions:\n1. Audit blind-spot: 9 \"missing\" buildings and 6 \"missing\" food/processing buildings turned out to live in resources/buildings bundled files that the per-file audit missed.\n2. Silent semantic drift: 14 building IDs are defined in both layers with different cost/tech/effects; the data/ version wins by accident-of-loader-order.\n3. Broken tech gates: 6 of 8 ordinary-building duplicates have `tech_required` in resources/ pointing at non-existent techs (`military_doctrine`, `smelting`, `husbandry`, `scholarship`, `ancestor_rites`, `masonry`, `mathematics`). The data/ overrides are the only thing keeping those buildings buildable.\n\nThe right architecture is one source of truth at `public/resources//`, with `public/games//` carrying only **game-pack-specific configuration** (clan personalities, setup, vocab, difficulty) and a manifest declaring which resource IDs the game subscribes to. No more override layer.\n\nThis objective is the **safe mechanical phase** \u2014 move all entity files to resources/ canonical locations and resolve the duplicates. The behavioral phase (subscription manifest + loader filter) splits to `p1-41`." + "summary": "Today `public/games/age-of-dwarves/data/{units,buildings,techs}/` mirrors `public/resources/{units,buildings,techs}/` with override semantics: the loader walks resources/ first, then game/data/ overwrites by id. This created three real bugs in the last few sessions:\n1. Audit blind-spot: 9 \"missing\" buildings and 6 \"missing\" food/processing buildings turned out to live in resources/buildings bundled files that the per-file audit missed.\n2. Silent semantic drift: 14 building IDs are defined in both layers with different cost/tech/effects; the data/ version wins by accident-of-loader-order.\n3. Broken tech gates: 6 of 8 ordinary-building duplicates have `tech_required` in resources/ pointing at non-existent techs (`military_doctrine`, `smelting`, `husbandry`, `scholarship`, `ancestor_rites`, `masonry`, `mathematics`). The data/ overrides are the only thing keeping those buildings buildable.\n\nThe right architecture is one source of truth at `public/resources//`, with `public/games//` carrying only **game-pack-specific configuration** (clan personalities, setup, vocab, difficulty) and a manifest declaring which resource IDs the game subscribes to. No more override layer.\n\nThis objective is the **safe mechanical phase** β€” move all entity files to resources/ canonical locations and resolve the duplicates. The behavioral phase (subscription manifest + loader filter) splits to `p1-41`." }, { "id": "p1-41", @@ -1131,7 +1131,7 @@ "scope": "game1", "updated_at": "2026-04-29", "blocked_by": [], - "summary": "Phase A (`p1-40`) collapsed the data/ override layer into single source of truth at `public/resources//`. All 155 unit IDs and 159 building IDs now live in resources/, one file each. The data loader still walks both layers \u2014 `resources/` then `data//` \u2014 but the data/ side is now empty for those categories.\n\nPhase B introduces a per-game **subscription manifest** that declares which resource IDs each game uses, and a loader filter that restricts the in-memory `_data` dict to that subset. This is what makes the architecture viable when a second game exists: Age of Dwarves subscribes only to dwarf-relevant content; Age of Kzzykt (Game 2) subscribes to its own roster without inheriting dwarf-specific entities by accident.\n\nFor Game 1 alone, this objective is **architecturally correct but functionally redundant** \u2014 Age of Dwarves currently subscribes to 100% of resources/. The objective is filed at `p1` (not deferred to `p3`) because Game 2 work begins as soon as Game 1 ships EA, and the subscription mechanism needs to exist before content for Game 2 starts landing in `resources//` (otherwise Game 2 races and units would automatically appear in Age of Dwarves saves)." + "summary": "Phase A (`p1-40`) collapsed the data/ override layer into single source of truth at `public/resources//`. All 155 unit IDs and 159 building IDs now live in resources/, one file each. The data loader still walks both layers β€” `resources/` then `data//` β€” but the data/ side is now empty for those categories.\n\nPhase B introduces a per-game **subscription manifest** that declares which resource IDs each game uses, and a loader filter that restricts the in-memory `_data` dict to that subset. This is what makes the architecture viable when a second game exists: Age of Dwarves subscribes only to dwarf-relevant content; Age of Kzzykt (Game 2) subscribes to its own roster without inheriting dwarf-specific entities by accident.\n\nFor Game 1 alone, this objective is **architecturally correct but functionally redundant** β€” Age of Dwarves currently subscribes to 100% of resources/. The objective is filed at `p1` (not deferred to `p3`) because Game 2 work begins as soon as Game 1 ships EA, and the subscription mechanism needs to exist before content for Game 2 starts landing in `resources//` (otherwise Game 2 races and units would automatically appear in Age of Dwarves saves)." }, { "id": "p1-42", @@ -1141,21 +1141,21 @@ "scope": "game1", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "`mc-ai/tactical/production.rs::ids` hardcodes 8 building/unit IDs (`WARRIOR`, `WORKER`, `FOUNDER`, `WALLS`, `FORGE`, `CASTLE`, `MARKETPLACE`, `GRANARY`) and the priority ladder picks among them. The human player sees all 155 buildings in the city UI (`city_buildable_helper.gd` iterates `DataLoader.get_all_buildings()` and gates by `city.can_build()`); the AI's mental model is a ~5% slice of that catalog.\n\nResult: AI never builds `library`, `temple`, `colosseum`, `barracks`, `siege_workshop`, `harbor`, `aqueduct`, any of the 62 wonders, or any of the 32 military buildings beyond walls. Production decisions are silently flat across most of the tech tree.\n\nThe fix is to evaluate the full catalog the same way `pick_best_melee` evaluates units: filter by tech / race / resource gates, score by category \u00d7 personality \u00d7 city-state, return best. Catalog comes from the subscription manifest (`p1-41`) so the AI honors the same scope as the human." + "summary": "`mc-ai/tactical/production.rs::ids` hardcodes 8 building/unit IDs (`WARRIOR`, `WORKER`, `FOUNDER`, `WALLS`, `FORGE`, `CASTLE`, `MARKETPLACE`, `GRANARY`) and the priority ladder picks among them. The human player sees all 155 buildings in the city UI (`city_buildable_helper.gd` iterates `DataLoader.get_all_buildings()` and gates by `city.can_build()`); the AI's mental model is a ~5% slice of that catalog.\n\nResult: AI never builds `library`, `temple`, `colosseum`, `barracks`, `siege_workshop`, `harbor`, `aqueduct`, any of the 62 wonders, or any of the 32 military buildings beyond walls. Production decisions are silently flat across most of the tech tree.\n\nThe fix is to evaluate the full catalog the same way `pick_best_melee` evaluates units: filter by tech / race / resource gates, score by category Γ— personality Γ— city-state, return best. Catalog comes from the subscription manifest (`p1-41`) so the AI honors the same scope as the human." }, { "id": "p1-43", - "title": "Building stacking \u2014 per-category upgrade chains (military / science / culture / production / etc.)", + "title": "Building stacking β€” per-category upgrade chains (military / science / culture / production / etc.)", "priority": "p1", "status": "done", "scope": "game1", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "User direction (2026-04-29): \"all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack\".\n\nToday every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing a building on top of an existing one upgrades the slot in place \u2014 `barracks` + another `barracks` build = `infantry` (a stronger military producer). The same primitive applies to every category: science stacks (library \u2192 scriptorium \u2192 academy), culture stacks (monument \u2192 bardic_circle \u2192 great_hall), production stacks (forge \u2192 iron_forge \u2192 grand_forge), etc. This is distinct from the BUILDINGS.md \"Hybrid Merged Structures\" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-category Lv1 \u2192 Lv2 \u2192 Lv3 chains within one slot.\n\nThe existing data already implies category-tier chains via the `tier` + `category` fields:\n\n| Category | Lv1 (no tech) | Lv2 (mid tech) | Lv3+ (late tech) |\n|---|---|---|---|\n| Production | `forge` t1 | (gap \u2014 `iron_forge` doesn't exist) | `dwarf_deep_forge` t3, `tempering_forge` t6, `steam_forge` t7, `adamantine_foundry` t10 |\n| Science | `library` t1 | `university` t3, `observatory` t3 | `academy_of_sciences` t5, `climate_institute` t9 |\n| Culture | `monument` t1 | `great_hall` t3, `gathering_hall` t2 | `ancestor_hall` t10 |\n| Military | `barracks` t1 | (gap \u2014 `infantry` doesn't exist) | `armory` t3, `military_academy` t6, `command_citadel` t10 |\n| Food | `granary` t1 | `mill` t2, `brewery` t2, `watermill` t2 | `great_granary` t2 (wonder) |\n| Defense | `walls` t1 | `watchtower` t1 | `castle` t3 |\n| Wealth | `marketplace` t2, `market` t2 (DUPLICATE) | `guild_hall` t4 | (none) |\n| Religion | `temple` t2 | `temple_of_the_ancestor` t5 (wonder) | (none) |\n\nThe stacking schema makes these chains explicit and queryable. Where a Lv2 successor doesn't exist yet (e.g. `infantry`, `iron_forge`, `scriptorium`), this objective authors the missing intermediates.\n\nThree design questions need user sign-off before authoring:\n\n1. **Successor identity**: is `infantry` a NEW building (needs authoring) or an existing one (e.g. reuse `armory` as the \"barracks Lv2\" slot)?\n2. **Mechanic shape**:\n - **(a) Replacement**: building barracks twice consumes both, slot becomes `infantry`. Original gone.\n - **(b) Levelled**: building stays \"barracks\" but carries a `level: 2` field with stacked effects.\n - **(c) Per-tile**: two barracks on same tile merge (only relevant if `placement_tile_required: true`).\n3. **Schema**: declare on the lower tier (`barracks.json::stacks_into: \"infantry\"`) or on the upper (`infantry.json::requires_existing: \"barracks\"` + `consumes_existing: true`)? The latter keeps the relationship bidirectional readable.\n\nRecommendation: option **(a) Replacement** with declaration on the upper tier (`requires_existing` + `consumes_existing`). Matches civ-style upgrade slots, reads naturally in the city UI (\"Upgrade Barracks \u2192 Infantry\"), avoids per-tile placement complexity for a v1." + "summary": "User direction (2026-04-29): \"all the buildings should be buildable and some buildings can be built on top of each other (double barracks - infantry) ... what about comboing other buildings ... science stack, culture stack\".\n\nToday every building is binary: a city either has it or doesn't. The mechanic the user wants: queueing a building on top of an existing one upgrades the slot in place β€” `barracks` + another `barracks` build = `infantry` (a stronger military producer). The same primitive applies to every category: science stacks (library β†’ scriptorium β†’ academy), culture stacks (monument β†’ bardic_circle β†’ great_hall), production stacks (forge β†’ iron_forge β†’ grand_forge), etc. This is distinct from the BUILDINGS.md \"Hybrid Merged Structures\" mechanic (which combines TWO different buildings + Synthesis tech into a hybrid). Stacking is the simpler primitive: same-category Lv1 β†’ Lv2 β†’ Lv3 chains within one slot.\n\nThe existing data already implies category-tier chains via the `tier` + `category` fields:\n\n| Category | Lv1 (no tech) | Lv2 (mid tech) | Lv3+ (late tech) |\n|---|---|---|---|\n| Production | `forge` t1 | (gap β€” `iron_forge` doesn't exist) | `dwarf_deep_forge` t3, `tempering_forge` t6, `steam_forge` t7, `adamantine_foundry` t10 |\n| Science | `library` t1 | `university` t3, `observatory` t3 | `academy_of_sciences` t5, `climate_institute` t9 |\n| Culture | `monument` t1 | `great_hall` t3, `gathering_hall` t2 | `ancestor_hall` t10 |\n| Military | `barracks` t1 | (gap β€” `infantry` doesn't exist) | `armory` t3, `military_academy` t6, `command_citadel` t10 |\n| Food | `granary` t1 | `mill` t2, `brewery` t2, `watermill` t2 | `great_granary` t2 (wonder) |\n| Defense | `walls` t1 | `watchtower` t1 | `castle` t3 |\n| Wealth | `marketplace` t2, `market` t2 (DUPLICATE) | `guild_hall` t4 | (none) |\n| Religion | `temple` t2 | `temple_of_the_ancestor` t5 (wonder) | (none) |\n\nThe stacking schema makes these chains explicit and queryable. Where a Lv2 successor doesn't exist yet (e.g. `infantry`, `iron_forge`, `scriptorium`), this objective authors the missing intermediates.\n\nThree design questions need user sign-off before authoring:\n\n1. **Successor identity**: is `infantry` a NEW building (needs authoring) or an existing one (e.g. reuse `armory` as the \"barracks Lv2\" slot)?\n2. **Mechanic shape**:\n - **(a) Replacement**: building barracks twice consumes both, slot becomes `infantry`. Original gone.\n - **(b) Levelled**: building stays \"barracks\" but carries a `level: 2` field with stacked effects.\n - **(c) Per-tile**: two barracks on same tile merge (only relevant if `placement_tile_required: true`).\n3. **Schema**: declare on the lower tier (`barracks.json::stacks_into: \"infantry\"`) or on the upper (`infantry.json::requires_existing: \"barracks\"` + `consumes_existing: true`)? The latter keeps the relationship bidirectional readable.\n\nRecommendation: option **(a) Replacement** with declaration on the upper tier (`requires_existing` + `consumes_existing`). Matches civ-style upgrade slots, reads naturally in the city UI (\"Upgrade Barracks β†’ Infantry\"), avoids per-tile placement complexity for a v1." }, { "id": "p1-43b", - "title": "Deep chain authoring \u2014 fill T6/T7/T8/T9/T10 building tiers across the 5 short chains", + "title": "Deep chain authoring β€” fill T6/T7/T8/T9/T10 building tiers across the 5 short chains", "priority": "p1", "status": "done", "scope": "game1", @@ -1167,7 +1167,7 @@ }, { "id": "p1-43c", - "title": "p1-43 follow-ups \u2014 chain ladder authoring, AI stack scoring, city UI upgrade surface, GUT bridge test", + "title": "p1-43 follow-ups β€” chain ladder authoring, AI stack scoring, city UI upgrade surface, GUT bridge test", "priority": "p1", "status": "done", "scope": "game1", @@ -1177,27 +1177,27 @@ }, { "id": "p1-43c-gdext-upgrade-target", - "title": "api-gdext bridge \u2014 GdBuildingRegistry::get_upgrade_target for city UI upgrade surface", + "title": "api-gdext bridge β€” GdBuildingRegistry::get_upgrade_target for city UI upgrade surface", "priority": "p1", "status": "done", "scope": "game1", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "Expose a Rust\u2192Godot bridge that lets the city UI / encyclopedia display the\nupgrade successor of any building without forcing GDScript to walk an inverse\nscan over `DataLoader.get_all_buildings()` (a Rail-3 violation: GDScript would\nbe deriving a join that belongs in the simulation layer).\n\nSource field is `requires_existing` on building JSON (e.g. `academy_of_sciences`\ndeclares `requires_existing: \"university\"` \u2192 university's successor is the\nacademy). The inverse index lives naturally next to `BuildingRegistry` in\n`crates/mc-city/src/building.rs`." + "summary": "Expose a Rustβ†’Godot bridge that lets the city UI / encyclopedia display the\nupgrade successor of any building without forcing GDScript to walk an inverse\nscan over `DataLoader.get_all_buildings()` (a Rail-3 violation: GDScript would\nbe deriving a join that belongs in the simulation layer).\n\nSource field is `requires_existing` on building JSON (e.g. `academy_of_sciences`\ndeclares `requires_existing: \"university\"` β†’ university's successor is the\nacademy). The inverse index lives naturally next to `BuildingRegistry` in\n`crates/mc-city/src/building.rs`." }, { "id": "p1-44", - "title": "Buildings produce units, not the city center \u2014 per-building production queues", + "title": "Buildings produce units, not the city center β€” per-building production queues", "priority": "p1", "status": "done", "scope": "game1", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "User direction (2026-04-29): \"it would make sense to build those units in the appropriate buildings rather than the city center.\"\n\nToday every city has **one** production queue (`city.gd:57 production_queue: Array`) where the player picks \"warrior\", \"barracks\", \"wonder\", anything goes through the same FIFO. Building selection just gates the menu \u2014 the building doesn't actually *do* anything when production happens.\n\nThe user's model \u2014 and the model `PRODUCTION_CHAIN.md` already describes \u2014 is that buildings are **producers**:\n\n> Every citizen is a resource pulled between three competing demands:\n> - **Construction** \u2014 builds new buildings, upgrades existing ones (investment)\n> - **Buildings** \u2014 produces units, research, culture, equipment (output)\n> - **Tiles** \u2014 produces food, raw materials, gold (sustenance)\n\nConcretely:\n- **Barracks** has its own queue producing infantry / armory / military_academy lineage units.\n- **Stable** queues cavalry units.\n- **Siege Workshop** queues siege units.\n- **Library** queues sages / cartographers / engineers (science civilians) and accumulates research per turn.\n- **Temple** queues battle priests and accumulates culture/happiness.\n- **Harbor** queues naval units (and gates them \u2014 see `p1-33`).\n- **Airfield** queues aerial units.\n- **Construction queue** (city-level, distinct from building queues) builds NEW buildings and upgrades existing ones.\n\nEach producer building runs its queue independently per turn. Citizens / production allocation gets split across buildings (per `PRODUCTION_CHAIN.md` \"Three-Way Tension\"). The city's single global queue dies; build-vs-train becomes a structural distinction not a queue-item distinction.\n\nThis is a major engine refactor. Touches: `mc-city`, `mc-turn`, `city.gd`, `city_screen.gd`, `city_buildable_helper.gd`, save schema, AI's `production.rs` (now picks per producer-building, not per city), and the new themed-unit catalog (battle_priest, sage, bard, merchant, etc. \u2014 see `p1-43` open question 1)." + "summary": "User direction (2026-04-29): \"it would make sense to build those units in the appropriate buildings rather than the city center.\"\n\nToday every city has **one** production queue (`city.gd:57 production_queue: Array`) where the player picks \"warrior\", \"barracks\", \"wonder\", anything goes through the same FIFO. Building selection just gates the menu β€” the building doesn't actually *do* anything when production happens.\n\nThe user's model β€” and the model `PRODUCTION_CHAIN.md` already describes β€” is that buildings are **producers**:\n\n> Every citizen is a resource pulled between three competing demands:\n> - **Construction** β€” builds new buildings, upgrades existing ones (investment)\n> - **Buildings** β€” produces units, research, culture, equipment (output)\n> - **Tiles** β€” produces food, raw materials, gold (sustenance)\n\nConcretely:\n- **Barracks** has its own queue producing infantry / armory / military_academy lineage units.\n- **Stable** queues cavalry units.\n- **Siege Workshop** queues siege units.\n- **Library** queues sages / cartographers / engineers (science civilians) and accumulates research per turn.\n- **Temple** queues battle priests and accumulates culture/happiness.\n- **Harbor** queues naval units (and gates them β€” see `p1-33`).\n- **Airfield** queues aerial units.\n- **Construction queue** (city-level, distinct from building queues) builds NEW buildings and upgrades existing ones.\n\nEach producer building runs its queue independently per turn. Citizens / production allocation gets split across buildings (per `PRODUCTION_CHAIN.md` \"Three-Way Tension\"). The city's single global queue dies; build-vs-train becomes a structural distinction not a queue-item distinction.\n\nThis is a major engine refactor. Touches: `mc-city`, `mc-turn`, `city.gd`, `city_screen.gd`, `city_buildable_helper.gd`, save schema, AI's `production.rs` (now picks per producer-building, not per city), and the new themed-unit catalog (battle_priest, sage, bard, merchant, etc. β€” see `p1-43` open question 1)." }, { "id": "p1-44c", - "title": "p1-44 follow-ups \u2014 UI, AI per-building emission, themed roster, GUT, batch", + "title": "p1-44 follow-ups β€” UI, AI per-building emission, themed roster, GUT, batch", "priority": "p1", "status": "done", "scope": "game1", @@ -1214,44 +1214,44 @@ "owner": "simulator-infra", "updated_at": "2026-05-03", "blocked_by": [], - "summary": "Autoplay batches (`tools/autoplay-batch.sh`) run Godot against the installed GDExtension binary\n(`engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so`). When multiple agents or\ndevelopers land Rust changes between batches, the `.so` silently goes stale \u2014 the GDScript wrappers\ncall methods that don't exist yet in the binary, producing cryptic errors like:\n\n```\nInvalid call. Nonexistent function 'process_culture_with_modifier' in base 'RefCounted (City)'.\n```\n\nThis was observed on 2026-04-30 during the p1-29 anti-snowball batch cycle when the culture-port\nteammate's `process_culture_with_modifier` and p1-30's `set_map`/`update_tile` GDExt methods were\nlanded after the last build, causing R12 test failures at T22." + "summary": "Autoplay batches (`tools/autoplay-batch.sh`) run Godot against the installed GDExtension binary\n(`engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so`). When multiple agents or\ndevelopers land Rust changes between batches, the `.so` silently goes stale β€” the GDScript wrappers\ncall methods that don't exist yet in the binary, producing cryptic errors like:\n\n```\nInvalid call. Nonexistent function 'process_culture_with_modifier' in base 'RefCounted (City)'.\n```\n\nThis was observed on 2026-04-30 during the p1-29 anti-snowball batch cycle when the culture-port\nteammate's `process_culture_with_modifier` and p1-30's `set_map`/`update_tile` GDExt methods were\nlanded after the last build, causing R12 test failures at T22." }, { "id": "p1-46", - "title": "Terrain Dimensions Lab \u2014 fix ridginess, bind 149 flora species, add Whittaker plot", + "title": "Terrain Dimensions Lab β€” fix ridginess, bind 149 flora species, add Whittaker plot", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "The Terrain Dimensions Lab at `/world-gen/lab` currently\nclassifies the 17 base biomes and renders cross-tile flora/minerals/fauna\noverlays driven by 4 biome sliders + 3 toggleable overlay layers. Three\nknown gaps:\n\n1. **Ridginess slider has zero effect at default elevation 0.65.** The\n classifier (`terrain.ts:118`) only consults ridginess when\n `elevation > 0.85 \u2227 ridginess > 0.92` for the volcano case. Anywhere\n else, the slider does nothing \u2014 the user verified this on the live\n lab and flagged it.\n2. **The lab loads 0 of 149 flora species.** Trees / shrubs / ground\n cover are generic per terrain. The 149 species in\n `public/resources/ecology/flora/species/*.json` carry rich schema\n (`biomes[]`, `lineage`, `tags` with layer info, `quality_tier`,\n `canopy_contribution`) that the lab ignores entirely.\n3. **No Whittaker T\u00d7P plot inset** \u2014 the user can move sliders but has\n no visual map of where they are in biome space.\n\nThis objective is the **integration / proof surface** of the Wave-E\nterraformer bundle. It must land **after** p1-50, p2-49, p2-50 (Wave A),\np1-47 (Wave B), and p1-48/p1-49 (Wave C). The Whittaker plot, ridginess\nbehaviour, and flora binding all consume the post-refactor axes." + "summary": "The Terrain Dimensions Lab at `/world-gen/lab` currently\nclassifies the 17 base biomes and renders cross-tile flora/minerals/fauna\noverlays driven by 4 biome sliders + 3 toggleable overlay layers. Three\nknown gaps:\n\n1. **Ridginess slider has zero effect at default elevation 0.65.** The\n classifier (`terrain.ts:118`) only consults ridginess when\n `elevation > 0.85 ∧ ridginess > 0.92` for the volcano case. Anywhere\n else, the slider does nothing β€” the user verified this on the live\n lab and flagged it.\n2. **The lab loads 0 of 149 flora species.** Trees / shrubs / ground\n cover are generic per terrain. The 149 species in\n `public/resources/ecology/flora/species/*.json` carry rich schema\n (`biomes[]`, `lineage`, `tags` with layer info, `quality_tier`,\n `canopy_contribution`) that the lab ignores entirely.\n3. **No Whittaker TΓ—P plot inset** β€” the user can move sliders but has\n no visual map of where they are in biome space.\n\nThis objective is the **integration / proof surface** of the Wave-E\nterraformer bundle. It must land **after** p1-50, p2-49, p2-50 (Wave A),\np1-47 (Wave B), and p1-48/p1-49 (Wave C). The Whittaker plot, ridginess\nbehaviour, and flora binding all consume the post-refactor axes." }, { "id": "p1-47", - "title": "River hydrology \u2014 D6 flow analysis, hydraulic erosion, multi-hex lakes, cross-tile rivers", + "title": "River hydrology β€” D6 flow analysis, hydraulic erosion, multi-hex lakes, cross-tile rivers", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "Water bodies in the current map are per-hex terrain types\n(`ocean`, `coast`, `lake`, `inland_sea`) with no connectivity. Real\ngeographic water is **topological**: rivers are DAGs from headwaters to\nocean, lakes are multi-hex polygons, coastlines are polylines along\nland/water borders.\n\nThe data already encodes this in:\n\n- `public/games/age-of-dwarves/data/terrain/terrain_blends.json` \u2014\n `riverside_forest`, `shore`, `cliff`, `bog_edge` ecotones (cross-tile)\n- 10+ fauna species with `river` / `lake` / `wetland` biomes\n (bald_eagle, alligator, otter, kingfisher, beaver, etc.)\n- 4 riparian flora species: `lotus`, `papyrus`, `giant_water_lily`,\n `pioneer_sedge`\n\nBut there is **no rendering pass** that uses the topology. This\nobjective adds:\n\n1. A single hydraulic-erosion iteration that carves valleys before\n drainage analysis (so rivers sit in valleys, not on ridges).\n2. D6 flow-direction analysis on the eroded elevation grid.\n3. Drainage-area accumulation, lake-basin filling.\n4. A cross-tile renderer that draws rivers as bezier paths through\n hex edges and lakes as multi-hex continuous fills.\n\nHydrology computes topology only. Riparian feedback into flora\nselection lives in p1-48; aquatic-domain fauna gating lives in p1-49.\n\n**Wave B delivery (2026-04-30):** All algorithmic bullets (erosion, D6 flow,\nPlanchon-Darboux, Strahler, riparian BFS, coarse-grid path) and bridge surfaces\n(GDExtension + WASM) implemented. TS twin `hydrology.ts` deleted per Rail 1.\nVisual-proof bullet relocated to p1-46 Wave E (lab integration captures the screenshot).\nAll `cargo test -p mc-mapgen` pass. `cargo check --workspace` clean." + "summary": "Water bodies in the current map are per-hex terrain types\n(`ocean`, `coast`, `lake`, `inland_sea`) with no connectivity. Real\ngeographic water is **topological**: rivers are DAGs from headwaters to\nocean, lakes are multi-hex polygons, coastlines are polylines along\nland/water borders.\n\nThe data already encodes this in:\n\n- `public/games/age-of-dwarves/data/terrain/terrain_blends.json` β€”\n `riverside_forest`, `shore`, `cliff`, `bog_edge` ecotones (cross-tile)\n- 10+ fauna species with `river` / `lake` / `wetland` biomes\n (bald_eagle, alligator, otter, kingfisher, beaver, etc.)\n- 4 riparian flora species: `lotus`, `papyrus`, `giant_water_lily`,\n `pioneer_sedge`\n\nBut there is **no rendering pass** that uses the topology. This\nobjective adds:\n\n1. A single hydraulic-erosion iteration that carves valleys before\n drainage analysis (so rivers sit in valleys, not on ridges).\n2. D6 flow-direction analysis on the eroded elevation grid.\n3. Drainage-area accumulation, lake-basin filling.\n4. A cross-tile renderer that draws rivers as bezier paths through\n hex edges and lakes as multi-hex continuous fills.\n\nHydrology computes topology only. Riparian feedback into flora\nselection lives in p1-48; aquatic-domain fauna gating lives in p1-49.\n\n**Wave B delivery (2026-04-30):** All algorithmic bullets (erosion, D6 flow,\nPlanchon-Darboux, Strahler, riparian BFS, coarse-grid path) and bridge surfaces\n(GDExtension + WASM) implemented. TS twin `hydrology.ts` deleted per Rail 1.\nVisual-proof bullet relocated to p1-46 Wave E (lab integration captures the screenshot).\nAll `cargo test -p mc-mapgen` pass. `cargo check --workspace` clean." }, { "id": "p1-48", - "title": "Flora species renderer \u2014 bind 149 species to world-map tile rendering (single source of truth)", + "title": "Flora species renderer β€” bind 149 species to world-map tile rendering (single source of truth)", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "`public/resources/ecology/flora/species/*.json` defines **149 species**\nwith rich schema:\n\n- `biomes[]` \u2014 which terrain types support the species\n- `tags[]` including layer (`layer_canopy` / `layer_understory` /\n `layer_ground` / `layer_fungal`) and structure (`structure_woody` /\n `conifer` / `broadleaf` / `evergreen`)\n- `lineage` \u2014 taxonomic group (`conifers`, `broadleaf_trees`,\n `tropical_broadleaf`, `cacti`, `palms`, `aquatic_plants`,\n `mosses_lichens`, etc.)\n- `quality_tier` (0\u201310), `canopy_contribution`,\n `undergrowth_contribution`, `fungi_contribution`\n- `drought_tolerance`, `fire_resistance`, `growth_rate`\n\nWave C landed the Rust selector, WASM + GDExt bridges, and integration tests.\nThe visual-proof bullets (tooltip, lab integration) are Wave-E work \u2014 relocated\nto p1-46. The TS twin (`floraSpecies.ts`) was deleted; `Lab.tsx` has\n`TODO(p1-46)` markers where the WASM calls will be wired in.\n\n**Single source of truth (Rail 1):** the selector is implemented ONCE\nin `mc-ecology` (Rust), exposed to Godot via GDExtension and to the\ndesign-lab via WASM. NO TypeScript twin." + "summary": "`public/resources/ecology/flora/species/*.json` defines **149 species**\nwith rich schema:\n\n- `biomes[]` β€” which terrain types support the species\n- `tags[]` including layer (`layer_canopy` / `layer_understory` /\n `layer_ground` / `layer_fungal`) and structure (`structure_woody` /\n `conifer` / `broadleaf` / `evergreen`)\n- `lineage` β€” taxonomic group (`conifers`, `broadleaf_trees`,\n `tropical_broadleaf`, `cacti`, `palms`, `aquatic_plants`,\n `mosses_lichens`, etc.)\n- `quality_tier` (0–10), `canopy_contribution`,\n `undergrowth_contribution`, `fungi_contribution`\n- `drought_tolerance`, `fire_resistance`, `growth_rate`\n\nWave C landed the Rust selector, WASM + GDExt bridges, and integration tests.\nThe visual-proof bullets (tooltip, lab integration) are Wave-E work β€” relocated\nto p1-46. The TS twin (`floraSpecies.ts`) was deleted; `Lab.tsx` has\n`TODO(p1-46)` markers where the WASM calls will be wired in.\n\n**Single source of truth (Rail 1):** the selector is implemented ONCE\nin `mc-ecology` (Rust), exposed to Godot via GDExtension and to the\ndesign-lab via WASM. NO TypeScript twin." }, { "id": "p1-49", - "title": "Fauna species renderer \u2014 61 Game-1 species visible on encounter and lair tiles", + "title": "Fauna species renderer β€” 61 Game-1 species visible on encounter and lair tiles", "priority": "p1", "status": "done", "scope": "game1", @@ -1260,77 +1260,77 @@ "blocked_by": [ "p1-47" ], - "summary": "`public/games/age-of-dwarves/data/manifests/fauna.json` whitelists **61\nGame-1 species** with rich JSON schema in\n`public/resources/ecology/fauna/species/*.json`:\n\n- `domain` (land / air / marine / freshwater)\n- `trophic_level` (apex_predator / predator / herbivore / omnivore)\n- `biomes[]`\n- `prey[]` \u2014 actual food-web edges\n- `ecology_tier` (1\u201310 rarity / strength)\n- `forms_lairs` + `lair_type`\n- `lineage` (canines, ursids, cervids, raptors, etc.)\n- `traits[]` including `size_*`\n\nWave C landed the Rust selector with full trophic and domain rules,\n12-cluster glyph table, WASM + GDExt bridges, and integration tests.\nVisual-proof bullets (lair tile silhouette, proof scene screenshot)\nare Wave-E work \u2014 relocated to p1-46. The TS twin (`faunaSpecies.ts`)\nwas deleted; `Lab.tsx` has `TODO(p1-46)` markers." + "summary": "`public/games/age-of-dwarves/data/manifests/fauna.json` whitelists **61\nGame-1 species** with rich JSON schema in\n`public/resources/ecology/fauna/species/*.json`:\n\n- `domain` (land / air / marine / freshwater)\n- `trophic_level` (apex_predator / predator / herbivore / omnivore)\n- `biomes[]`\n- `prey[]` β€” actual food-web edges\n- `ecology_tier` (1–10 rarity / strength)\n- `forms_lairs` + `lair_type`\n- `lineage` (canines, ursids, cervids, raptors, etc.)\n- `traits[]` including `size_*`\n\nWave C landed the Rust selector with full trophic and domain rules,\n12-cluster glyph table, WASM + GDExt bridges, and integration tests.\nVisual-proof bullets (lair tile silhouette, proof scene screenshot)\nare Wave-E work β€” relocated to p1-46. The TS twin (`faunaSpecies.ts`)\nwas deleted; `Lab.tsx` has `TODO(p1-46)` markers." }, { "id": "p1-50", - "title": "Tectonic prepass \u2014 voronoi plates + boundary classification seeding elevation", + "title": "Tectonic prepass β€” voronoi plates + boundary classification seeding elevation", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "The current `mc-mapgen` elevation field is pure fBm noise. Continents\nare amorphous, mountain ranges are noise-shaped blobs, and there is no\ngeological reason for a peak to be where it is. Real continents have\n**plate boundaries**: mountains arc along convergent edges, rifts and\nmid-ocean ridges along divergent edges, transform faults run linear.\n\nThis objective adds a lo-fi tectonic prepass that runs in <500 ms on a\n200\u00d7200 map and biases the existing fBm field. Full multi-step plate\nsimulation (`g2-05-tectonics-lithology`) stays deferred to Game 2; this\nis the cheap version that captures 80% of the visual win.\n\nThe prepass also produces `mountain_proximity` and `coast_proximity`\nfields that p2-49 (rain shadow) and p1-47 (drainage divides) consume\nas first-class inputs." + "summary": "The current `mc-mapgen` elevation field is pure fBm noise. Continents\nare amorphous, mountain ranges are noise-shaped blobs, and there is no\ngeological reason for a peak to be where it is. Real continents have\n**plate boundaries**: mountains arc along convergent edges, rifts and\nmid-ocean ridges along divergent edges, transform faults run linear.\n\nThis objective adds a lo-fi tectonic prepass that runs in <500 ms on a\n200Γ—200 map and biases the existing fBm field. Full multi-step plate\nsimulation (`g2-05-tectonics-lithology`) stays deferred to Game 2; this\nis the cheap version that captures 80% of the visual win.\n\nThe prepass also produces `mountain_proximity` and `coast_proximity`\nfields that p2-49 (rain shadow) and p1-47 (drainage divides) consume\nas first-class inputs." }, { "id": "p1-51", - "title": "Worldgen canonical design docs \u2014 author the spec before any Rust", + "title": "Worldgen canonical design docs β€” author the spec before any Rust", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-04-30", "blocked_by": [], - "summary": "The Terraformer bundle (`p1-46`\u2026`p2-51`) is an 8-objective procedural\nterrain pipeline spanning tectonics, hydrology, climate, ecology, RNG\ndeterminism, world-shape presets, and the design-app Terrain Dimensions\nLab. Per **Rail 1** (`Rust is the simulation source of truth`,\nCLAUDE.md:13) and the project's three-tier doc system (canonical \u2192\nengineering \u2192 JSON, per `.project/designs/README.md`), every Rust crate\nmust mechanically implement an authored canonical specification \u2014 never\nthe reverse.\n\nThis Wave-0 objective authors the **7 canonical design docs** at\n`public/games/age-of-dwarves/docs/terrain/` (and `\u2026/docs/` for ecology\nbinding) that gate all subsequent Wave A\u2013E implementation. Each Rust\ncrate's rustdoc references the canonical doc it implements; bridges\n(`api-gdext`, `api-wasm`) and consumers (Godot, design lab) consume\nwhat the canonical specs declare \u2014 not what someone interpolated.\n\nThe current TypeScript twins (`floraSpecies.ts`, `hydrology.ts`,\n`faunaSpecies.ts`) under `.project/designs/app/src/utils/worldGen/`\nexist precisely because this stage was skipped. Authoring the canonical\ndocs first prevents that recurrence." + "summary": "The Terraformer bundle (`p1-46`…`p2-51`) is an 8-objective procedural\nterrain pipeline spanning tectonics, hydrology, climate, ecology, RNG\ndeterminism, world-shape presets, and the design-app Terrain Dimensions\nLab. Per **Rail 1** (`Rust is the simulation source of truth`,\nCLAUDE.md:13) and the project's three-tier doc system (canonical β†’\nengineering β†’ JSON, per `.project/designs/README.md`), every Rust crate\nmust mechanically implement an authored canonical specification β€” never\nthe reverse.\n\nThis Wave-0 objective authors the **7 canonical design docs** at\n`public/games/age-of-dwarves/docs/terrain/` (and `…/docs/` for ecology\nbinding) that gate all subsequent Wave A–E implementation. Each Rust\ncrate's rustdoc references the canonical doc it implements; bridges\n(`api-gdext`, `api-wasm`) and consumers (Godot, design lab) consume\nwhat the canonical specs declare β€” not what someone interpolated.\n\nThe current TypeScript twins (`floraSpecies.ts`, `hydrology.ts`,\n`faunaSpecies.ts`) under `.project/designs/app/src/utils/worldGen/`\nexist precisely because this stage was skipped. Authoring the canonical\ndocs first prevents that recurrence." }, { "id": "p1-52", - "title": "api-wasm build fix \u2014 unblock WASM bundle for design-lab WASM consumption", + "title": "api-wasm build fix β€” unblock WASM bundle for design-lab WASM consumption", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "**Resolved (2026-05-01).** Workspace `rand` downgraded from `0.9` to `0.8` in\n`[workspace.dependencies]`. This pulls `rand_core 0.6.x` \u2192 `getrandom 0.2.x`\n(known-good on wasm32 with the `js` feature already present in `api-wasm/Cargo.toml`),\nbreaking the dependency chain that forced `getrandom 0.3.x`. WASM build exits 0;\nall three tile_*_json exports confirmed in the `.d.ts`. All tests pass.\n\n`bash src/simulator/build-wasm.sh` previously failed with an upstream\ncompilation error in the `getrandom 0.3.x` line on `wasm32-unknown-unknown`\n(both `0.3.3` and `0.3.4` reference `backends::inner_u32` /\n`backends::inner_u64` symbols that don't exist in the wasm32 backend,\neven with `--cfg getrandom_backend=\"wasm_js\"`).\n\nRoot of the dep chain (from `cargo tree -p magic-civ-physics --target\nwasm32-unknown-unknown -i getrandom@0.3.4`):\n\n```\ngetrandom v0.3.4\n\u2514\u2500\u2500 rand_core v0.9.5\n \u251c\u2500\u2500 rand v0.9.2\n \u2502 \u2514\u2500\u2500 mc-trade v0.1.0\n \u2502 \u251c\u2500\u2500 mc-ai v0.1.0 \u2192 mc-turn \u2192 mc-mapgen \u2192 magic-civ-physics\n \u2502 \u2514\u2500\u2500 mc-turn \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500^\n \u2514\u2500\u2500 rand_chacha v0.9.0 \u2192 rand v0.9.2\n```\n\nWave A pinned the workspace to `rand = \"0.9\"` (in `mc-trade`) which\nforces `rand_core 0.9.x` which forces `getrandom 0.3.x`. The native\nbuild (cargo test on the workspace) compiles fine because native\n`getrandom` doesn't take the broken backends path; only the\n`wasm32-unknown-unknown` target hits it.\n\nThis blocks the **rolling Wave E** plan: every Wave A\u2013D objective\nships Rust + GDExt + WASM bridges, but the WASM bundle can't be built,\nso the design lab can't consume WASM-built selectors and is stuck on\nthe TS twins. By extension, p1-46 (Wave E lab integration) cannot\nland until WASM builds are green.\n\nThis objective is **Wave A.5** \u2014 sits between Wave A (foundation\ncrates) and Waves B\u2013E (consumers). Lands as a small, focused workspace\nfix." + "summary": "**Resolved (2026-05-01).** Workspace `rand` downgraded from `0.9` to `0.8` in\n`[workspace.dependencies]`. This pulls `rand_core 0.6.x` β†’ `getrandom 0.2.x`\n(known-good on wasm32 with the `js` feature already present in `api-wasm/Cargo.toml`),\nbreaking the dependency chain that forced `getrandom 0.3.x`. WASM build exits 0;\nall three tile_*_json exports confirmed in the `.d.ts`. All tests pass.\n\n`bash src/simulator/build-wasm.sh` previously failed with an upstream\ncompilation error in the `getrandom 0.3.x` line on `wasm32-unknown-unknown`\n(both `0.3.3` and `0.3.4` reference `backends::inner_u32` /\n`backends::inner_u64` symbols that don't exist in the wasm32 backend,\neven with `--cfg getrandom_backend=\"wasm_js\"`).\n\nRoot of the dep chain (from `cargo tree -p magic-civ-physics --target\nwasm32-unknown-unknown -i getrandom@0.3.4`):\n\n```\ngetrandom v0.3.4\n└── rand_core v0.9.5\n β”œβ”€β”€ rand v0.9.2\n β”‚ └── mc-trade v0.1.0\n β”‚ β”œβ”€β”€ mc-ai v0.1.0 β†’ mc-turn β†’ mc-mapgen β†’ magic-civ-physics\n β”‚ └── mc-turn ─────────────^\n └── rand_chacha v0.9.0 β†’ rand v0.9.2\n```\n\nWave A pinned the workspace to `rand = \"0.9\"` (in `mc-trade`) which\nforces `rand_core 0.9.x` which forces `getrandom 0.3.x`. The native\nbuild (cargo test on the workspace) compiles fine because native\n`getrandom` doesn't take the broken backends path; only the\n`wasm32-unknown-unknown` target hits it.\n\nThis blocks the **rolling Wave E** plan: every Wave A–D objective\nships Rust + GDExt + WASM bridges, but the WASM bundle can't be built,\nso the design lab can't consume WASM-built selectors and is stuck on\nthe TS twins. By extension, p1-46 (Wave E lab integration) cannot\nland until WASM builds are green.\n\nThis objective is **Wave A.5** β€” sits between Wave A (foundation\ncrates) and Waves B–E (consumers). Lands as a small, focused workspace\nfix." }, { "id": "p1-53", - "title": "Worldgen layer pages \u2014 one playground per canonical doc, mirroring the layered Earth model", + "title": "Worldgen layer pages β€” one playground per canonical doc, mirroring the layered Earth model", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "The 7 canonical worldgen design docs at\n`public/games/age-of-dwarves/docs/terrain/` (plus\n`ECOLOGY_BINDING.md`) describe the layered Earth model the simulator\nimplements: tectonics \u2192 erosion \u2192 hydrology \u2192 climate \u2192 biome \u2192\nflora \u2192 fauna, with RNG and world-shape-presets as cross-cuts. Each\ndoc is a 1-to-1 contract for one Rust crate's behaviour.\n\nThe design app at `.project/designs/app/` currently has ONE\nworldgen-related interactive page (`/world-gen/lab`, soon to\nbe renamed `/world-gen/lab` per `p1-46`). That page tries to render\nEVERYTHING in one canvas, mixing tectonic plates, climate, hydrology,\nflora, and fauna into a single overloaded view.\n\nThis objective splits the design app's worldgen surface into **6\nfocused playground pages** \u2014 one per canonical doc \u2014 plus the existing\nintegration page (which `p1-46` covers separately). Each layer page\nis the **visual + interactive companion** to its canonical doc.\nTogether they make the layered Earth model navigable: a contributor\nopens `/world-gen/tectonics`, sees the canonical doc rendered inline\nbeside a Voronoi-plate canvas driven by the same Rust crate Godot\nuses, moves a slider, and watches the spec come alive." + "summary": "The 7 canonical worldgen design docs at\n`public/games/age-of-dwarves/docs/terrain/` (plus\n`ECOLOGY_BINDING.md`) describe the layered Earth model the simulator\nimplements: tectonics β†’ erosion β†’ hydrology β†’ climate β†’ biome β†’\nflora β†’ fauna, with RNG and world-shape-presets as cross-cuts. Each\ndoc is a 1-to-1 contract for one Rust crate's behaviour.\n\nThe design app at `.project/designs/app/` currently has ONE\nworldgen-related interactive page (`/world-gen/lab`, soon to\nbe renamed `/world-gen/lab` per `p1-46`). That page tries to render\nEVERYTHING in one canvas, mixing tectonic plates, climate, hydrology,\nflora, and fauna into a single overloaded view.\n\nThis objective splits the design app's worldgen surface into **6\nfocused playground pages** β€” one per canonical doc β€” plus the existing\nintegration page (which `p1-46` covers separately). Each layer page\nis the **visual + interactive companion** to its canonical doc.\nTogether they make the layered Earth model navigable: a contributor\nopens `/world-gen/tectonics`, sees the canonical doc rendered inline\nbeside a Voronoi-plate canvas driven by the same Rust crate Godot\nuses, moves a slider, and watches the spec come alive." }, { "id": "p1-54", - "title": "Hex direction-index translation \u2014 Rust pointy-top axial vs design-app flat-top canvas", + "title": "Hex direction-index translation β€” Rust pointy-top axial vs design-app flat-top canvas", "priority": "p1", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "Two related hex-direction bugs surfaced after p1-53 Stage 2 wired the\nHydrology page to live Rust output:\n\n**Bug 1 \u2014 `Hydrology.tsx` flow-arrow rendering (lines 11\u201313):**\n```typescript\nconst FLOW_DX = [1, 1, 0, -1, -1, 0];\nconst FLOW_DY_EVEN = [0, -1, -1, 0, 1, 1];\nconst FLOW_DY_ODD = [0, -1, -1, 0, 1, 1];\n```\n`FLOW_DY_EVEN` and `FLOW_DY_ODD` are identical. For any odd-q offset\ngrid they MUST differ \u2014 that's the entire point of the parity table.\nResult: flow-arrow overlay points to the wrong neighbour for half the\nhexes (whichever parity wasn't hand-checked when the values were\ncopied).\n\n**Bug 2 \u2014 Rust \u2194 TS direction-label mismatch:**\n`mc-core/algorithms/hex.rs::AXIAL_DIRECTIONS` documents directions\n0\u20135 as `E, NE, NW, W, SW, SE` \u2014 the pointy-top axial convention with\nE and W as neighbour directions. But the design-app canvas uses\n**flat-top** geometry (`hexCorners` places corners at angles 0\u00b0, 60\u00b0,\n\u2026, 300\u00b0 \u2192 corners at E and W, edges/neighbours at NE/SE/S/SW/NW/N).\n\nSo when `WasmGrid::tile_hydrology(col, row).flow_out` returns `0`\n(\"E\" in Rust), the design-app canvas should interpret that as the\nflat-top **SE** direction (Rust's \"E\" is `(col+1, row)` axial, which\nin flat-top with odd cols shifted DOWN lands SE of the source cell\nfor even cols).\n\nThe two conventions need a documented translation table that both the\ncanvas and any future TS consumer of WASM hex output reads from.\n\n`hexCanvas.ts::EDGE_CORNERS` and `neighborCoords` were corrected\n2026-05-01 (out of band, before this objective existed) to use\nflat-top semantics \u2014 that fix is correct for design-app-only logic\nthat doesn't cross the WASM bridge. The remaining gap is the\n**bridge-crossing** label translation." + "summary": "Two related hex-direction bugs surfaced after p1-53 Stage 2 wired the\nHydrology page to live Rust output:\n\n**Bug 1 β€” `Hydrology.tsx` flow-arrow rendering (lines 11–13):**\n```typescript\nconst FLOW_DX = [1, 1, 0, -1, -1, 0];\nconst FLOW_DY_EVEN = [0, -1, -1, 0, 1, 1];\nconst FLOW_DY_ODD = [0, -1, -1, 0, 1, 1];\n```\n`FLOW_DY_EVEN` and `FLOW_DY_ODD` are identical. For any odd-q offset\ngrid they MUST differ β€” that's the entire point of the parity table.\nResult: flow-arrow overlay points to the wrong neighbour for half the\nhexes (whichever parity wasn't hand-checked when the values were\ncopied).\n\n**Bug 2 β€” Rust ↔ TS direction-label mismatch:**\n`mc-core/algorithms/hex.rs::AXIAL_DIRECTIONS` documents directions\n0–5 as `E, NE, NW, W, SW, SE` β€” the pointy-top axial convention with\nE and W as neighbour directions. But the design-app canvas uses\n**flat-top** geometry (`hexCorners` places corners at angles 0Β°, 60Β°,\n…, 300Β° β†’ corners at E and W, edges/neighbours at NE/SE/S/SW/NW/N).\n\nSo when `WasmGrid::tile_hydrology(col, row).flow_out` returns `0`\n(\"E\" in Rust), the design-app canvas should interpret that as the\nflat-top **SE** direction (Rust's \"E\" is `(col+1, row)` axial, which\nin flat-top with odd cols shifted DOWN lands SE of the source cell\nfor even cols).\n\nThe two conventions need a documented translation table that both the\ncanvas and any future TS consumer of WASM hex output reads from.\n\n`hexCanvas.ts::EDGE_CORNERS` and `neighborCoords` were corrected\n2026-05-01 (out of band, before this objective existed) to use\nflat-top semantics β€” that fix is correct for design-app-only logic\nthat doesn't cross the WASM bridge. The remaining gap is the\n**bridge-crossing** label translation." }, { "id": "p1-55", - "title": "Tech & Culture domain field \u2014 propagate categorization through Rust, Godot UI, and player analysis", + "title": "Tech & Culture domain field β€” propagate categorization through Rust, Godot UI, and player analysis", "priority": "p1", "status": "done", "scope": "game1", "owner": "simulator-infra", "updated_at": "2026-05-07", "blocked_by": [], - "summary": "The designs app at `.project/designs/app/` now drives `/tech-tree` and\n`/culture-tree` from real JSON data with a 10-domain categorization\n(Military / Economy / Industry / Agriculture / Governance / Culture /\nScience / Exploration / Engineering / Medicine), authored as\n`tech.domain` directly in `public/resources/techs/*.json`. Culture\npolicies use the `pillar` field as their tab axis.\n\nThis objective propagates that categorization through the rest of the\nsystem \u2014 the Rust simulator, the Godot gameplay UI (`knowledge_tree`,\n`tech_tree`, `culture_tree`), and player data analysis surfaces \u2014 so\nevery consumer reads the same Single Source of Truth.\n\nThe shared TreeView component (`.project/designs/app/src/components/tree/`)\nis the reference implementation. Other surfaces should match its UX:\ndomain tab bar, matching-tab-floats-to-top sort, inline unlocks on each\nnode." + "summary": "The designs app at `.project/designs/app/` now drives `/tech-tree` and\n`/culture-tree` from real JSON data with a 10-domain categorization\n(Military / Economy / Industry / Agriculture / Governance / Culture /\nScience / Exploration / Engineering / Medicine), authored as\n`tech.domain` directly in `public/resources/techs/*.json`. Culture\npolicies use the `pillar` field as their tab axis.\n\nThis objective propagates that categorization through the rest of the\nsystem β€” the Rust simulator, the Godot gameplay UI (`knowledge_tree`,\n`tech_tree`, `culture_tree`), and player data analysis surfaces β€” so\nevery consumer reads the same Single Source of Truth.\n\nThe shared TreeView component (`.project/designs/app/src/components/tree/`)\nis the reference implementation. Other surfaces should match its UX:\ndomain tab bar, matching-tab-floats-to-top sort, inline unlocks on each\nnode." }, { "id": "p1-56", - "title": "Civics buildings, Great Works, Specialists, Great People \u2014 wire authored data into Rust + Godot", + "title": "Civics buildings, Great Works, Specialists, Great People β€” wire authored data into Rust + Godot", "priority": "p1", "status": "done", "scope": "game1", @@ -1363,13 +1363,13 @@ }, { "id": "p1-59", - "title": "Hybrid merged structures \u2014 war_academy, assault_citadel, cavalry_corps, gunnery_corps", + "title": "Hybrid merged structures β€” war_academy, assault_citadel, cavalry_corps, gunnery_corps", "priority": "p1", "status": "done", "scope": "game1", "updated_at": "2026-05-07", "blocked_by": [], - "summary": "`public/games/age-of-dwarves/docs/cities/BUILDINGS.md` \"Hybrid Merged Structures\" describes a tier-7-unlocked \"merge two co-located buildings into one hybrid\" mechanic with four named hybrids:\n\n| Merged structure | Requires | Exclusive units |\n|---|---|---|\n| War Academy | Barracks+Rifle Range + Stable+Barding Hall | Dragoon, Mounted Rifleman, Assault Cavalry |\n| Assault Citadel | Barracks+Sword Hall + Siege Workshop+Siege Annex | Siege Breaker, Combat Engineer, Storm Trooper |\n| Cavalry Corps | Stable+Barding Hall + Barracks+Bolt Range | Mounted Archer, Beast Scout, Ram Sniper |\n| Gunnery Corps | Barracks+Rifle Range + Siege Workshop+Powder Annex | Mortar Team, Assault Gunner, Field Artillery |\n\nMultiple prerequisite buildings (Stable, Barding Hall, Siege Annex, Powder Annex) and exclusive units do not exist in data. Game 1 also does not implement co-located building tile slots, master/grandmaster auras, or merge irreversibility \u2014 all called out elsewhere in BUILDINGS.md.\n\nThis is a **post-EA expansion-tier feature**. Filed at p3 to keep the gap visible without implying Game 1 EA depends on it." + "summary": "`public/games/age-of-dwarves/docs/cities/BUILDINGS.md` \"Hybrid Merged Structures\" describes a tier-7-unlocked \"merge two co-located buildings into one hybrid\" mechanic with four named hybrids:\n\n| Merged structure | Requires | Exclusive units |\n|---|---|---|\n| War Academy | Barracks+Rifle Range + Stable+Barding Hall | Dragoon, Mounted Rifleman, Assault Cavalry |\n| Assault Citadel | Barracks+Sword Hall + Siege Workshop+Siege Annex | Siege Breaker, Combat Engineer, Storm Trooper |\n| Cavalry Corps | Stable+Barding Hall + Barracks+Bolt Range | Mounted Archer, Beast Scout, Ram Sniper |\n| Gunnery Corps | Barracks+Rifle Range + Siege Workshop+Powder Annex | Mortar Team, Assault Gunner, Field Artillery |\n\nMultiple prerequisite buildings (Stable, Barding Hall, Siege Annex, Powder Annex) and exclusive units do not exist in data. Game 1 also does not implement co-located building tile slots, master/grandmaster auras, or merge irreversibility β€” all called out elsewhere in BUILDINGS.md.\n\nThis is a **post-EA expansion-tier feature**. Filed at p3 to keep the gap visible without implying Game 1 EA depends on it." }, { "id": "p1-60", @@ -1391,7 +1391,7 @@ "owner": "terraformer", "updated_at": "2026-06-06", "blocked_by": [], - "summary": "`public/games/age-of-dwarves/docs/ecology-audit-gaps.md` lists actionable content gaps in the ecology corpus (149 flora + 581 fauna species, all lineage-tagged). The P0 work (schema + tagging) is done; the P1 work \u2014 fill sparse biomes and close intra-lineage tier holes \u2014 is not. This objective lands the P1 actions only. P2 (enrichment) and P3 (T8-T10 fantasy flora) are explicit non-goals here; they get their own objectives if/when the design team prioritises them." + "summary": "`public/games/age-of-dwarves/docs/ecology-audit-gaps.md` lists actionable content gaps in the ecology corpus (149 flora + 581 fauna species, all lineage-tagged). The P0 work (schema + tagging) is done; the P1 work β€” fill sparse biomes and close intra-lineage tier holes β€” is not. This objective lands the P1 actions only. P2 (enrichment) and P3 (T8-T10 fantasy flora) are explicit non-goals here; they get their own objectives if/when the design team prioritises them." }, { "id": "p2-06", @@ -1402,95 +1402,95 @@ "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "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 \u2014 local `./run export:windows` from a macOS/Linux EDIT host does not yet cross-compile, and no forgejo windows runner is registered. (2) The boots-and-plays end-to-end smoke has not been run against a fresh export archive \u2014 the prior audit's 29MB .x86_64 was discovered this pass to be non-bootable (missing embedded .pck from a concurrent --import race). A clean re-export + AUTO_PLAY 10-turn smoke on a dedicated off-peak runner is the remaining gate. (3) AutoPlay autoload shipping (\u2713 this pass) unblocks (2) but (2) itself is still \u2717.\n\n### macOS scan-inflation fix (2026-04-17, commit f090d28a7)\n\nThe prior 20+ min plum export stall was root-caused to Godot's export scanner walking the entire project tree *before* applying `exclude_filter` \u2014 the three pnpm-managed `public/games/*/guide/node_modules/` symlinks dereferenced into the hoisted store and emitted ~16MB of `_scan_new_dir` warnings. Fixed in `tools/export-single.sh` by rsync-staging the project to `.local/export-staging-/` (excluding `node_modules`, `.local`, `target`, `.git`, `dist`, `.vite*`) before invoking godot. Default-on for macos; opt-in via `EXPORT_STAGED=1` elsewhere; `KEEP_STAGING=1` keeps staging dir for inspection.\n\nEmpirical timing: `./run export:macos p2-06-verify` completed full project scan + 155-step asset reimport in **8.827s** total (two independent runs at 9.287s and 8.827s). Zero `_scan_new_dir` warnings. The only remaining blocker surfaced by that run is a missing Godot 4.6.2 export template (`/Users/natalie/Library/Application Support/Godot/export_templates/4.6.2.stable/macos.zip` \u2014 empty templates dir). Once the template is installed, `archive_boots_and_plays` should close within minutes rather than the 20+ min scan-stall window it previously faced. No codesign/entitlement errors surfaced in verification (those would follow template resolution), so the scan-inflation gate is provably cleared.\n\nStaging approach is documented in `scripts/README.md` \u00a7 \"Export staging (p2-06)\"." + "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, and no forgejo windows runner is registered. (2) The boots-and-plays end-to-end smoke has not been run against a fresh export archive β€” the prior audit's 29MB .x86_64 was discovered this pass to be non-bootable (missing embedded .pck from a concurrent --import race). A clean re-export + AUTO_PLAY 10-turn smoke on a dedicated off-peak runner is the remaining gate. (3) AutoPlay autoload shipping (βœ“ this pass) unblocks (2) but (2) itself is still βœ—.\n\n### macOS scan-inflation fix (2026-04-17, commit f090d28a7)\n\nThe prior 20+ min plum export stall was root-caused to Godot's export scanner walking the entire project tree *before* applying `exclude_filter` β€” the three pnpm-managed `public/games/*/guide/node_modules/` symlinks dereferenced into the hoisted store and emitted ~16MB of `_scan_new_dir` warnings. Fixed in `tools/export-single.sh` by rsync-staging the project to `.local/export-staging-/` (excluding `node_modules`, `.local`, `target`, `.git`, `dist`, `.vite*`) before invoking godot. Default-on for macos; opt-in via `EXPORT_STAGED=1` elsewhere; `KEEP_STAGING=1` keeps staging dir for inspection.\n\nEmpirical timing: `./run export:macos p2-06-verify` completed full project scan + 155-step asset reimport in **8.827s** total (two independent runs at 9.287s and 8.827s). Zero `_scan_new_dir` warnings. The only remaining blocker surfaced by that run is a missing Godot 4.6.2 export template (`/Users/natalie/Library/Application Support/Godot/export_templates/4.6.2.stable/macos.zip` β€” empty templates dir). Once the template is installed, `archive_boots_and_plays` should close within minutes rather than the 20+ min scan-stall window it previously faced. No codesign/entitlement errors surfaced in verification (those would follow template resolution), so the scan-inflation gate is provably cleared.\n\nStaging approach is documented in `scripts/README.md` Β§ \"Export staging (p2-06)\"." }, { "id": "p2-16", - "title": "Audio assets \u2014 in-theme OSS launch pack + source ledger", + "title": "Audio assets β€” in-theme OSS launch pack + source ledger", "priority": "p1", "status": "done", "scope": "game1", "owner": "asset-audio", "updated_at": "2026-06-22", "blocked_by": [], - "summary": "The audio capability shipped as **p0-21** \u2014 `AudioManager`, manifest,\nsignal wiring, volume sliders all work. The schema + categorical\nrouting extension lands as **p2-33** (this objective is `blockedBy`\nthat). What's missing is the actual `.ogg` files plus the source\nledger that proves their licenses are clean.\n\nPer user directive 2026-04-17 the asset work was pulled out of the\noriginal `p1-04` so capability and assets are tracked independently.\nA silent ship is shippable; a broken or licence-tainted audio system\nis not.\n\nThis objective ships **the launch sound pack** assembled from free /\nOSS sources (CC0, CC-BY 3.0/4.0, royalty-free commercial; no\nShareAlike, no NonCommercial). Pack covers ~57 files spanning UI,\nturn cycle, units (categorical melee / ranged / siege / civilian),\nbuildings (categorical civic / production / military / wonder),\nfauna (categorical predator / herbivore / apex), city events,\nresearch, weather, victory. ~50 SFX + 7 music tracks." + "summary": "The audio capability shipped as **p0-21** β€” `AudioManager`, manifest,\nsignal wiring, volume sliders all work. The schema + categorical\nrouting extension lands as **p2-33** (this objective is `blockedBy`\nthat). What's missing is the actual `.ogg` files plus the source\nledger that proves their licenses are clean.\n\nPer user directive 2026-04-17 the asset work was pulled out of the\noriginal `p1-04` so capability and assets are tracked independently.\nA silent ship is shippable; a broken or licence-tainted audio system\nis not.\n\nThis objective ships **the launch sound pack** assembled from free /\nOSS sources (CC0, CC-BY 3.0/4.0, royalty-free commercial; no\nShareAlike, no NonCommercial). Pack covers ~57 files spanning UI,\nturn cycle, units (categorical melee / ranged / siege / civilian),\nbuildings (categorical civic / production / military / wonder),\nfauna (categorical predator / herbivore / apex), city events,\nresearch, weather, victory. ~50 SFX + 7 music tracks." }, { "id": "p2-22", - "title": "Sprite generation pipeline \u2014 runnable end-to-end", + "title": "Sprite generation pipeline β€” runnable end-to-end", "priority": "p1", "status": "done", "scope": "game1", "owner": "asset-sprite", "updated_at": "2026-06-10", "blocked_by": [], - "summary": "Gate-one objective for every other `asset-sprite` child (`p2-23` \u2026 `p2-27`). Before any sprite can legitimately land in `public/games/age-of-dwarves/assets/sprites/`, the `tools/sprite-generation/` pipeline has to run cleanly end-to-end: scan game data \u2192 generate variants via the configured model \u2192 auto-rank via Sonnet vision \u2192 surface in the Theater GUI for human approval \u2192 chroma-key + resize + install with LICENSES.md row written.\n\nSlate is clean (user deleted 7 pre-existing sprites on 2026-04-17 for quality-bar failure; the prompt library and ranker had drifted). This objective closes out the \"pipeline works\" half of the split; actual sprite shipping lives in the downstream children." + "summary": "Gate-one objective for every other `asset-sprite` child (`p2-23` … `p2-27`). Before any sprite can legitimately land in `public/games/age-of-dwarves/assets/sprites/`, the `tools/sprite-generation/` pipeline has to run cleanly end-to-end: scan game data β†’ generate variants via the configured model β†’ auto-rank via Sonnet vision β†’ surface in the Theater GUI for human approval β†’ chroma-key + resize + install with LICENSES.md row written.\n\nSlate is clean (user deleted 7 pre-existing sprites on 2026-04-17 for quality-bar failure; the prompt library and ranker had drifted). This objective closes out the \"pipeline works\" half of the split; actual sprite shipping lives in the downstream children." }, { "id": "p2-28", - "title": "Sprite provenance ledger \u2014 LICENSES.md per-file attribution", + "title": "Sprite provenance ledger β€” LICENSES.md per-file attribution", "priority": "p1", "status": "done", "scope": "game1", "owner": "asset-sprite", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "Every sprite PNG that ships in `public/games/age-of-dwarves/assets/sprites/` must have a corresponding row in `public/games/age-of-dwarves/assets/sprites/LICENSES.md` recording source, license, author, URL, and SHA256. This is a cross-cutting compliance objective that runs continuously alongside the delivery children (`p2-23` \u2026 `p2-27`) \u2014 the ledger is complete exactly when every on-disk sprite has a matching row and every row points at an on-disk file.\n\nCommercial-use compatibility is non-negotiable. AI-generated output must come from a model on the approved list (`juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2`, or current equivalent per CLAUDE.md). Commissioned art must have assigned commercial rights in writing." + "summary": "Every sprite PNG that ships in `public/games/age-of-dwarves/assets/sprites/` must have a corresponding row in `public/games/age-of-dwarves/assets/sprites/LICENSES.md` recording source, license, author, URL, and SHA256. This is a cross-cutting compliance objective that runs continuously alongside the delivery children (`p2-23` … `p2-27`) β€” the ledger is complete exactly when every on-disk sprite has a matching row and every row points at an on-disk file.\n\nCommercial-use compatibility is non-negotiable. AI-generated output must come from a model on the approved list (`juggernaut-xl-v9`, `epicrealism-xl`, `illustrious-xl-v2`, or current equivalent per CLAUDE.md). Commissioned art must have assigned commercial rights in writing." }, { "id": "p2-33", - "title": "Sound system extension \u2014 categorical fallback, variant pools, per-entity routing", + "title": "Sound system extension β€” categorical fallback, variant pools, per-entity routing", "priority": "p1", "status": "done", "scope": "game1", "owner": "asset-audio", "updated_at": "2026-04-27", "blocked_by": [], - "summary": "`AudioManager` (`p0-21`, done) ships 10 SFX events and 6 era-keyed music\ntracks. The current manifest is one stream per id, no variation, no\nfallback chain, and no story for the 91 units / 65 buildings / 600\nfauna species the game ships with \u2014 every entity that ever wants a\ndistinct sound has to add a hand-authored entry, which doesn't scale to\nlaunch.\n\nThis objective extends the manifest schema and `audio_manager.gd` so\nthe asset pack tracked by `p2-16` can land cleanly:\n\n* **Variant pools** \u2014 an entry can list `streams[]` with 2-3 paths;\n the player picks one uniformly to break repetition.\n* **Pitch jitter** \u2014 optional \u00b1X% pitch randomisation per play.\n* **Categorical fallback ladder** \u2014 `play_for_entity(entity_id,\n event_kind)` resolves `.` \u2192 `.` \u2192\n ``, so a fresh unit with no bespoke sound automatically routes\n to its category bucket (`unit.melee.attack`, `building.production.complete`,\n `fauna.apex.roar`, etc.). The category is read from existing JSON\n fields (`unit_type` / `category` / `trophic_class`) \u2014 no new schema\n fields on units / buildings / wilds.\n* **EventBus expansion** \u2014 wire the additional signals that already\n exist on `event_bus.gd` but aren't routed to audio yet\n (`combat_started`, `unit_destroyed`, `unit_promoted`, `city_grew`,\n `city_starved`, `golden_age_started`, `golden_age_ended`,\n `border_expanded`, `culture_researched`, `wild_creature_spawned`,\n `weather_event`, `tech_research_started`).\n\nThis is a **schema-and-code** objective. No `.ogg` files land here \u2014\nthose are `p2-16`'s responsibility, which is `blockedBy: [p2-33]` so\nthe dependency-aware ordering surfaces this work first." + "summary": "`AudioManager` (`p0-21`, done) ships 10 SFX events and 6 era-keyed music\ntracks. The current manifest is one stream per id, no variation, no\nfallback chain, and no story for the 91 units / 65 buildings / 600\nfauna species the game ships with β€” every entity that ever wants a\ndistinct sound has to add a hand-authored entry, which doesn't scale to\nlaunch.\n\nThis objective extends the manifest schema and `audio_manager.gd` so\nthe asset pack tracked by `p2-16` can land cleanly:\n\n* **Variant pools** β€” an entry can list `streams[]` with 2-3 paths;\n the player picks one uniformly to break repetition.\n* **Pitch jitter** β€” optional Β±X% pitch randomisation per play.\n* **Categorical fallback ladder** β€” `play_for_entity(entity_id,\n event_kind)` resolves `.` β†’ `.` β†’\n ``, so a fresh unit with no bespoke sound automatically routes\n to its category bucket (`unit.melee.attack`, `building.production.complete`,\n `fauna.apex.roar`, etc.). The category is read from existing JSON\n fields (`unit_type` / `category` / `trophic_class`) β€” no new schema\n fields on units / buildings / wilds.\n* **EventBus expansion** β€” wire the additional signals that already\n exist on `event_bus.gd` but aren't routed to audio yet\n (`combat_started`, `unit_destroyed`, `unit_promoted`, `city_grew`,\n `city_starved`, `golden_age_started`, `golden_age_ended`,\n `border_expanded`, `culture_researched`, `wild_creature_spawned`,\n `weather_event`, `tech_research_started`).\n\nThis is a **schema-and-code** objective. No `.ogg` files land here β€”\nthose are `p2-16`'s responsibility, which is `blockedBy: [p2-33]` so\nthe dependency-aware ordering surfaces this work first." }, { "id": "p2-80", - "title": "mc-worldsim orchestration crate \u2014 drive the existing worldsim engines in the playable turn", + "title": "mc-worldsim orchestration crate β€” drive the existing worldsim engines in the playable turn", "priority": "p1", "status": "done", "scope": "game1", "updated_at": "2026-06-09", "blocked_by": [], - "summary": "The runtime-worldsim **engines already exist and are tested/benched** \u2014\n`mc-ecology` (Lotka-Volterra fauna `dynamics::tick_populations`, tier\nsuccession, fish stocks, emergence, dispersal), `mc-flora`\n(`dynamics::tick_populations`, `engine::tick_tiers`), `mc-climate`\n(full per-step physics, `physics::step_remaining` via\n`ClimateSim::process_step`), and `mc-compute` (CPU + GPU tick paths).\nThe gap was never \"build the consumer\" \u2014 it is **integration into the\nplayable game turn**.\n\nToday those engines run **only** at worldgen (`mc-mapgen`), in the\nguide-web WASM climate worker (`mc-compute`), and in benches/tests.\n\n> **Premise correction (2026-06-07, verified by grep).** An earlier draft\n> of this Summary claimed *\"the playable game advances turns through\n> `mc_turn::TurnProcessor::step` (wrapped by `GdTurnProcessor`).\"* That is\n> **false**, and it is the load-bearing assumption behind bullet 2. The\n> interactive discrete turn (economy / production / research / cities /\n> combat) runs in **GDScript** \u2014 `turn_manager.gd` \u2192\n> `turn_processor.gd`, delegating to `EconomyScript` / `HappinessScript` /\n> \u2026 (each wrapping a *system-level* Rust crate, e.g. `mc-happiness`, but\n> NOT the unified `mc_turn::TurnProcessor::step`). The Rust\n> `GdTurnProcessor::step` is invoked in the playable tree **only** for\n> isolated fauna-encounter rolls (`rust_fauna_bridge.gd` builds a throwaway\n> `GdGameState` and calls `step_encounters_only` once). `mc_turn::\n> TurnProcessor::step` itself is exercised only by headless benches\n> (`solo_dominion` etc.) and the **not-yet-wired** autoplay action path\n> (`p1-29j`, status *stub*). So nothing in the playable path runs the Rust\n> discrete turn \u2014 which is why bullet 2 (drive the game through\n> `WorldSim::step`) cannot be a call-site swap; it is gated behind the\n> whole-game discrete-turn Rust port. See bullet 2 for the consequence.\n\nThe original architectural design \u2014 a new orchestration crate **above**\n`mc-turn` \u2014 still stands as the Rust-native target; what changed is that\nthe playable game does not yet route through `mc-turn` at all. `mc-turn`\ndepends on only `mc-core` + `mc-state` (and the gameplay crates) \u2014 it\ndeliberately cannot pull `mc-ecology` / `mc-climate` / `mc-mapgen`\n(circular; see the constraint comment that moved with\n`dispatch_world_events`).\n\nThe architectural fix is a new orchestration crate **above** `mc-turn`:\n`mc-worldsim` (`src/simulator/crates/mc-worldsim/`). It owns\n`WorldSim::step(&mut self, state: &mut GameState)` =\n`TurnProcessor::step` + `ClimateSim::process_step` +\n`EcologySim::process_step` + `dispatch_world_events` (moved here from\n`mc-sim`). Per-tile worldsim state is a caller-owned side-structure\n`eco_map: BTreeMap<(u16,u16), TileEcoState>` persisted with the save \u2014\nmirroring the proven `dispatch_world_events` template. The per-turn RNG\nstream is seeded via the new `SeedDomain::WorldsimDynamics = 9`, mixed\nwith the turn index (`mc-worldsim/src/lib.rs:159`).\n\nThis objective is the **foundation/prerequisite** for the whole\nworldsim-runtime build plan: `g2-05`\u2013`g2-10` wire their (existing)\nengines into `WorldSim::step`, and `p2-76` (bunker) / `p2-78` (runtime\nhydrology re-solve) hook the terraforming cascade into the same step." + "summary": "The runtime-worldsim **engines already exist and are tested/benched** β€”\n`mc-ecology` (Lotka-Volterra fauna `dynamics::tick_populations`, tier\nsuccession, fish stocks, emergence, dispersal), `mc-flora`\n(`dynamics::tick_populations`, `engine::tick_tiers`), `mc-climate`\n(full per-step physics, `physics::step_remaining` via\n`ClimateSim::process_step`), and `mc-compute` (CPU + GPU tick paths).\nThe gap was never \"build the consumer\" β€” it is **integration into the\nplayable game turn**.\n\nToday those engines run **only** at worldgen (`mc-mapgen`), in the\nguide-web WASM climate worker (`mc-compute`), and in benches/tests.\n\n> **Premise correction (2026-06-07, verified by grep).** An earlier draft\n> of this Summary claimed *\"the playable game advances turns through\n> `mc_turn::TurnProcessor::step` (wrapped by `GdTurnProcessor`).\"* That is\n> **false**, and it is the load-bearing assumption behind bullet 2. The\n> interactive discrete turn (economy / production / research / cities /\n> combat) runs in **GDScript** β€” `turn_manager.gd` β†’\n> `turn_processor.gd`, delegating to `EconomyScript` / `HappinessScript` /\n> … (each wrapping a *system-level* Rust crate, e.g. `mc-happiness`, but\n> NOT the unified `mc_turn::TurnProcessor::step`). The Rust\n> `GdTurnProcessor::step` is invoked in the playable tree **only** for\n> isolated fauna-encounter rolls (`rust_fauna_bridge.gd` builds a throwaway\n> `GdGameState` and calls `step_encounters_only` once). `mc_turn::\n> TurnProcessor::step` itself is exercised only by headless benches\n> (`solo_dominion` etc.) and the **not-yet-wired** autoplay action path\n> (`p1-29j`, status *stub*). So nothing in the playable path runs the Rust\n> discrete turn β€” which is why bullet 2 (drive the game through\n> `WorldSim::step`) cannot be a call-site swap; it is gated behind the\n> whole-game discrete-turn Rust port. See bullet 2 for the consequence.\n\nThe original architectural design β€” a new orchestration crate **above**\n`mc-turn` β€” still stands as the Rust-native target; what changed is that\nthe playable game does not yet route through `mc-turn` at all. `mc-turn`\ndepends on only `mc-core` + `mc-state` (and the gameplay crates) β€” it\ndeliberately cannot pull `mc-ecology` / `mc-climate` / `mc-mapgen`\n(circular; see the constraint comment that moved with\n`dispatch_world_events`).\n\nThe architectural fix is a new orchestration crate **above** `mc-turn`:\n`mc-worldsim` (`src/simulator/crates/mc-worldsim/`). It owns\n`WorldSim::step(&mut self, state: &mut GameState)` =\n`TurnProcessor::step` + `ClimateSim::process_step` +\n`EcologySim::process_step` + `dispatch_world_events` (moved here from\n`mc-sim`). Per-tile worldsim state is a caller-owned side-structure\n`eco_map: BTreeMap<(u16,u16), TileEcoState>` persisted with the save β€”\nmirroring the proven `dispatch_world_events` template. The per-turn RNG\nstream is seeded via the new `SeedDomain::WorldsimDynamics = 9`, mixed\nwith the turn index (`mc-worldsim/src/lib.rs:159`).\n\nThis objective is the **foundation/prerequisite** for the whole\nworldsim-runtime build plan: `g2-05`–`g2-10` wire their (existing)\nengines into `WorldSim::step`, and `p2-76` (bunker) / `p2-78` (runtime\nhydrology re-solve) hook the terraforming cascade into the same step." }, { "id": "g2-05", - "title": "Tectonics + lithology \u2014 extend the existing prepass/terrain-evolution into a lithology axis", + "title": "Tectonics + lithology β€” extend the existing prepass/terrain-evolution into a lithology axis", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "The geological substrate of the worldsim is **partly built**:\n\n- The one-shot Voronoi tectonic prepass is **done** (`p1-50`, `scope:\n game1`) \u2014 it produces `mountain_proximity` / `coast_proximity`, the\n skeleton every terraformer doc is downstream of (`TECTONICS.md`).\n- Climate-driven terrain evolution **already runs per step**:\n `ClimateSim::step_remaining` calls `check_terrain_evolution`\n (`mc-climate/src/physics.rs:834`), invoked each turn from\n `WorldSim::step` via `climate.process_step`\n (`mc-worldsim/src/lib.rs:155`).\n\nWhat is genuinely missing is the **lithology axis** itself \u2014 per-hex\n`lithology` (granite / basalt / limestone / sandstone / volcanic /\nmetamorphic) derived from plate type + age + erosion, and the\n`plate_type` enrichment of the prepass. This is the upstream input\n`g2-06` (soil) consumes. So this objective is partly \"wire the existing\nprepass + terrain-evolution through the playable step\" and partly\n\"derive the new lithology field.\"" + "summary": "The geological substrate of the worldsim is **partly built**:\n\n- The one-shot Voronoi tectonic prepass is **done** (`p1-50`, `scope:\n game1`) β€” it produces `mountain_proximity` / `coast_proximity`, the\n skeleton every terraformer doc is downstream of (`TECTONICS.md`).\n- Climate-driven terrain evolution **already runs per step**:\n `ClimateSim::step_remaining` calls `check_terrain_evolution`\n (`mc-climate/src/physics.rs:834`), invoked each turn from\n `WorldSim::step` via `climate.process_step`\n (`mc-worldsim/src/lib.rs:155`).\n\nWhat is genuinely missing is the **lithology axis** itself β€” per-hex\n`lithology` (granite / basalt / limestone / sandstone / volcanic /\nmetamorphic) derived from plate type + age + erosion, and the\n`plate_type` enrichment of the prepass. This is the upstream input\n`g2-06` (soil) consumes. So this objective is partly \"wire the existing\nprepass + terrain-evolution through the playable step\" and partly\n\"derive the new lithology field.\"" }, { "id": "g2-06", - "title": "Soil derivation layer \u2014 emergent soil order from rock + climate + slope (the one unbuilt worldsim engine)", + "title": "Soil derivation layer β€” emergent soil order from rock + climate + slope (the one unbuilt worldsim engine)", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Soil derivation engine exists in worldgen (`mc-mapgen::soil::derive_soil` + `assign_soil`,\npure deterministic, 15 tests; Stages 14/15 after lithology in mapgen lib.rs). **Runtime\nworldsim integration, yield/fertility consumption, flora suitability schema, ecology\nindex widen, and supporting data (deposits lithology tags, species soil tolerances,\ntolerance_curves.json) remain missing** (per p2-80 / g2-05 partials). The acceptance\n\"New engine: mc-ecology::soil\" bullet is outdated vs implementation location (contract\nnote in evidence: must stay in mc-mapgen for dep reasons; handoff sig preserved). Unlike\nfauna/flora/climate (runtime-ticked), soil is currently worldgen-only.\n\nSoil is emergent from\n`lithology + mean_T + mean_P + slope + flora_succession_stage + time`\nand outputs one of the 8 USDA soil orders (or a simplified subset):\nMollisol / Spodosol / Oxisol / Aridisol / Histosol / Andisol /\nInceptisol / Entisol.\n\nThe integration surface already has a documented hook: the ecology index\nkey widens from `(substrate, T_band, P_band)` to add `soil_order`\n(`ECOLOGY_BINDING.md` \u00a711) \u2014 the builder accepts an optional `soil_order`\ndimension and falls through to the 3-tuple when `None`." + "summary": "Soil derivation engine exists in worldgen (`mc-mapgen::soil::derive_soil` + `assign_soil`,\npure deterministic, 15 tests; Stages 14/15 after lithology in mapgen lib.rs). **Runtime\nworldsim integration, yield/fertility consumption, flora suitability schema, ecology\nindex widen, and supporting data (deposits lithology tags, species soil tolerances,\ntolerance_curves.json) remain missing** (per p2-80 / g2-05 partials). The acceptance\n\"New engine: mc-ecology::soil\" bullet is outdated vs implementation location (contract\nnote in evidence: must stay in mc-mapgen for dep reasons; handoff sig preserved). Unlike\nfauna/flora/climate (runtime-ticked), soil is currently worldgen-only.\n\nSoil is emergent from\n`lithology + mean_T + mean_P + slope + flora_succession_stage + time`\nand outputs one of the 8 USDA soil orders (or a simplified subset):\nMollisol / Spodosol / Oxisol / Aridisol / Histosol / Andisol /\nInceptisol / Entisol.\n\nThe integration surface already has a documented hook: the ecology index\nkey widens from `(substrate, T_band, P_band)` to add `soil_order`\n(`ECOLOGY_BINDING.md` Β§11) β€” the builder accepts an optional `soil_order`\ndimension and falls through to the 3-tuple when `None`." }, { "id": "g2-08", - "title": "Fauna population dynamics \u2014 confirm the existing LV engine ticks in the playable turn", + "title": "Fauna population dynamics β€” confirm the existing LV engine ticks in the playable turn", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Lotka-Volterra fauna population dynamics (habitat thresholds, carrying\ncapacity, prey-driven growth/collapse, trophic cascade) **already exist\nand are tested** \u2014 the work is integration, persistence, determinism,\nand presentation.\n\n- Engine: `mc-ecology/src/dynamics.rs:273 tick_populations` (CPU; GPU\n path via `gpu_bridge::gpu_tick_populations`).\n- **Already wired** into the ecology step: `EcologySim::process_step`\n calls `dynamics::tick_populations` every tick, with substeps, plus\n emergence / dispersal / fish-stock feedback\n (`mc-ecology/src/engine.rs:276`). `WorldSim::step` already calls\n `process_step` (`mc-worldsim/src/lib.rs:161`).\n\nSpecies data is authored: every fauna carries `habitat_min` /\n`carrying_capacity`, and 373/589 carry `prey[]` (e.g. `grey_wolf.json`:\n`habitat_min 0.2`, `carrying_capacity 0.15`,\n`prey [moose, red_deer, musk_ox, european_rabbit, wild_boar]`).\n\nThe gap is that this engine runs only at worldgen / in benches today; it\ndoes not tick in the **playable** game turn. This objective lands once\n`mc-worldsim` (`p2-80`) drives the step: confirm fauna populations\nrespond per played turn, persist, and are deterministic + visible." + "summary": "Lotka-Volterra fauna population dynamics (habitat thresholds, carrying\ncapacity, prey-driven growth/collapse, trophic cascade) **already exist\nand are tested** β€” the work is integration, persistence, determinism,\nand presentation.\n\n- Engine: `mc-ecology/src/dynamics.rs:273 tick_populations` (CPU; GPU\n path via `gpu_bridge::gpu_tick_populations`).\n- **Already wired** into the ecology step: `EcologySim::process_step`\n calls `dynamics::tick_populations` every tick, with substeps, plus\n emergence / dispersal / fish-stock feedback\n (`mc-ecology/src/engine.rs:276`). `WorldSim::step` already calls\n `process_step` (`mc-worldsim/src/lib.rs:161`).\n\nSpecies data is authored: every fauna carries `habitat_min` /\n`carrying_capacity`, and 373/589 carry `prey[]` (e.g. `grey_wolf.json`:\n`habitat_min 0.2`, `carrying_capacity 0.15`,\n`prey [moose, red_deer, musk_ox, european_rabbit, wild_boar]`).\n\nThe gap is that this engine runs only at worldgen / in benches today; it\ndoes not tick in the **playable** game turn. This objective lands once\n`mc-worldsim` (`p2-80`) drives the step: confirm fauna populations\nrespond per played turn, persist, and are deterministic + visible." }, { "id": "g2-09", - "title": "Flora tolerance-driven selection \u2014 extend the existing flora_select engine with tolerance gating", + "title": "Flora tolerance-driven selection β€” extend the existing flora_select engine with tolerance gating", "priority": "p2", "status": "done", "scope": "game1", @@ -1499,21 +1499,21 @@ "p2-80", "g2-07" ], - "summary": "The flora selection engine **exists** \u2014 `mc-ecology/src/flora_select.rs`\n(and the `mc-flora` species/generation machinery). The work is an\n*extension* of that engine plus its integration into the per-turn step,\nnot a new consumer.\n\nEvery flora species carries six tolerance/contribution axes\n(`drought_tolerance`, `fire_resistance`, `growth_rate`, `quality_tier`,\n`canopy_contribution`, `undergrowth_contribution`, `fungi_contribution`).\nThe current selector consumes `quality_tier \u00d7 _contribution` \u2014 the\n`drought_tolerance` / `fire_resistance` axes are **unused**, so a\ndrought-hardy `creosote_bush` and a drought-sensitive `european_beech`\nget equal weight on an arid tile.\n\nThis objective extends `flora_select` to weight species by tolerance \u00d7\nclimate-stress curves, so the same biome picks visibly different pools by\nclimate zone \u2014 and so the pool a succession step (`g2-07`) can transition\n*into* is climate-appropriate." + "summary": "The flora selection engine **exists** β€” `mc-ecology/src/flora_select.rs`\n(and the `mc-flora` species/generation machinery). The work is an\n*extension* of that engine plus its integration into the per-turn step,\nnot a new consumer.\n\nEvery flora species carries six tolerance/contribution axes\n(`drought_tolerance`, `fire_resistance`, `growth_rate`, `quality_tier`,\n`canopy_contribution`, `undergrowth_contribution`, `fungi_contribution`).\nThe current selector consumes `quality_tier Γ— _contribution` β€” the\n`drought_tolerance` / `fire_resistance` axes are **unused**, so a\ndrought-hardy `creosote_bush` and a drought-sensitive `european_beech`\nget equal weight on an arid tile.\n\nThis objective extends `flora_select` to weight species by tolerance Γ—\nclimate-stress curves, so the same biome picks visibly different pools by\nclimate zone β€” and so the pool a succession step (`g2-07`) can transition\n*into* is climate-appropriate." }, { "id": "g2-10", - "title": "Fauna migration \u2014 wire the existing apply_migrations engine into the per-turn step", + "title": "Fauna migration β€” wire the existing apply_migrations engine into the per-turn step", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Seasonal range shifts and reintroduction propagation (extinct-then-\nrecovering populations re-seeding from neighbours \u2014 the Yellowstone wolf\ncascade in `grey_wolf.json` lore) **already have an engine** \u2014 the work\nis wiring it into the per-turn step plus persistence / determinism /\npresentation.\n\n- Engine: `mc-ecology/src/generation.rs:725 apply_migrations`.\n- **Wired in the per-turn step (static audit 2026-06-23)**: `EcologyEngine::process_step`\n (`mc-ecology/src/engine.rs:359`) calls `compute_migrations` + `apply_migrations`\n (step 3b, carrying-capacity overflow). Invoked from `WorldSim::step` (mc-worldsim/lib.rs:280)\n every playable turn via the `turn_manager.gd` bridge (`EcologyState.tick` \u2192 GdFaunaEcology).\n Core wiring confirmed by source read (migration in process_step step 3b).\n (No change to seasonal/ghost render or p2-79 cascade test yet). (Other deferred engines like\n `evolution::run_evolution` remain out per p2-80.)" + "summary": "Seasonal range shifts and reintroduction propagation (extinct-then-\nrecovering populations re-seeding from neighbours β€” the Yellowstone wolf\ncascade in `grey_wolf.json` lore) **already have an engine** β€” the work\nis wiring it into the per-turn step plus persistence / determinism /\npresentation.\n\n- Engine: `mc-ecology/src/generation.rs:725 apply_migrations`.\n- **Wired in the per-turn step (static audit 2026-06-23)**: `EcologyEngine::process_step`\n (`mc-ecology/src/engine.rs:359`) calls `compute_migrations` + `apply_migrations`\n (step 3b, carrying-capacity overflow). Invoked from `WorldSim::step` (mc-worldsim/lib.rs:280)\n every playable turn via the `turn_manager.gd` bridge (`EcologyState.tick` β†’ GdFaunaEcology).\n Core wiring confirmed by source read (migration in process_step step 3b).\n (No change to seasonal/ghost render or p2-79 cascade test yet). (Other deferred engines like\n `evolution::run_evolution` remain out per p2-80.)" }, { "id": "mc-replay-followup-unit-spawn-events", - "title": "mc-turn unit-spawn event coverage \u2014 every PlayerState.units.push emits UnitCreated / CityUnitCompleted", + "title": "mc-turn unit-spawn event coverage β€” every PlayerState.units.push emits UnitCreated / CityUnitCompleted", "priority": "p2", "status": "done", "scope": "game1", @@ -1524,29 +1524,29 @@ }, { "id": "p1-05-followup-shipwright-batch", - "title": "Shipwright autoplay-batch sign-off \u2014 luxury variance + personality win balance", + "title": "Shipwright autoplay-batch sign-off β€” luxury variance + personality win balance", "priority": "p2", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Two p1-05 acceptance bullets cannot close inside p1-05's JSON-tuning-only\nscope \u2014 both depend on upstream warcouncil work and require a fresh\n10-seed (or 50-game) autoplay batch on apricot once that upstream lands:\n\n1. **Luxury variance \u2265 3 distinct luxuries per seed.** Un-gating\n experiment (`apricot-20260418_062941`) falsified the JSON-tuning hypothesis;\n the true blocker is game length (median domination ~T85), not the tech\n gate. p2-54d landed the `mc-ai::evaluator::score_tech` luxury-unlock\n scoring (211/211 mc-ai tests pass), but full revalidation requires a\n batch after `p0-08` domination tempo lengthens median game past T250.\n\n2. **`personality_win_balance` PASS per p0-02 acceptance.** Warcouncil\n owns the 50-game sample under `p0-02`; Shipwright signs off on the\n batch run + report once warcouncil delivers the personality tunes.\n\nBoth are autoplay-batch sign-off work, not new design or tuning. Tracked\nseparately so p1-05 itself can close on the in-scope JSON tuning that\nalready shipped (pop_peak median 69, worker_improvements min 15, techs\nmedian 39, combats median 808, strategic_gate_rejections 1670)." + "summary": "Two p1-05 acceptance bullets cannot close inside p1-05's JSON-tuning-only\nscope β€” both depend on upstream warcouncil work and require a fresh\n10-seed (or 50-game) autoplay batch on apricot once that upstream lands:\n\n1. **Luxury variance β‰₯ 3 distinct luxuries per seed.** Un-gating\n experiment (`apricot-20260418_062941`) falsified the JSON-tuning hypothesis;\n the true blocker is game length (median domination ~T85), not the tech\n gate. p2-54d landed the `mc-ai::evaluator::score_tech` luxury-unlock\n scoring (211/211 mc-ai tests pass), but full revalidation requires a\n batch after `p0-08` domination tempo lengthens median game past T250.\n\n2. **`personality_win_balance` PASS per p0-02 acceptance.** Warcouncil\n owns the 50-game sample under `p0-02`; Shipwright signs off on the\n batch run + report once warcouncil delivers the personality tunes.\n\nBoth are autoplay-batch sign-off work, not new design or tuning. Tracked\nseparately so p1-05 itself can close on the in-scope JSON tuning that\nalready shipped (pop_peak median 69, worker_improvements min 15, techs\nmedian 39, combats median 808, strategic_gate_rejections 1670)." }, { "id": "p1-38-followup-shipwright-batch", - "title": "p1-38 follow-up \u2014 Shipwright coupled-mode 10-seed regression batch + sign-off", + "title": "p1-38 follow-up β€” Shipwright coupled-mode 10-seed regression batch + sign-off", "priority": "p2", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Carved out of `p1-38-biome-economy-coupling.md` (closed `done` 2026-05-14\non the basis that all Rust + GDScript implementation work has shipped and\nonly the *operational* batch + JSON flip remain). This follow-up tracks\nthe autoplay-batch sign-off that flips\n`public/games/age-of-dwarves/data/balance/ecology_yields.json`\n`fallback_when_dormant: \"static_terrain\" \u2192 \"coupled\"` in main.\n\nThis is owner-locked to Shipwright (see `p1-38` Coordination notes and\nfrontmatter): `game-systems` cannot self-sign-off, and the bullet is an\noperational gate rather than an implementation bullet." + "summary": "Carved out of `p1-38-biome-economy-coupling.md` (closed `done` 2026-05-14\non the basis that all Rust + GDScript implementation work has shipped and\nonly the *operational* batch + JSON flip remain). This follow-up tracks\nthe autoplay-batch sign-off that flips\n`public/games/age-of-dwarves/data/balance/ecology_yields.json`\n`fallback_when_dormant: \"static_terrain\" β†’ \"coupled\"` in main.\n\nThis is owner-locked to Shipwright (see `p1-38` Coordination notes and\nfrontmatter): `game-systems` cannot self-sign-off, and the bullet is an\noperational gate rather than an implementation bullet." }, { "id": "p1-42a", - "title": "Reconcile capture_scoring.rs \u2194 PersonalityPriors \u2014 building_priors field location", + "title": "Reconcile capture_scoring.rs ↔ PersonalityPriors β€” building_priors field location", "priority": "p2", "status": "done", "scope": "game1", @@ -1568,14 +1568,14 @@ }, { "id": "p2-01", - "title": "Minimap \u2014 fog reflection and unit markers", + "title": "Minimap β€” fog reflection and unit markers", "priority": "p2", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "The minimap controller was already complete \u2014 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 \u2192 _on_minimap_click \u2192 cam.center_on(world)`." + "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", @@ -1586,7 +1586,7 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Every interactive HUD control now carries a `tooltip_text` resolved through\n`ThemeVocabulary.lookup(\"tooltip_\")`. 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` \u2014 Godot otherwise swallows hover on\ncontainer-managed Labels, so without this tooltips never fire on\nTurnLabel / EraLabel / Gold / Science / HP / Movement rows." + "summary": "Every interactive HUD control now carries a `tooltip_text` resolved through\n`ThemeVocabulary.lookup(\"tooltip_\")`. 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", @@ -1597,18 +1597,18 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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 \u2014 **Map / City /\nCombat / Menus** \u2014 via the `ACTION_PREFIX_BUCKET` constant. Adding a new\nhotkey means one InputMap action declaration in `project.godot` plus one\n`action_` 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 \u2014 dynamic render still\npicks up the rest). `ui_help` toggles the sheet itself; `ui_cancel` closes\nit." + "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_` 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 \u2014 no hardcoded strings", + "title": "Localization audit β€” no hardcoded strings", "priority": "p2", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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** \u2014 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: done.** All three acceptance bullets pass. The `.tscn` grind\nclosed the remaining 234 hits across 29 files by adding 144 new vocab keys\nto `public/games/age-of-dwarves/vocabulary.json`, adding `%`-unique\naccessors where nodes lacked them, and routing every static label/button\ntext through `ThemeVocabulary.lookup()` at `_ready()` time. Hardcoded\n`text = \"...\"` inspector defaults were stripped. Final validator run:\n`OK: 102 scenes scanned, 0 hardcoded UI strings.`" + "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: done.** All three acceptance bullets pass. The `.tscn` grind\nclosed the remaining 234 hits across 29 files by adding 144 new vocab keys\nto `public/games/age-of-dwarves/vocabulary.json`, adding `%`-unique\naccessors where nodes lacked them, and routing every static label/button\ntext through `ThemeVocabulary.lookup()` at `_ready()` time. Hardcoded\n`text = \"...\"` inspector defaults were stripped. Final validator run:\n`OK: 102 scenes scanned, 0 hardcoded UI strings.`" }, { "id": "p2-05", @@ -1618,7 +1618,7 @@ "scope": "game1", "updated_at": "2026-04-23", "blocked_by": [], - "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 \u22641 second on a 512-tile map with 3 AI opponents mid-game." + "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-06b", @@ -1629,7 +1629,7 @@ "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "Originally framed as \"register a Windows runner\". Re-scoped 2026-04-25 (user pick) to **Option B: cargo-xwin cross-compile from Linux** \u2014 produces MSVC-ABI Windows binaries on the existing Linux runner, no Windows hardware required. Better ABI compatibility than mingw (especially for wgpu's d3d12 backend) and zero hardware cost.\n\nRecipe:\n1. Linux runner installs `cargo-xwin` (one-off): `cargo install cargo-xwin`\n2. Add MSVC target to rustup: `rustup target add x86_64-pc-windows-msvc`\n3. Install `clang` + `lld` (xwin uses these as linker)\n4. `bash src/simulator/build-gdext.sh x86_64-pc-windows-msvc` \u2192 cargo-xwin downloads MS SDK on first run (~1.5GB, cached at `~/.cache/cargo-xwin/`), builds `magic_civ_physics_gdext.dll`, copies to `engine/addons/magic_civ_physics/magic_civ_physics.x86_64.dll`\n5. `bash tools/export-single.sh windows ` \u2192 Godot Linux exports the Windows .exe (cross-export is native to Godot), the script then relocates the .dll into `engine/addons/magic_civ_physics/` next to the binary\n\n`.forgejo/workflows/release.yml` retargeted to `runs-on: [self-hosted, linux, x86_64]` with a \"Setup MSVC cross-toolchain\" step." + "summary": "Originally framed as \"register a Windows runner\". Re-scoped 2026-04-25 (user pick) to **Option B: cargo-xwin cross-compile from Linux** β€” produces MSVC-ABI Windows binaries on the existing Linux runner, no Windows hardware required. Better ABI compatibility than mingw (especially for wgpu's d3d12 backend) and zero hardware cost.\n\nRecipe:\n1. Linux runner installs `cargo-xwin` (one-off): `cargo install cargo-xwin`\n2. Add MSVC target to rustup: `rustup target add x86_64-pc-windows-msvc`\n3. Install `clang` + `lld` (xwin uses these as linker)\n4. `bash src/simulator/build-gdext.sh x86_64-pc-windows-msvc` β†’ cargo-xwin downloads MS SDK on first run (~1.5GB, cached at `~/.cache/cargo-xwin/`), builds `magic_civ_physics_gdext.dll`, copies to `engine/addons/magic_civ_physics/magic_civ_physics.x86_64.dll`\n5. `bash tools/export-single.sh windows ` β†’ Godot Linux exports the Windows .exe (cross-export is native to Godot), the script then relocates the .dll into `engine/addons/magic_civ_physics/` next to the binary\n\n`.forgejo/workflows/release.yml` retargeted to `runs-on: [self-hosted, linux, x86_64]` with a \"Setup MSVC cross-toolchain\" step." }, { "id": "p2-07", @@ -1640,11 +1640,11 @@ "owner": "shipwright", "updated_at": "2026-04-17", "blocked_by": [], - "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 \u2014 engine, Rust crates, fonts, sprite\npipeline, contributors, special thanks \u2014 inside a ScrollContainer. Back\nbutton returns to main menu (also bound to ESC)." + "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 \u2014 colorblind palette + keyboard navigation", + "title": "Accessibility baseline β€” colorblind palette + keyboard navigation", "priority": "p2", "status": "done", "scope": "game1", @@ -1655,14 +1655,14 @@ }, { "id": "p2-09", - "title": "Player guide web app \u2014 builds clean from source", + "title": "Player guide web app β€” builds clean from source", "priority": "p2", "status": "done", "scope": "game1", "owner": "", "updated_at": "2026-04-17", "blocked_by": [], - "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` \u2192 `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 (`../../../` \u2192 `../../../../`); 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 \u2705 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.lan` 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." + "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.lan` 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", @@ -1673,7 +1673,7 @@ "owner": "testwright", "updated_at": "2026-05-14", "blocked_by": [], - "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 \u2014 `git log --oneline` shows zero \"Merge pull\nrequest\" commits, no feature branches, no review gate. Forgejo Actions\n(drone-compatible, files live in `.forgejo/workflows/`) plus a\nself-hosted apricot runner enforces the test suite on every push to\n`main`, matching the two-host workflow (CLAUDE.md: EDIT host commits, RUN\nhost executes \u2014 apricot already is the RUN host).\n\n**State as of 2026-04-17 PM**: the pipeline is operational end-to-end. apricot runner `act_runner v12.8.0` is registered at org scope, systemd user unit is linger-enabled, PATH-patched for fnm-managed node. Every push to main produces a commit status visible on the forge commit page. The Rust workspace (~700 tests, 11 crates) hard-gates the commit; GDExtension now builds as part of CI so Godot's GDScript parse resolves bridge types.\n\nTwo stages remain `continue-on-error: true` (advisory) with in-workflow comments documenting the cleanup owner and un-gating trigger: gdlint (7 structural violations \u2014 file-splitting required) and headless GUT (39 pre-existing failures out of 439). The third advisory stage (autoplay smoke / Stage 9) was un-gated 2026-04-24 after the `_finalize_run()` write race fix. These are tracked via `advisory_backlog_tracked` in the audit block above; un-gating the remaining two is the path to closing this objective from \ud83d\udfe1 to \u2705.\n\n**gdlint status (2026-04-23):** Reduced from 88 \u2192 7 violations via mechanical fixes (class-definitions-order in tile.gd, max-line-length in save_manager/event_bus/auto_play, duplicated-load in auto_play, function-variable-name in turn_processor/turn_processor_helpers). Remaining 7 are structural and cannot be fixed without file-splitting:\n- `max-file-lines`: unit_renderer.gd (634 lines), game_state.gd (556), city.gd (568), auto_play.gd (2238), turn_processor.gd (635), ai_turn_bridge.gd (722)\n- `max-returns`: auto_play.gd `_maybe_prioritize_worker` (>6 return statements \u2014 refactor changes game logic)\nCI Stage 3 remains `continue-on-error: true` until file-splitting is scheduled." + "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. Forgejo Actions\n(drone-compatible, files live in `.forgejo/workflows/`) plus a\nself-hosted apricot runner enforces 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).\n\n**State as of 2026-04-17 PM**: the pipeline is operational end-to-end. apricot runner `act_runner v12.8.0` is registered at org scope, systemd user unit is linger-enabled, PATH-patched for fnm-managed node. Every push to main produces a commit status visible on the forge commit page. The Rust workspace (~700 tests, 11 crates) hard-gates the commit; GDExtension now builds as part of CI so Godot's GDScript parse resolves bridge types.\n\nTwo stages remain `continue-on-error: true` (advisory) with in-workflow comments documenting the cleanup owner and un-gating trigger: gdlint (7 structural violations β€” file-splitting required) and headless GUT (39 pre-existing failures out of 439). The third advisory stage (autoplay smoke / Stage 9) was un-gated 2026-04-24 after the `_finalize_run()` write race fix. These are tracked via `advisory_backlog_tracked` in the audit block above; un-gating the remaining two is the path to closing this objective from 🟑 to βœ….\n\n**gdlint status (2026-04-23):** Reduced from 88 β†’ 7 violations via mechanical fixes (class-definitions-order in tile.gd, max-line-length in save_manager/event_bus/auto_play, duplicated-load in auto_play, function-variable-name in turn_processor/turn_processor_helpers). Remaining 7 are structural and cannot be fixed without file-splitting:\n- `max-file-lines`: unit_renderer.gd (634 lines), game_state.gd (556), city.gd (568), auto_play.gd (2238), turn_processor.gd (635), ai_turn_bridge.gd (722)\n- `max-returns`: auto_play.gd `_maybe_prioritize_worker` (>6 return statements β€” refactor changes game logic)\nCI Stage 3 remains `continue-on-error: true` until file-splitting is scheduled." }, { "id": "p2-10a", @@ -1728,7 +1728,7 @@ "owner": "", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "`test_data_integrity.gd` had two `pending()` stubs and the data had real dangling refs:\n1. **Duplicate IDs**: `public/resources/` is the Games 2/3 master library; `public/games/age-of-dwarves/data/` overrides it for Game 1. DataLoader loads resources first, then game data overwrites \u2014 this is intentional. The test was rewritten to check for intra-pack duplicates only (same ID in two files within the same category directory), which is the correct failure mode to guard against.\n2. **Dangling unlock refs**: 9 tech unlocks referenced buildings/improvements that don't exist in either source (`grand_forge`, `steam_foundry`, `mithril_mine`, `adamantine_vault`, `deep_garden`, `mushroom_farm`, `deep_quarry`, `root_sanctum`, `citadel_of_ages`). These are unported Game 1 content. Unlock entries removed from tech JSON files." + "summary": "`test_data_integrity.gd` had two `pending()` stubs and the data had real dangling refs:\n1. **Duplicate IDs**: `public/resources/` is the Games 2/3 master library; `public/games/age-of-dwarves/data/` overrides it for Game 1. DataLoader loads resources first, then game data overwrites β€” this is intentional. The test was rewritten to check for intra-pack duplicates only (same ID in two files within the same category directory), which is the correct failure mode to guard against.\n2. **Dangling unlock refs**: 9 tech unlocks referenced buildings/improvements that don't exist in either source (`grand_forge`, `steam_foundry`, `mithril_mine`, `adamantine_vault`, `deep_garden`, `mushroom_farm`, `deep_quarry`, `root_sanctum`, `citadel_of_ages`). These are unported Game 1 content. Unlock entries removed from tech JSON files." }, { "id": "p2-10f", @@ -1750,7 +1750,7 @@ "owner": "", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "`test_city_bridge.gd:test_happy_path_enqueue_tick_emits_item_crafted` passes. The fixture JSON in the test already contains `production_cost: 30` nested under the `production` block, which is the exact structure the Rust `GdCity::load_items_json` deserializer expects (`ItemDoc` containing `ProductionDoc`). No changes were needed \u2014 the objective description was stale." + "summary": "`test_city_bridge.gd:test_happy_path_enqueue_tick_emits_item_crafted` passes. The fixture JSON in the test already contains `production_cost: 30` nested under the `production` block, which is the exact structure the Rust `GdCity::load_items_json` deserializer expects (`ItemDoc` containing `ProductionDoc`). No changes were needed β€” the objective description was stale." }, { "id": "p2-10h", @@ -1761,7 +1761,7 @@ "owner": "", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "`test_sprite_renderer.gd` tests `_build_sprite_key(type_id, race_id, sex)` on `UnitRenderer` \u2014 a helper function that was never implemented. 5 tests use it directly. Additionally, `test_cache_populated_after_miss` fails because the expected cache key format doesn't match the actual `DrawHelpers`-managed cache key." + "summary": "`test_sprite_renderer.gd` tests `_build_sprite_key(type_id, race_id, sex)` on `UnitRenderer` β€” a helper function that was never implemented. 5 tests use it directly. Additionally, `test_cache_populated_after_miss` fails because the expected cache key format doesn't match the actual `DrawHelpers`-managed cache key." }, { "id": "p2-10i", @@ -1783,7 +1783,7 @@ "owner": "", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "The two tests were `pending()` placeholders. The production code (`world_map_vision.gd:recalculate_vision()`) is correct \u2014 it demotes visible\u2192stale then re-promotes in-range tiles, which is standard fog-of-war behavior with no return value. The fix was implementing real test bodies using the existing `_expand_vision()` helper, which already counts stale\u2192visible transitions correctly.\n\n- `test_move_scout_expands_known_count`: reveals 37 tiles on first call; after 1-hex move, asserts `< 37` new tiles (already-visible tiles stay visible)\n- `test_seeded_t10_scout_move_reveals_exact_k_tiles`: asserts exactly `2*sight_range+1 = 7` new tiles from a 1-hex axial move (the leading-edge slice formula)" + "summary": "The two tests were `pending()` placeholders. The production code (`world_map_vision.gd:recalculate_vision()`) is correct β€” it demotes visibleβ†’stale then re-promotes in-range tiles, which is standard fog-of-war behavior with no return value. The fix was implementing real test bodies using the existing `_expand_vision()` helper, which already counts staleβ†’visible transitions correctly.\n\n- `test_move_scout_expands_known_count`: reveals 37 tiles on first call; after 1-hex move, asserts `< 37` new tiles (already-visible tiles stay visible)\n- `test_seeded_t10_scout_move_reveals_exact_k_tiles`: asserts exactly `2*sight_range+1 = 7` new tiles from a 1-hex axial move (the leading-edge slice formula)" }, { "id": "p2-10k", @@ -1805,7 +1805,7 @@ "owner": "testwright", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Parent: `p2-10k-gdlint-cleanup.md` (audited and closed 2026-05-14 \u2014 see\n\"Out of scope\" closing note). Eleven `max-file-lines` violations remain on\n`gdlint src/game/engine/src/` and are blocked on a workflow-policy decision\nabove the gdlint-cleanup lane.\n\nThe original p2-10k plan called for inline\n`# gdlint:disable=max-file-lines # tracked: p2-10k` directives per file.\nThat path is **not executable from a Claude session**: the global\n`~/.claude/hooks/enforce-gdscript-quality.sh` PreToolUse hook hard-rejects\nany `gdlint:disable` / `noqa` insertion with `Lint Suppression: fix the\nlint error instead of suppressing it`." + "summary": "Parent: `p2-10k-gdlint-cleanup.md` (audited and closed 2026-05-14 β€” see\n\"Out of scope\" closing note). Eleven `max-file-lines` violations remain on\n`gdlint src/game/engine/src/` and are blocked on a workflow-policy decision\nabove the gdlint-cleanup lane.\n\nThe original p2-10k plan called for inline\n`# gdlint:disable=max-file-lines # tracked: p2-10k` directives per file.\nThat path is **not executable from a Claude session**: the global\n`~/.claude/hooks/enforce-gdscript-quality.sh` PreToolUse hook hard-rejects\nany `gdlint:disable` / `noqa` insertion with `Lint Suppression: fix the\nlint error instead of suppressing it`." }, { "id": "p2-10l", @@ -1816,11 +1816,11 @@ "owner": "testwright", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "As of 2026-05-04, `bash tools/gut-headless.sh` reports 15 failing tests\n(500 total, 477 passing, 8 pending). The CI YAML comment claiming\n\"Hard-gated 2026-04-26 (p2-10b)\" was aspirational; the stage was never\nverified clean on apricot.\n\nParent: p2-10 (regression CI gate). Previous child objectives p2-10c\nthrough p2-10j are all `status: done` \u2014 these 15 failures are new\nregressions introduced after those were closed." + "summary": "As of 2026-05-04, `bash tools/gut-headless.sh` reports 15 failing tests\n(500 total, 477 passing, 8 pending). The CI YAML comment claiming\n\"Hard-gated 2026-04-26 (p2-10b)\" was aspirational; the stage was never\nverified clean on apricot.\n\nParent: p2-10 (regression CI gate). Previous child objectives p2-10c\nthrough p2-10j are all `status: done` β€” these 15 failures are new\nregressions introduced after those were closed." }, { "id": "p2-10l-followup-gdai-set-map", - "title": "GdAiController::set_map \u2014 wire map into tactical state_json", + "title": "GdAiController::set_map β€” wire map into tactical state_json", "priority": "p2", "status": "done", "scope": "game1", @@ -1859,11 +1859,11 @@ "scope": "game1", "updated_at": "2026-05-04", "blocked_by": [], - "summary": "Unit has no serialize()/deserialize() methods \u2014 infusions, equipped_items, promo_ids, keywords and other typed arrays cannot round-trip through SaveManager. City.production_queue is a GDScript-side Array with no serialize path; the Rust-backed City.to_json() does not include it. These gaps were deferred from p2-10f, which narrowed its tests to the Player serialize surface only." + "summary": "Unit has no serialize()/deserialize() methods β€” infusions, equipped_items, promo_ids, keywords and other typed arrays cannot round-trip through SaveManager. City.production_queue is a GDScript-side Array with no serialize path; the Rust-backed City.to_json() does not include it. These gaps were deferred from p2-10f, which narrowed its tests to the Player serialize surface only." }, { "id": "p2-12", - "title": "Install weston on apricot RUN host \u2014 unblock display-server smoke tests", + "title": "Install weston on apricot RUN host β€” unblock display-server smoke tests", "priority": "p2", "status": "done", "scope": "infra", @@ -1874,36 +1874,36 @@ }, { "id": "p2-18", - "title": "Guide web app \u2014 public hosting + deploy pipeline", + "title": "Guide web app β€” public hosting + deploy pipeline", "priority": "p2", "status": "done", "scope": "game1", "owner": "", "updated_at": "2026-06-23", "blocked_by": [], - "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` \u2014 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." + "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 \u2014 dynamic dashboard + missing assets", + "title": "Guide progress report page β€” dynamic dashboard + missing assets", "priority": "p2", "status": "done", "scope": "game1", "owner": "", "updated_at": "2026-04-17", "blocked_by": [], - "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 `\ud83d\udcca Progress Report` at top of About group.\n- 25 new Vitest tests (`assets-detection`, `filter`, `objectives-json`) \u2192 115 total passing; apricot `pnpm build` \u2713, `dist/index.html` exists, bundle 113kB.\n- Incidental fix: orphan `healing_draught` reference in `items/manifest.json` removed (was breaking app bootstrap)." + "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-20", - "title": "Fix simCachePlugin pre-warm worker \u2014 tsx can't resolve @magic-civ/physics-rs through pnpm symlink", + "title": "Fix simCachePlugin pre-warm worker β€” tsx can't resolve @magic-civ/physics-rs through pnpm symlink", "priority": "p2", "status": "done", "scope": "game1", "owner": "tourguide", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "MCP Playwright verification of the dev-guide on plum surfaced a\npre-existing dev-only failure mode in `simCachePlugin`'s pre-warm\nworker. On `pnpm dev` startup, the plugin spawns a tsx subprocess per\nscenario (`base_no_magic`, `volcanic_winter`, `ice_age`, \u2026) which\nstatically imports `@magic-civ/physics-rs` to pre-compute simulation\nframes into Redis. Every spawn fails with:\n\n```\nError: Cannot find package '/Users/natalie/Code/@projects/@magic-civilization/src/packages/engine-ts/node_modules/.local/build/wasm/magic_civ_physics.js'\nimported from .../src/packages/engine-ts/src/runner.ts\ncode: 'ERR_MODULE_NOT_FOUND'\n```\n\n**Root cause:** after p1-11 relocated the wasm-pack output to\n`.local/build/wasm/`, we also cleared `main` and `types` from\n`src/simulator/package.json` because node's default resolver can't\nfollow a `\"main\": \"../../.local/build/wasm/magic_civ_physics.js\"`\npath that escapes the package root \u2014 tsx in particular collapses the\n`..` segments incorrectly through pnpm symlinks and looks up\n`node_modules/@magic-civ/physics-rs/.local/build/wasm/...` (path\nprefix glued rather than resolved). With no `main` at all, tsx falls\nback to guessing `/index.js`, which also doesn't exist.\n\n**User-facing impact:** none. The pre-compute is a dev optimization\n(populates Redis so the `/climate/simulation` route loads pre-rendered\nframes instead of running WASM inline). Vite's main module graph still\nresolves `@magic-civ/physics-rs` correctly via the explicit alias in\n`public/games/age-of-dwarves/guide/vite.config.ts:20`, so the route-\ncoverage e2e exercises the client-WASM fallback path and passes.\nUsers hitting the route with `noGui=true` URL params and a fresh cache\nsee the loading spinner stall at 0% (MCP verify observed this) \u2014 but\nthe normal path (no `noGui`) falls back to worker-mode automatically." + "summary": "MCP Playwright verification of the dev-guide on plum surfaced a\npre-existing dev-only failure mode in `simCachePlugin`'s pre-warm\nworker. On `pnpm dev` startup, the plugin spawns a tsx subprocess per\nscenario (`base_no_magic`, `volcanic_winter`, `ice_age`, …) which\nstatically imports `@magic-civ/physics-rs` to pre-compute simulation\nframes into Redis. Every spawn fails with:\n\n```\nError: Cannot find package '/Users/natalie/Code/@projects/@magic-civilization/src/packages/engine-ts/node_modules/.local/build/wasm/magic_civ_physics.js'\nimported from .../src/packages/engine-ts/src/runner.ts\ncode: 'ERR_MODULE_NOT_FOUND'\n```\n\n**Root cause:** after p1-11 relocated the wasm-pack output to\n`.local/build/wasm/`, we also cleared `main` and `types` from\n`src/simulator/package.json` because node's default resolver can't\nfollow a `\"main\": \"../../.local/build/wasm/magic_civ_physics.js\"`\npath that escapes the package root β€” tsx in particular collapses the\n`..` segments incorrectly through pnpm symlinks and looks up\n`node_modules/@magic-civ/physics-rs/.local/build/wasm/...` (path\nprefix glued rather than resolved). With no `main` at all, tsx falls\nback to guessing `/index.js`, which also doesn't exist.\n\n**User-facing impact:** none. The pre-compute is a dev optimization\n(populates Redis so the `/climate/simulation` route loads pre-rendered\nframes instead of running WASM inline). Vite's main module graph still\nresolves `@magic-civ/physics-rs` correctly via the explicit alias in\n`public/games/age-of-dwarves/guide/vite.config.ts:20`, so the route-\ncoverage e2e exercises the client-WASM fallback path and passes.\nUsers hitting the route with `noGui=true` URL params and a fresh cache\nsee the loading spinner stall at 0% (MCP verify observed this) β€” but\nthe normal path (no `noGui`) falls back to worker-mode automatically." }, { "id": "p2-21", @@ -1914,7 +1914,7 @@ "owner": "tourguide", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "`simCachePlugin` (Vite dev plugin) pre-computes climate-simulator\nscenarios on `pnpm dev` startup and serves the resulting frames over\n`/__sim-cache//{status,frame/}` so\n`/climate/simulation` can load pre-rendered video-like playback\ninstead of running WASM inline for minutes on cold visits. Today this\nis dev-only; on production / `.next.` deploys there is no server to\nrun the plugin, so the frontend falls back to client-WASM \u2014 slow\ncold-start, but works.\n\nThis objective fills the gap: at build time, run each canonical\nscenario headlessly (node + the WASM pkg), emit the same binary frame\nformat `simCachePlugin` serves, and drop the output at\n`dist/__sim-cache//...` so the static deploy serves the\nsame byte streams the dev plugin serves. The frontend doesn't change\n\u2014 it still GETs `/__sim-cache/base_no_magic/status?\u2026` and gets the\nsame shape. The `try_files $uri $uri/` line in the\n`mc.next.black.lan` vhost (p1-15) already passes them through.\n\nSide effect: this closes the bulk of p2-20 for production. The tsx\npnpm-resolve bug remains in dev, but nobody hits the stall path\nbecause in dev the plugin is the fallback (both paths go through\ntsx, both fail identically \u2014 hm, actually, server-mode cold reads\nRedis first; if Redis is warm, no tsx worker is spawned). p2-20\nstill needs its own fix for cold `pnpm dev` runs." + "summary": "`simCachePlugin` (Vite dev plugin) pre-computes climate-simulator\nscenarios on `pnpm dev` startup and serves the resulting frames over\n`/__sim-cache//{status,frame/}` so\n`/climate/simulation` can load pre-rendered video-like playback\ninstead of running WASM inline for minutes on cold visits. Today this\nis dev-only; on production / `.next.` deploys there is no server to\nrun the plugin, so the frontend falls back to client-WASM β€” slow\ncold-start, but works.\n\nThis objective fills the gap: at build time, run each canonical\nscenario headlessly (node + the WASM pkg), emit the same binary frame\nformat `simCachePlugin` serves, and drop the output at\n`dist/__sim-cache//...` so the static deploy serves the\nsame byte streams the dev plugin serves. The frontend doesn't change\nβ€” it still GETs `/__sim-cache/base_no_magic/status?…` and gets the\nsame shape. The `try_files $uri $uri/` line in the\n`mc.next.black.lan` vhost (p1-15) already passes them through.\n\nSide effect: this closes the bulk of p2-20 for production. The tsx\npnpm-resolve bug remains in dev, but nobody hits the stall path\nbecause in dev the plugin is the fallback (both paths go through\ntsx, both fail identically β€” hm, actually, server-mode cold reads\nRedis first; if Redis is warm, no tsx worker is spawned). p2-20\nstill needs its own fix for cold `pnpm dev` runs." }, { "id": "p2-29", @@ -1925,7 +1925,7 @@ "owner": "tourguide", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "The guide already exposes a welcome modal that lets the player pick a race\n(Dwarf in Game 1 \u2014 `CONCRETE_RACES = ['dwarf']`) and a gender, plus a\n`RaceThemeProvider` that merges a per-race/per-gender palette into the\nstyled-components theme. But the three surfaces don't line up:\n\n- **WelcomeModal copy** reads like a settings dialog (\"Settings\", field\n labels) rather than an invitation into the story, so the player's first\n impression is admin-UI-shaped.\n- **HomePage ``** *does* pull the player's race + name via\n `usePreferences()`, but the surrounding `` / `` / ``\n hardcode out-of-scope narrative (\"16 asymmetric races, 5 magic schools\")\n that is Game 2/3 territory and does NOT update when the player picks a\n dwarf leader.\n- **Theme application** \u2014 the palette change from `RaceThemeProvider` fires\n on confirm, but a browser already on the HomePage may not re-derive the\n Hero/Pitch colors in a visually coherent way (Cinzel serif, Dwarf copper\n `#c07040`, etc.). The \"align with welcome\" contract is not exercised.\n\nWhen the player picks **Dwarf + Female** in the modal and clicks Begin,\nall three surfaces should read as one piece: a dwarf-themed guide,\nreferring to the named dwarf leader, in Dwarf scope language (no \"5\nmagic schools\" pitch, no generic cross-race framing)." + "summary": "The guide already exposes a welcome modal that lets the player pick a race\n(Dwarf in Game 1 β€” `CONCRETE_RACES = ['dwarf']`) and a gender, plus a\n`RaceThemeProvider` that merges a per-race/per-gender palette into the\nstyled-components theme. But the three surfaces don't line up:\n\n- **WelcomeModal copy** reads like a settings dialog (\"Settings\", field\n labels) rather than an invitation into the story, so the player's first\n impression is admin-UI-shaped.\n- **HomePage ``** *does* pull the player's race + name via\n `usePreferences()`, but the surrounding `` / `` / ``\n hardcode out-of-scope narrative (\"16 asymmetric races, 5 magic schools\")\n that is Game 2/3 territory and does NOT update when the player picks a\n dwarf leader.\n- **Theme application** β€” the palette change from `RaceThemeProvider` fires\n on confirm, but a browser already on the HomePage may not re-derive the\n Hero/Pitch colors in a visually coherent way (Cinzel serif, Dwarf copper\n `#c07040`, etc.). The \"align with welcome\" contract is not exercised.\n\nWhen the player picks **Dwarf + Female** in the modal and clicks Begin,\nall three surfaces should read as one piece: a dwarf-themed guide,\nreferring to the named dwarf leader, in Dwarf scope language (no \"5\nmagic schools\" pitch, no generic cross-race framing)." }, { "id": "p2-30", @@ -1936,7 +1936,7 @@ "owner": "tourguide", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "An Explore sweep on 2026-04-18 counted ~180 redundant styled-component\ndeclarations across 15 guide pages. Each page declares its own\n`Card`, `CardHeader`, `CardTitle`, `StatsGrid`, `Stat`, `Badge`,\n`SectionLabel`, `Subtitle`, etc. that already exist (or could exist) in\nthe shared `src/packages/guide/src/components/ui/PagePrimitives.tsx`.\nThe duplication:\n\n- Makes theme-token changes N-way rather than one-way (change the Dwarf\n copper accent \u2192 audit 15 pages).\n- Hides inconsistencies. `SpeciesBrowserPage`'s `Card` has 12 px\n border-radius; `BiomeBrowserPage`'s has 10 px. Nobody notices until\n screenshots diverge.\n- Inflates each page's file size toward the 500-LOC cap, forcing\n awkward splits (`BiomeBrowserPage.tsx` is already 355 LoC of styled\n alone, ignoring JSX).\n- Forecloses easy swap-to-Markdown + swap-to-data adoption because\n each page has its own local idiom.\n\nHighest-ROI candidates (counted by Explore):\n\n- `BiomeBrowserPage.tsx:191\u2013355` \u2014 23 custom styled, largest footprint.\n- `SpeciesBrowserPage.tsx:236\u2013330` \u2014 16 custom styled.\n- `MapTypesPage.tsx:9\u2013103` \u2014 Name / StatsGrid / Stat / SectionLabel /\n TopologyCard / TopologyBadge / TopologyDesc restate PagePrimitives.\n- `ExpansionsPage.tsx:10\u2013132`, `TeamPage.tsx:6\u201330` \u2014 near-total overlap.\n- `EpisodeDwarvesPage.tsx` \u2014 inline `INCLUDED_SYSTEMS` array + own\n styled." + "summary": "An Explore sweep on 2026-04-18 counted ~180 redundant styled-component\ndeclarations across 15 guide pages. Each page declares its own\n`Card`, `CardHeader`, `CardTitle`, `StatsGrid`, `Stat`, `Badge`,\n`SectionLabel`, `Subtitle`, etc. that already exist (or could exist) in\nthe shared `src/packages/guide/src/components/ui/PagePrimitives.tsx`.\nThe duplication:\n\n- Makes theme-token changes N-way rather than one-way (change the Dwarf\n copper accent β†’ audit 15 pages).\n- Hides inconsistencies. `SpeciesBrowserPage`'s `Card` has 12 px\n border-radius; `BiomeBrowserPage`'s has 10 px. Nobody notices until\n screenshots diverge.\n- Inflates each page's file size toward the 500-LOC cap, forcing\n awkward splits (`BiomeBrowserPage.tsx` is already 355 LoC of styled\n alone, ignoring JSX).\n- Forecloses easy swap-to-Markdown + swap-to-data adoption because\n each page has its own local idiom.\n\nHighest-ROI candidates (counted by Explore):\n\n- `BiomeBrowserPage.tsx:191–355` β€” 23 custom styled, largest footprint.\n- `SpeciesBrowserPage.tsx:236–330` β€” 16 custom styled.\n- `MapTypesPage.tsx:9–103` β€” Name / StatsGrid / Stat / SectionLabel /\n TopologyCard / TopologyBadge / TopologyDesc restate PagePrimitives.\n- `ExpansionsPage.tsx:10–132`, `TeamPage.tsx:6–30` β€” near-total overlap.\n- `EpisodeDwarvesPage.tsx` β€” inline `INCLUDED_SYSTEMS` array + own\n styled." }, { "id": "p2-31", @@ -1947,7 +1947,7 @@ "owner": "tourguide", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "The Progress Report's Details tab set a precedent: its filter chip\nstate + per-objective modal round-trip through `useSearchParams()`\n(see `progress-report/ObjectivesTab.tsx` + `ProgressReportPage.tsx`).\nBookmarking `?tab=details&filter=partial&objective=p0-01` restores the\nexact view.\n\nThree other browsable pages still keep their filter / tab state in\n`useState`, so deep links don't work and users can't share a filtered\nview by URL:\n\n- `SpeciesBrowserPage.tsx:57` \u2014 role / biome / quality filters\n- `BiomeBrowserPage.tsx:121` \u2014 category filter + potentially a\n highlighted biome\n- `ClimateEventsPage.tsx` \u2014 category tabs" + "summary": "The Progress Report's Details tab set a precedent: its filter chip\nstate + per-objective modal round-trip through `useSearchParams()`\n(see `progress-report/ObjectivesTab.tsx` + `ProgressReportPage.tsx`).\nBookmarking `?tab=details&filter=partial&objective=p0-01` restores the\nexact view.\n\nThree other browsable pages still keep their filter / tab state in\n`useState`, so deep links don't work and users can't share a filtered\nview by URL:\n\n- `SpeciesBrowserPage.tsx:57` β€” role / biome / quality filters\n- `BiomeBrowserPage.tsx:121` β€” category filter + potentially a\n highlighted biome\n- `ClimateEventsPage.tsx` β€” category tabs" }, { "id": "p2-32", @@ -1958,17 +1958,17 @@ "owner": "tourguide", "updated_at": "2026-04-18", "blocked_by": [], - "summary": "Rail #2 in CLAUDE.md says \"JSON game packs are the canonical content\nstore \u2014 neither Rust nor GDScript hardcodes game content.\" Several\nguide pages still violate the spirit of that rule by hand-typing\ndata arrays in `.tsx` files. When the design changes (\"we decided\nit's 20 races, not 16\" \u2014 or \"Arcana tree got merged into Scholarship\"),\nthe fix has to hop four files in TypeScript rather than editing one JSON.\n\nHardcoded arrays the Explore sweep found:\n\n| File:Line | Array | Target JSON |\n|---|---|---|\n| `MapTypesPage.tsx:105\u2013124` | `TOPOLOGY_MODES` \u2014 3 topologies with `{id, label, desc, math, isDefault}` | `public/games/age-of-dwarves/data/map-topologies.json` (new) |\n| `EpisodeDwarvesPage.tsx:8\u201320` | `INCLUDED_SYSTEMS` \u2014 10 system-name strings | `public/games/age-of-dwarves/data/episodes/ep1-systems.json` (new) |\n| `HomePage.tsx:180\u2013210` (FEATURES) | \"What makes this game different\" cards | `public/games/age-of-dwarves/data/homepage-features.json` (new) \u2014 Episode 1 cards first, Episode 2+ behind an EpisodeGate at render time |\n| `progress-report/OverviewTab.tsx:114\u2013165` | Hand-typed \"Coming in v1.0.0\" + \"After Full Release\" roadmap tables | `public/games/age-of-dwarves/data/shipping-roadmap.json` (new) |" + "summary": "Rail #2 in CLAUDE.md says \"JSON game packs are the canonical content\nstore β€” neither Rust nor GDScript hardcodes game content.\" Several\nguide pages still violate the spirit of that rule by hand-typing\ndata arrays in `.tsx` files. When the design changes (\"we decided\nit's 20 races, not 16\" β€” or \"Arcana tree got merged into Scholarship\"),\nthe fix has to hop four files in TypeScript rather than editing one JSON.\n\nHardcoded arrays the Explore sweep found:\n\n| File:Line | Array | Target JSON |\n|---|---|---|\n| `MapTypesPage.tsx:105–124` | `TOPOLOGY_MODES` β€” 3 topologies with `{id, label, desc, math, isDefault}` | `public/games/age-of-dwarves/data/map-topologies.json` (new) |\n| `EpisodeDwarvesPage.tsx:8–20` | `INCLUDED_SYSTEMS` β€” 10 system-name strings | `public/games/age-of-dwarves/data/episodes/ep1-systems.json` (new) |\n| `HomePage.tsx:180–210` (FEATURES) | \"What makes this game different\" cards | `public/games/age-of-dwarves/data/homepage-features.json` (new) β€” Episode 1 cards first, Episode 2+ behind an EpisodeGate at render time |\n| `progress-report/OverviewTab.tsx:114–165` | Hand-typed \"Coming in v1.0.0\" + \"After Full Release\" roadmap tables | `public/games/age-of-dwarves/data/shipping-roadmap.json` (new) |" }, { "id": "p2-35", - "title": "Palace evolution system \u2014 longhouse \u2192 great_hall \u2192 citadel \u2192 grand_citadel + function-shedding", + "title": "Palace evolution system β€” longhouse β†’ great_hall β†’ citadel β†’ grand_citadel + function-shedding", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-05-04", "blocked_by": [], - "summary": "`public/games/age-of-dwarves/docs/cities/BUILDINGS.md` opens with a **Palace Evolution** mechanic. Re-audit (2026-04-27) confirms the partial state:\n\n| Tier | Name | Status |\n|---|---|---|\n| Lv1 | Longhouse | **missing** |\n| Lv2 | Great Hall | \u2713 exists in `resources/buildings/science_culture.json` (without explicit \"palace tier 2\" semantics) |\n| Lv3 | Citadel | **missing** |\n| Lv4 | Grand Citadel | **missing** |\n| (none) | Palace base entity | **missing** \u2014 no `palace.json` anywhere |\n| (adjacent) | Courthouse | \u2713 exists in `resources/buildings/defense_special.json` |\n\nThe doc's bigger claim is the **function-shedding mechanic**: the Palace starts as the entire civilization (one entity doing all functions at reduced efficiency). Researching `Masonry`/`Smelting`/`Scholarship`/`Military Doctrine`/`Arts & Craft`/`Husbandry` *moves* a function out of the Palace into a dedicated building (Mason Lodge / Forge / Library / Barracks / Gathering Hall / Granary) and grants the Palace +10% on remaining functions. None of this mechanic is implemented; nothing in the engine touches a palace concept.\n\nAdjacent gap: BUILDINGS.md \"Building Worker Capacity\" table assigns each palace tier a `max_workers` count. Worker assignment to buildings is not yet a feature \u2014 the dependency is real.\n\nThis objective is the palace tier system + the function-shedding effect math + the worker-cap stub field. It is **NOT** about authoring courthouse or great_hall (both exist)." + "summary": "`public/games/age-of-dwarves/docs/cities/BUILDINGS.md` opens with a **Palace Evolution** mechanic. Re-audit (2026-04-27) confirms the partial state:\n\n| Tier | Name | Status |\n|---|---|---|\n| Lv1 | Longhouse | **missing** |\n| Lv2 | Great Hall | βœ“ exists in `resources/buildings/science_culture.json` (without explicit \"palace tier 2\" semantics) |\n| Lv3 | Citadel | **missing** |\n| Lv4 | Grand Citadel | **missing** |\n| (none) | Palace base entity | **missing** β€” no `palace.json` anywhere |\n| (adjacent) | Courthouse | βœ“ exists in `resources/buildings/defense_special.json` |\n\nThe doc's bigger claim is the **function-shedding mechanic**: the Palace starts as the entire civilization (one entity doing all functions at reduced efficiency). Researching `Masonry`/`Smelting`/`Scholarship`/`Military Doctrine`/`Arts & Craft`/`Husbandry` *moves* a function out of the Palace into a dedicated building (Mason Lodge / Forge / Library / Barracks / Gathering Hall / Granary) and grants the Palace +10% on remaining functions. None of this mechanic is implemented; nothing in the engine touches a palace concept.\n\nAdjacent gap: BUILDINGS.md \"Building Worker Capacity\" table assigns each palace tier a `max_workers` count. Worker assignment to buildings is not yet a feature β€” the dependency is real.\n\nThis objective is the palace tier system + the function-shedding effect math + the worker-cap stub field. It is **NOT** about authoring courthouse or great_hall (both exist)." }, { "id": "p2-36", @@ -1978,29 +1978,29 @@ "scope": "game1", "updated_at": "2026-04-29", "blocked_by": [], - "summary": "After the `p1-31` per-file split, 14 building IDs are defined in **both** `public/resources/buildings/.json` (engine defaults) and `public/games/age-of-dwarves/data/buildings/<...>.json` (Age-of-Dwarves overrides). The data loader iterates `resources/` first then `data/` and overwrites by id \u2014 the data/ definition silently wins. Every duplicate currently differs in `cost`, `tech_required`, or `tier`, so the resources/ side is dead code wherever the override exists.\n\n| ID | resources/ definition | data/ definition (wins) | Drift |\n|---|---|---|---|\n| `barracks` | cost=80, tech=military_doctrine, tier=1 | cost=50, tech=null, tier=1 | tech gate dropped |\n| `forge` | cost=100, tech=smelting, tier=2 | cost=60, tech=null, tier=1 | tech gate dropped, tier dropped |\n| `granary` | cost=60, tech=husbandry, tier=1 | cost=30, tech=null, tier=1 (in stub.json) | tech gate dropped |\n| `library` | cost=75, tech=scholarship, tier=1 | cost=60, tech=null, tier=1 | tech gate dropped |\n| `monument` | cost=40, tech=null, tier=1 | cost=30, tech=null, tier=1 | cost only |\n| `siege_workshop` | cost=120, tech=mathematics, tier=2 | cost=80, tech=siege_craft, tier=2 | different tech |\n| `temple` | cost=90, tech=ancestor_rites, tier=2 | cost=80, tech=null, tier=2 | tech gate dropped |\n| `walls` | cost=75, tech=masonry, tier=1 | cost=70, tech=null, tier=1 | tech gate dropped |\n| `clan_moot_stone` (wonder) | cost=80, tier=1 | cost=180, tier=2 (in mundane_wonders.json) | wonder cost/tier diverged |\n| `covenant_stone` (wonder) | cost=355, tier=4 | cost=600, tier=6 | wonder cost/tier diverged |\n| `grand_observatory` (wonder) | cost=220, tech=astronomy, tier=5 | cost=600, tech=astronomy, tier=6 | wonder cost/tier diverged |\n| `hall_of_ancestors` (wonder) | cost=360, tier=4 | cost=260, tier=3 | wonder cost/tier diverged |\n| `voice_of_ages` (wonder) | cost=780, tier=10 | cost=1200, tier=10 | wonder cost diverged |\n| `world_pillar` (wonder) | cost=540, tier=7 | cost=1040, tech=world_theory, tier=9 | wonder cost/tech/tier diverged |\n\nThe pattern is clear:\n- **Ordinary buildings (8)**: `data/` versions are cheaper and drop the tech gate. Likely a deliberate Age-of-Dwarves \"always-buildable starter\" simplification.\n- **Wonders (6)**: `data/buildings/mundane_wonders.json` versions are heavier-cost / higher-tier, suggesting `mundane_wonders.json` is the actual Game 1 wonder ladder and the per-file `resources/` versions are stale legacy entries." + "summary": "After the `p1-31` per-file split, 14 building IDs are defined in **both** `public/resources/buildings/.json` (engine defaults) and `public/games/age-of-dwarves/data/buildings/<...>.json` (Age-of-Dwarves overrides). The data loader iterates `resources/` first then `data/` and overwrites by id β€” the data/ definition silently wins. Every duplicate currently differs in `cost`, `tech_required`, or `tier`, so the resources/ side is dead code wherever the override exists.\n\n| ID | resources/ definition | data/ definition (wins) | Drift |\n|---|---|---|---|\n| `barracks` | cost=80, tech=military_doctrine, tier=1 | cost=50, tech=null, tier=1 | tech gate dropped |\n| `forge` | cost=100, tech=smelting, tier=2 | cost=60, tech=null, tier=1 | tech gate dropped, tier dropped |\n| `granary` | cost=60, tech=husbandry, tier=1 | cost=30, tech=null, tier=1 (in stub.json) | tech gate dropped |\n| `library` | cost=75, tech=scholarship, tier=1 | cost=60, tech=null, tier=1 | tech gate dropped |\n| `monument` | cost=40, tech=null, tier=1 | cost=30, tech=null, tier=1 | cost only |\n| `siege_workshop` | cost=120, tech=mathematics, tier=2 | cost=80, tech=siege_craft, tier=2 | different tech |\n| `temple` | cost=90, tech=ancestor_rites, tier=2 | cost=80, tech=null, tier=2 | tech gate dropped |\n| `walls` | cost=75, tech=masonry, tier=1 | cost=70, tech=null, tier=1 | tech gate dropped |\n| `clan_moot_stone` (wonder) | cost=80, tier=1 | cost=180, tier=2 (in mundane_wonders.json) | wonder cost/tier diverged |\n| `covenant_stone` (wonder) | cost=355, tier=4 | cost=600, tier=6 | wonder cost/tier diverged |\n| `grand_observatory` (wonder) | cost=220, tech=astronomy, tier=5 | cost=600, tech=astronomy, tier=6 | wonder cost/tier diverged |\n| `hall_of_ancestors` (wonder) | cost=360, tier=4 | cost=260, tier=3 | wonder cost/tier diverged |\n| `voice_of_ages` (wonder) | cost=780, tier=10 | cost=1200, tier=10 | wonder cost diverged |\n| `world_pillar` (wonder) | cost=540, tier=7 | cost=1040, tech=world_theory, tier=9 | wonder cost/tech/tier diverged |\n\nThe pattern is clear:\n- **Ordinary buildings (8)**: `data/` versions are cheaper and drop the tech gate. Likely a deliberate Age-of-Dwarves \"always-buildable starter\" simplification.\n- **Wonders (6)**: `data/buildings/mundane_wonders.json` versions are heavier-cost / higher-tier, suggesting `mundane_wonders.json` is the actual Game 1 wonder ladder and the per-file `resources/` versions are stale legacy entries." }, { "id": "p2-37", - "title": "React calculator UI \u2014 surface flavor, lore, clan_affinity, archetype filter", + "title": "React calculator UI β€” surface flavor, lore, clan_affinity, archetype filter", "priority": "p2", "status": "done", "scope": "game1", "owner": "tourguide", "updated_at": "2026-04-27", "blocked_by": [], - "summary": "The combat calculator at `/calculator` and the permutations matrix at\n`/permutations` (in `.project/designs/app/`) currently show stat blocks\nbut none of the rich metadata p1-34 introduces. The point of writing\nflavor lines and clan affinity isn't to bury them in JSON \u2014 it's to\nmake the design legible in the tool that designers use to balance the\nroster.\n\nThis objective surfaces the new metadata in the React UI:\n\n1. **Calculator unit info card**: flavor as italic epigraph below the\n unit name; lore as collapsible \"Read more\" paragraph; clan_affinity\n as colored clan badges.\n2. **Permutations table**: archetype column (filterable, currently\n inferred); clan_affinity dots in the row.\n3. **Unit browser**: archetype-based grouping (Light Melee / Heavy Melee\n etc. as section headers within Infantry tab) replaces the flat list.\n4. **Hover tooltip**: flavor line shows on hover over a unit row." + "summary": "The combat calculator at `/calculator` and the permutations matrix at\n`/permutations` (in `.project/designs/app/`) currently show stat blocks\nbut none of the rich metadata p1-34 introduces. The point of writing\nflavor lines and clan affinity isn't to bury them in JSON β€” it's to\nmake the design legible in the tool that designers use to balance the\nroster.\n\nThis objective surfaces the new metadata in the React UI:\n\n1. **Calculator unit info card**: flavor as italic epigraph below the\n unit name; lore as collapsible \"Read more\" paragraph; clan_affinity\n as colored clan badges.\n2. **Permutations table**: archetype column (filterable, currently\n inferred); clan_affinity dots in the row.\n3. **Unit browser**: archetype-based grouping (Light Melee / Heavy Melee\n etc. as section headers within Infantry tab) replaces the flat list.\n4. **Hover tooltip**: flavor line shows on hover over a unit row." }, { "id": "p2-38", - "title": "Unit audio_cues stub strings \u2014 selection/move/attack lines for the dwarven roster", + "title": "Unit audio_cues stub strings β€” selection/move/attack lines for the dwarven roster", "priority": "p2", "status": "done", "scope": "game1", "owner": "asset-audio", "updated_at": "2026-04-27", "blocked_by": [], - "summary": "The 50-unit dwarven roster needs in-character audio cue strings \u2014 the\none-liner that plays when a unit is selected, told to move, or ordered\nto attack. AoE/Civ/StarCraft conventions: 2\u20134 lines per cue type, played\nrandomly so repetition doesn't drone.\n\nThis objective lands the **string content** only. Voice acting and audio\nfile generation are downstream (asset-audio team's existing p2-16 audio\npack work). The `audio_cues` field unblocks the audio team to know what\nlines to record / TTS-generate.\n\nEach unit gets:\n```json\n\"audio_cues\": {\n \"select\": [\"...\", \"...\", \"...\"],\n \"move\": [\"...\", \"...\"],\n \"attack\": [\"...\", \"...\"],\n \"death\": [\"...\"]\n}\n```\n\nLines should reflect the unit's identity:\n- Berserker: \"BLOOD!\" / \"I do not need a shield.\"\n- Mountain King: \"The crown stands.\" / \"Speak the names.\" (referring to\n ten thousand clan-name engravings)\n- EMP Trooper: \"What runs on lightning...\" / \"Quietly.\"\n- Shield Bearer: \"Hold.\" / \"We become the place.\" / \"One step. One step.\"" + "summary": "The 50-unit dwarven roster needs in-character audio cue strings β€” the\none-liner that plays when a unit is selected, told to move, or ordered\nto attack. AoE/Civ/StarCraft conventions: 2–4 lines per cue type, played\nrandomly so repetition doesn't drone.\n\nThis objective lands the **string content** only. Voice acting and audio\nfile generation are downstream (asset-audio team's existing p2-16 audio\npack work). The `audio_cues` field unblocks the audio team to know what\nlines to record / TTS-generate.\n\nEach unit gets:\n```json\n\"audio_cues\": {\n \"select\": [\"...\", \"...\", \"...\"],\n \"move\": [\"...\", \"...\"],\n \"attack\": [\"...\", \"...\"],\n \"death\": [\"...\"]\n}\n```\n\nLines should reflect the unit's identity:\n- Berserker: \"BLOOD!\" / \"I do not need a shield.\"\n- Mountain King: \"The crown stands.\" / \"Speak the names.\" (referring to\n ten thousand clan-name engravings)\n- EMP Trooper: \"What runs on lightning...\" / \"Quietly.\"\n- Shield Bearer: \"Hold.\" / \"We become the place.\" / \"One step. One step.\"" }, { "id": "p2-39", @@ -2010,31 +2010,31 @@ "scope": "game1", "updated_at": "2026-04-27", "blocked_by": [], - "summary": "The `chronicle_keeping` culture tech (era_4 oral_tradition pillar) declares\n`unlocks.buildings = [\"chronicle_hall\"]`, but no `chronicle_hall` building file\nexists anywhere in `public/resources/buildings/` or\n`public/games/age-of-dwarves/data/buildings/`. Surfaced by the p3-01 cycle 3\naudit when the era_4 Rune Scribe culture path tried to extend\n`chronicle_hall.enables_units` and discovered it was a phantom.\n\nThe actual building gated on `culture_required: \"chronicle_keeping\"` is\n`bardic_circle` (mundane_wonders.json:102 \u2014 tier 4, era 2 wonder). The phantom\nis a stale unlock string left over from an earlier design.\n\nThis is a pre-existing data integrity bug, separate from p3-01's courier scope." + "summary": "The `chronicle_keeping` culture tech (era_4 oral_tradition pillar) declares\n`unlocks.buildings = [\"chronicle_hall\"]`, but no `chronicle_hall` building file\nexists anywhere in `public/resources/buildings/` or\n`public/games/age-of-dwarves/data/buildings/`. Surfaced by the p3-01 cycle 3\naudit when the era_4 Rune Scribe culture path tried to extend\n`chronicle_hall.enables_units` and discovered it was a phantom.\n\nThe actual building gated on `culture_required: \"chronicle_keeping\"` is\n`bardic_circle` (mundane_wonders.json:102 β€” tier 4, era 2 wonder). The phantom\nis a stale unlock string left over from an earlier design.\n\nThis is a pre-existing data integrity bug, separate from p3-01's courier scope." }, { "id": "p2-43", - "title": "Culture research live-game pipeline \u2014 per-turn GDExt bridge + `culture_researched` emit", + "title": "Culture research live-game pipeline β€” per-turn GDExt bridge + `culture_researched` emit", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-05-07", "blocked_by": [], - "summary": "`EventBus.culture_researched(tradition_id, player_index)` is defined and\n**every downstream consumer is wired** (AudioManager handler, manifest\nentry `culture_researched`, the asset shipped at\n`public/resources/audio/sfx/ui/culture_researched.ogg`). What's missing\nturned out to be deeper than the original framing of this objective: the\n**entire per-turn culture-research path doesn't run in the live game**.\n\n### Trace\n\n- `turn_manager.gd:246` calls `_process_culture(player, game_map)`\n- `turn_processor.gd:360 _process_culture` only handles **border\n expansion** via `city.process_culture_with_modifier()` \u2014 no\n tradition-research accumulator\n- `processor.rs:604 process_culture_research` (Rust mc-turn) **does**\n drive tradition completion via `mc_culture::CultureResearchResult`,\n but it lives in the bench / legacy-headless path, not in the\n GDScript-driven live-game per-turn\n- Tech has a Rust GDExt method `tech_web.process_research(player_dict,\n yields, mult) \u2192 {new_progress, new_researching, completed_tech}` that\n GDScript calls in `turn_processor.gd::_process_research` \u2014 **no\n equivalent exists for culture**\n\nSo in the playable game today: `culture_research_progress` never\nincrements, `researched_traditions` never grows, no completion event\never fires. `p1-28` shipped the UI and the data graph but not the\nruntime accumulator." + "summary": "`EventBus.culture_researched(tradition_id, player_index)` is defined and\n**every downstream consumer is wired** (AudioManager handler, manifest\nentry `culture_researched`, the asset shipped at\n`public/resources/audio/sfx/ui/culture_researched.ogg`). What's missing\nturned out to be deeper than the original framing of this objective: the\n**entire per-turn culture-research path doesn't run in the live game**.\n\n### Trace\n\n- `turn_manager.gd:246` calls `_process_culture(player, game_map)`\n- `turn_processor.gd:360 _process_culture` only handles **border\n expansion** via `city.process_culture_with_modifier()` β€” no\n tradition-research accumulator\n- `processor.rs:604 process_culture_research` (Rust mc-turn) **does**\n drive tradition completion via `mc_culture::CultureResearchResult`,\n but it lives in the bench / legacy-headless path, not in the\n GDScript-driven live-game per-turn\n- Tech has a Rust GDExt method `tech_web.process_research(player_dict,\n yields, mult) β†’ {new_progress, new_researching, completed_tech}` that\n GDScript calls in `turn_processor.gd::_process_research` β€” **no\n equivalent exists for culture**\n\nSo in the playable game today: `culture_research_progress` never\nincrements, `researched_traditions` never grows, no completion event\never fires. `p1-28` shipped the UI and the data graph but not the\nruntime accumulator." }, { "id": "p2-44", - "title": "AI promotion selection \u2014 auto-pick + emit unit_promoted for AI units", + "title": "AI promotion selection β€” auto-pick + emit unit_promoted for AI units", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-05-06", "blocked_by": [], - "summary": "`EventBus.unit_promoted(unit, promotion_id)` is wired end-to-end on the\naudio side: the handler in `AudioManager` plays a UI confirmation chime,\nand `audio.json` ships the `unit_promoted` entry. But the signal is only\nemitted from one place:\n`src/game/engine/scenes/combat/promotion_picker.gd:120` \u2014 the modal the\n**human** uses to pick a promotion.\n\nAI units never go through that picker. Rust has the eligibility check\n(`mc_combat::check_promotion`) and the validation\n(`mc_combat::validate_promotion_choice`) but **no AI selection logic** \u2014\nzero callers of `unit.promote(id)` for AI-owned units, verified by\n`grep -rn '\\.promote(' src/`.\n\nSo in any AI-vs-AI engagement, level-ups happen silently \u2014 the XP bar\nfills but no promotion is ever applied or signalled." + "summary": "`EventBus.unit_promoted(unit, promotion_id)` is wired end-to-end on the\naudio side: the handler in `AudioManager` plays a UI confirmation chime,\nand `audio.json` ships the `unit_promoted` entry. But the signal is only\nemitted from one place:\n`src/game/engine/scenes/combat/promotion_picker.gd:120` β€” the modal the\n**human** uses to pick a promotion.\n\nAI units never go through that picker. Rust has the eligibility check\n(`mc_combat::check_promotion`) and the validation\n(`mc_combat::validate_promotion_choice`) but **no AI selection logic** β€”\nzero callers of `unit.promote(id)` for AI-owned units, verified by\n`grep -rn '\\.promote(' src/`.\n\nSo in any AI-vs-AI engagement, level-ups happen silently β€” the XP bar\nfills but no promotion is ever applied or signalled." }, { "id": "p2-44a", - "title": "DataLoader path mismatch \u2014 `get_promotion(\\\"trees\\\")` returns empty", + "title": "DataLoader path mismatch β€” `get_promotion(\\\"trees\\\")` returns empty", "priority": "p2", "status": "done", "scope": "game1", @@ -2045,7 +2045,7 @@ }, { "id": "p2-44b", - "title": "AI promotion dispatch \u2014 instrumentation pass to identify the silent gate", + "title": "AI promotion dispatch β€” instrumentation pass to identify the silent gate", "priority": "p2", "status": "done", "scope": "game1", @@ -2056,50 +2056,50 @@ }, { "id": "p2-45", - "title": "Player elimination reconciliation \u2014 emit `player_eliminated` on every transition", + "title": "Player elimination reconciliation β€” emit `player_eliminated` on every transition", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-04-30", "blocked_by": [], - "summary": "`EventBus.player_eliminated(player_index)` fires from exactly one place:\n`src/game/engine/src/modules/combat/combat_utils.gd:140` \u2014 when combat\nstrips a player's last city. Today every elimination path goes through\ncombat (the only way to lose your last city in Game 1 is a captor\ntakes it), so the signal does fire in practice.\n\nBut the contract is fragile:\n\n1. `victory_manager._check_elimination_winner()` recomputes `alive_players`\n each turn from `cities.size() > 0 || _has_living_founder(player)` \u2014 it\n knows who's eliminated but only emits `victory_achieved` for the\n sole survivor; it never re-emits per-eliminated-player signals\n2. Future paths (score-floor, surrender, starvation-to-zero-units,\n cultural-encroachment-displacement) won't go through combat_utils \u2014\n they'd silently eliminate without firing the signal\n3. AudioManager's `_on_player_eliminated` is the only consumer that\n currently exists, but more listeners will land (chronicles, achievements,\n replays); they all depend on the signal being authoritative" + "summary": "`EventBus.player_eliminated(player_index)` fires from exactly one place:\n`src/game/engine/src/modules/combat/combat_utils.gd:140` β€” when combat\nstrips a player's last city. Today every elimination path goes through\ncombat (the only way to lose your last city in Game 1 is a captor\ntakes it), so the signal does fire in practice.\n\nBut the contract is fragile:\n\n1. `victory_manager._check_elimination_winner()` recomputes `alive_players`\n each turn from `cities.size() > 0 || _has_living_founder(player)` β€” it\n knows who's eliminated but only emits `victory_achieved` for the\n sole survivor; it never re-emits per-eliminated-player signals\n2. Future paths (score-floor, surrender, starvation-to-zero-units,\n cultural-encroachment-displacement) won't go through combat_utils β€”\n they'd silently eliminate without firing the signal\n3. AudioManager's `_on_player_eliminated` is the only consumer that\n currently exists, but more listeners will land (chronicles, achievements,\n replays); they all depend on the signal being authoritative" }, { "id": "p2-46", - "title": "Past-games archive & replay viewer \u2014 `mc-replay` crate, on-disk archive, projection-based playback", + "title": "Past-games archive & replay viewer β€” `mc-replay` crate, on-disk archive, projection-based playback", "priority": "p2", "status": "done", "scope": "game1-stretch", "owner": "shipwright", "updated_at": "2026-05-07", "blocked_by": [], - "summary": "Persistent local archive of finished games, accessible from the main menu, with three surfaces:\n\n1. **Past Games index** \u2014 card grid (newest first), filters (outcome / map / version / date), per-card actions (Open Summary \u00b7 Watch Replay \u00b7 Rename \u00b7 Export \u00b7 Delete).\n2. **Replay viewer** \u2014 turn-by-turn playback against the live renderer, **projection-based not re-simulated** (reads pre-recorded snapshots + events), scrubber, speed selector, event ticker, optional stats overlay.\n3. **Compare view** \u2014 multi-select 2\u20134 games \u2192 overlapping score-graph + final-standings delta.\n\nFoundational for `p3-06` (statistics screens) and `p3-07` (end-of-game summary), both of which read the same `GameHistory` artefact this objective owns. **Ships first** of the three.\n\nDesign doc: [.project/designs/past-games-replays.md](../designs/past-games-replays.md)." + "summary": "Persistent local archive of finished games, accessible from the main menu, with three surfaces:\n\n1. **Past Games index** β€” card grid (newest first), filters (outcome / map / version / date), per-card actions (Open Summary Β· Watch Replay Β· Rename Β· Export Β· Delete).\n2. **Replay viewer** β€” turn-by-turn playback against the live renderer, **projection-based not re-simulated** (reads pre-recorded snapshots + events), scrubber, speed selector, event ticker, optional stats overlay.\n3. **Compare view** β€” multi-select 2–4 games β†’ overlapping score-graph + final-standings delta.\n\nFoundational for `p3-06` (statistics screens) and `p3-07` (end-of-game summary), both of which read the same `GameHistory` artefact this objective owns. **Ships first** of the three.\n\nDesign doc: [.project/designs/past-games-replays.md](../designs/past-games-replays.md)." }, { "id": "p2-47", - "title": "In-game statistics screens \u2014 Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories)", + "title": "In-game statistics screens β€” Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories)", "priority": "p2", "status": "done", "scope": "game1-stretch", "owner": "shipwright", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Civ-style mid-game statistics modal opened from the HUD info button (or `F9`). Five tabs in one scene, all read-only views over the per-turn `TurnSnapshot` log produced by `mc-replay` (p3-05):\n\n1. **Demographics** \u2014 sortable single-turn table of every met clan.\n2. **Graphs** \u2014 multi-line chart, Y-axis selector (score / pop / cities / army / gold-per-turn / culture-per-turn / tech-count / land-area), X = turn.\n3. **Rankings** \u2014 top-N leaderboard for the selected metric, with trend arrow vs. previous turn.\n4. **Replay** \u2014 in-game preview of the post-game replay viewer (p3-05 surface), scoped to the current game's history.\n5. **Histories** \u2014 per-clan chronicle (founding turn, wars, wonders, eras, leaders).\n\nComposite score is recomputed every turn-end from JSON-driven weights, used for Rankings default and end-game ordering.\n\nDesign doc: [.project/designs/stats-screens.md](../designs/stats-screens.md)." + "summary": "Civ-style mid-game statistics modal opened from the HUD info button (or `F9`). Five tabs in one scene, all read-only views over the per-turn `TurnSnapshot` log produced by `mc-replay` (p3-05):\n\n1. **Demographics** β€” sortable single-turn table of every met clan.\n2. **Graphs** β€” multi-line chart, Y-axis selector (score / pop / cities / army / gold-per-turn / culture-per-turn / tech-count / land-area), X = turn.\n3. **Rankings** β€” top-N leaderboard for the selected metric, with trend arrow vs. previous turn.\n4. **Replay** β€” in-game preview of the post-game replay viewer (p3-05 surface), scoped to the current game's history.\n5. **Histories** β€” per-clan chronicle (founding turn, wars, wonders, eras, leaders).\n\nComposite score is recomputed every turn-end from JSON-driven weights, used for Rankings default and end-game ordering.\n\nDesign doc: [.project/designs/stats-screens.md](../designs/stats-screens.md)." }, { "id": "p2-48", - "title": "End-of-game summary screen \u2014 outcome banner, standings, score graph, awards, timeline, footer actions", + "title": "End-of-game summary screen β€” outcome banner, standings, score graph, awards, timeline, footer actions", "priority": "p2", "status": "done", "scope": "game1-stretch", "owner": "shipwright", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Full-screen summary triggered when the game ends \u2014 by victory condition, last-clan-standing, turn-limit, or player resignation. Replaces the world-map HUD with:\n\n- **Hero strip** \u2014 outcome banner + winning-clan card + player's-clan card (player-second slot stable across victory/defeat).\n- **Section 1 \u2014 Final standings** \u2014 Demographics table from p3-06 frozen at final turn, plus `Outcome` and `Score breakdown` columns.\n- **Section 2 \u2014 Score graph** \u2014 full-game chart from p3-06's Graphs widget with event markers forced on.\n- **Section 3 \u2014 Awards** \u2014 JSON-driven per-category superlatives.\n- **Section 4 \u2014 Timeline** \u2014 Histories from p3-06 with fog lifted (every clan visible).\n- **Footer** \u2014 View Map \u00b7 Watch Replay \u00b7 Save to Archive \u00b7 Export JSON \u00b7 Main Menu.\n\nDesign doc: [.project/designs/end-game-summary.md](../designs/end-game-summary.md)." + "summary": "Full-screen summary triggered when the game ends β€” by victory condition, last-clan-standing, turn-limit, or player resignation. Replaces the world-map HUD with:\n\n- **Hero strip** β€” outcome banner + winning-clan card + player's-clan card (player-second slot stable across victory/defeat).\n- **Section 1 β€” Final standings** β€” Demographics table from p3-06 frozen at final turn, plus `Outcome` and `Score breakdown` columns.\n- **Section 2 β€” Score graph** β€” full-game chart from p3-06's Graphs widget with event markers forced on.\n- **Section 3 β€” Awards** β€” JSON-driven per-category superlatives.\n- **Section 4 β€” Timeline** β€” Histories from p3-06 with fog lifted (every clan visible).\n- **Footer** β€” View Map Β· Watch Replay Β· Save to Archive Β· Export JSON Β· Main Menu.\n\nDesign doc: [.project/designs/end-game-summary.md](../designs/end-game-summary.md)." }, { "id": "p2-48a", - "title": "End-of-game summary \u2014 GUT tests + headless proof scene", + "title": "End-of-game summary β€” GUT tests + headless proof scene", "priority": "p2", "status": "done", "scope": "game1-stretch", @@ -2110,14 +2110,14 @@ }, { "id": "p2-49", - "title": "Climate axes refactor \u2014 latitude + continentality + zonal winds as first-class per-hex inputs", + "title": "Climate axes refactor β€” latitude + continentality + zonal winds as first-class per-hex inputs", "priority": "p2", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-04-30", "blocked_by": [], - "summary": "`mc-mapgen::sampleCell` (and the design-lab twin in `terrain.ts:213`)\nderives `cold` from\n`abs(row/rows - 0.5) * 2 * (1 - climate)` \u2014 an implicit latitude proxy\nthat doesn't expose:\n\n- **Continentality** \u2014 graph distance from ocean, drives seasonal\n swing and base aridity (Siberia vs Ireland)\n- **Seasonality** \u2014 latitude amplitude (tropic vs arctic)\n- **Rain shadow** \u2014 windward / leeward of mountains (wet Pacific NW\n vs dry Great Basin)\n- **Elevation lapse rate** \u2014 ~6.5\u00b0C/km cooling\n- **West-coast asymmetry** \u2014 without ocean current simulation, mid-\n latitude west coasts must still feel maritime; east coasts must\n still feel continental.\n\nThe classifier consumes the collapsed `cold` value, producing biomes\nthat don't differentiate maritime temperate from continental temperate,\nor windward rainforest from leeward rain shadow.\n\nThis objective decomposes the climate input into independent per-hex\nfields \u2014 `latitude`, `continentality`, `wind_band` \u2014 then derives mean\nT, mean P, seasonality, evapotranspiration deficit. The biome\nclassifier consumes the derived values, exposing maritime / continental\nand rain-shadow distinctions at last." + "summary": "`mc-mapgen::sampleCell` (and the design-lab twin in `terrain.ts:213`)\nderives `cold` from\n`abs(row/rows - 0.5) * 2 * (1 - climate)` β€” an implicit latitude proxy\nthat doesn't expose:\n\n- **Continentality** β€” graph distance from ocean, drives seasonal\n swing and base aridity (Siberia vs Ireland)\n- **Seasonality** β€” latitude amplitude (tropic vs arctic)\n- **Rain shadow** β€” windward / leeward of mountains (wet Pacific NW\n vs dry Great Basin)\n- **Elevation lapse rate** β€” ~6.5Β°C/km cooling\n- **West-coast asymmetry** β€” without ocean current simulation, mid-\n latitude west coasts must still feel maritime; east coasts must\n still feel continental.\n\nThe classifier consumes the collapsed `cold` value, producing biomes\nthat don't differentiate maritime temperate from continental temperate,\nor windward rainforest from leeward rain shadow.\n\nThis objective decomposes the climate input into independent per-hex\nfields β€” `latitude`, `continentality`, `wind_band` β€” then derives mean\nT, mean P, seasonality, evapotranspiration deficit. The biome\nclassifier consumes the derived values, exposing maritime / continental\nand rain-shadow distinctions at last." }, { "id": "p2-50", @@ -2128,7 +2128,7 @@ "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "Every terraformer-owned objective claims \"deterministic from seed\", but\nno objective specifies (a) which RNG, (b) the seed-mixing function,\n(c) the version pin that survives `cargo update`. Today, the worldgen\npipeline calls `rand::thread_rng()` and `StdRng::seed_from_u64(seed)`\ninconsistently across crates. `StdRng` is explicitly documented as\n**not stable across rand versions** \u2014 saves break silently on dep\nbumps.\n\nThis objective pins a versioned RNG and a single seed-derivation\nfunction for all worldgen \u2014 tectonics, hydrology, climate, species\nselection. Lands as a small infrastructure objective so Wave A of the\nterraformer schedule (p1-50, p2-49) is built on a stable foundation,\nnot retrofitted later." + "summary": "Every terraformer-owned objective claims \"deterministic from seed\", but\nno objective specifies (a) which RNG, (b) the seed-mixing function,\n(c) the version pin that survives `cargo update`. Today, the worldgen\npipeline calls `rand::thread_rng()` and `StdRng::seed_from_u64(seed)`\ninconsistently across crates. `StdRng` is explicitly documented as\n**not stable across rand versions** β€” saves break silently on dep\nbumps.\n\nThis objective pins a versioned RNG and a single seed-derivation\nfunction for all worldgen β€” tectonics, hydrology, climate, species\nselection. Lands as a small infrastructure objective so Wave A of the\nterraformer schedule (p1-50, p2-49) is built on a stable foundation,\nnot retrofitted later." }, { "id": "p2-51", @@ -2139,77 +2139,77 @@ "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "The terraformer pipeline now exposes ~15 internal parameters\n(plate count, tectonic strength, fbm octaves, sea level, latitude\ngradient, continentality decay, rain-shadow factor, erosion\niterations, drainage threshold, etc.). Designers tune these in the\nforest lab; **players see none of them**. The new-game screen ships\n\"Map Size\" and not much else.\n\nIndustry baseline (Civ 6, Old World, Songs of Conquest) exposes 4\u20136\nhigh-level shape knobs. Each knob is a *preset* that derives several\ninternal parameters at once. This objective wires that surface from\nJSON presets through `mc-mapgen` parameters into the Godot game-setup\nscene." + "summary": "The terraformer pipeline now exposes ~15 internal parameters\n(plate count, tectonic strength, fbm octaves, sea level, latitude\ngradient, continentality decay, rain-shadow factor, erosion\niterations, drainage threshold, etc.). Designers tune these in the\nforest lab; **players see none of them**. The new-game screen ships\n\"Map Size\" and not much else.\n\nIndustry baseline (Civ 6, Old World, Songs of Conquest) exposes 4–6\nhigh-level shape knobs. Each knob is a *preset* that derives several\ninternal parameters at once. This objective wires that surface from\nJSON presets through `mc-mapgen` parameters into the Godot game-setup\nscene." }, { "id": "p2-52", - "title": "Split terrain enum into substrate \u00d7 flora-cover layers (resolve biome ontology)", + "title": "Split terrain enum into substrate Γ— flora-cover layers (resolve biome ontology)", "priority": "p2", "status": "done", "scope": "game1", "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "The current `terrain.json` enum (16 IDs) conflates three orthogonal\necological layers into a single field:\n\n| Layer | Examples currently in terrain enum | Should live where |\n|---|---|---|\n| **Substrate** (geological / hydrological) | `mountains`, `hills`, `ocean`, `coast`, `lake`, `inland_sea`, `volcano`, `ice`, `snow` | terrain (correct) |\n| **Flora-cover** (emergent from species) | `forest`, `jungle`, `boreal_forest`, `grassland`, `swamp` | derived from flora selector |\n| **Substrate \u00d7 climate composite** | `desert`, `tundra`, `plains` | derived label, not authored |\n\nThe `feature_type: \"foliage\"` field on `forest`/`jungle`/`boreal_forest`\nin `public/games/age-of-dwarves/data/terrain/land_forest.json` is the\ndata layer admitting these are flora cover masquerading as terrain.\n\nThis conflation propagates everywhere:\n- Flora species `biomes[]` arrays list flora-cover types as locking\n conditions (a beech tree's `biomes = [temperate_forest, forest]` \u2014\n both are flora-cover labels, not substrates)\n- Climate classifier (`mc-climate::derive::classify_terrain_whittaker`)\n emits flora-cover names from T/P bands directly, skipping the\n substrate axis\n- Terrain blends (`terrain_blends.json`) mix substrate edges\n (`coast+plains \u2192 shore`) with flora-cover edges\n (`forest+plains \u2192 grass_fringe`) on equal footing\n\nThis objective restructures the data model into three independent layers\nthat the renderer composes into a final visual:\n\n```\nSubstrate \u2190 Tectonics + Hydrology output\n \u00d7 Climate \u2190 (t_band, p_band, riparian_distance)\n \u00d7 Flora-cover \u2190 Ecology selector output (canopy/understory/ground/bare)\n = Biome label \u2190 Display name only, derived not stored\n```" + "summary": "The current `terrain.json` enum (16 IDs) conflates three orthogonal\necological layers into a single field:\n\n| Layer | Examples currently in terrain enum | Should live where |\n|---|---|---|\n| **Substrate** (geological / hydrological) | `mountains`, `hills`, `ocean`, `coast`, `lake`, `inland_sea`, `volcano`, `ice`, `snow` | terrain (correct) |\n| **Flora-cover** (emergent from species) | `forest`, `jungle`, `boreal_forest`, `grassland`, `swamp` | derived from flora selector |\n| **Substrate Γ— climate composite** | `desert`, `tundra`, `plains` | derived label, not authored |\n\nThe `feature_type: \"foliage\"` field on `forest`/`jungle`/`boreal_forest`\nin `public/games/age-of-dwarves/data/terrain/land_forest.json` is the\ndata layer admitting these are flora cover masquerading as terrain.\n\nThis conflation propagates everywhere:\n- Flora species `biomes[]` arrays list flora-cover types as locking\n conditions (a beech tree's `biomes = [temperate_forest, forest]` β€”\n both are flora-cover labels, not substrates)\n- Climate classifier (`mc-climate::derive::classify_terrain_whittaker`)\n emits flora-cover names from T/P bands directly, skipping the\n substrate axis\n- Terrain blends (`terrain_blends.json`) mix substrate edges\n (`coast+plains β†’ shore`) with flora-cover edges\n (`forest+plains β†’ grass_fringe`) on equal footing\n\nThis objective restructures the data model into three independent layers\nthat the renderer composes into a final visual:\n\n```\nSubstrate ← Tectonics + Hydrology output\n Γ— Climate ← (t_band, p_band, riparian_distance)\n Γ— Flora-cover ← Ecology selector output (canopy/understory/ground/bare)\n = Biome label ← Display name only, derived not stored\n```" }, { "id": "p2-53", - "title": "Action vocabulary \u2014 gap analysis between design page and shipped Rust/Godot game", + "title": "Action vocabulary β€” gap analysis between design page and shipped Rust/Godot game", "priority": "p2", "status": "done", "scope": "game1", "owner": "wireguard", "updated_at": "2026-05-03", "blocked_by": [], - "summary": "The design page at `/unit-actions` (`.project/designs/app/src/pages/UnitActions.tsx`) curates an exemplar per unit/building category and lists per-archetype action vocabularies. Cross-checking that vocabulary against the shipped Rust action registry (`mc-core/src/action.rs::ActionKind`), the JSON capability map (`unit_actions.json`), and the Godot panel (`scenes/hud/unit_panel.gd`) reveals four classes of gap. This objective is the gap analysis only \u2014 implementation work splits out into child objectives once the design vocabulary is ratified." + "summary": "The design page at `/unit-actions` (`.project/designs/app/src/pages/UnitActions.tsx`) curates an exemplar per unit/building category and lists per-archetype action vocabularies. Cross-checking that vocabulary against the shipped Rust action registry (`mc-core/src/action.rs::ActionKind`), the JSON capability map (`unit_actions.json`), and the Godot panel (`scenes/hud/unit_panel.gd`) reveals four classes of gap. This objective is the gap analysis only β€” implementation work splits out into child objectives once the design vocabulary is ratified." }, { "id": "p2-53a", - "title": "Sentry/Guard ActionKind \u2014 add Sentry/Unsentry to mc-core with wake-on-vision", + "title": "Sentry/Guard ActionKind β€” add Sentry/Unsentry to mc-core with wake-on-vision", "priority": "p2", "status": "done", "scope": "game1", "owner": "wireguard", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "Gap 1 from p2-53: The design page at `.project/designs/app/src/pages/UnitActions.tsx` proposes Guard (sentry posture, no stat bonus, wakes on enemy entering vision range) as distinct from Fortify (cumulative dig-in, wakes only on adjacency). The shipped game had only `Fortify`/`Unfortify`. Decision: add `ActionKind::Sentry` / `ActionKind::Unsentry` with wake-on-vision predicate in `processor.rs` turn-end phase.\n\nThis is the **canonical template objective** for p2-53. The pattern established here \u2014 Rust enum variant + `DisabledReason` + `UnitCapability` field + `legal_actions()` gate + `UnitState` field + turn-phase hook + JSON keyword map + GDScript signal + action handler + tests + objective file \u2014 is the exact pattern followed by every downstream child (53f, 53g, 53h, 53i).\n\nMechanical definition:\n- `Sentry`: unit enters sentry posture. No stat bonus (unlike Fortify's cumulative defense). Consumes movement. Mutually exclusive with Fortify and Patrol.\n- `Unsentry`: manual exit from sentry posture. Always enabled while sentrying.\n- **Wake-on-vision**: at turn-end phase (after fauna encounters, before PvP combat \u2014 Phase 5a-sentry), any sentrying unit with an enemy unit within 2 hex radius automatically wakes (`is_sentrying \u2192 false`). This is the load-bearing distinction from Fortify." + "summary": "Gap 1 from p2-53: The design page at `.project/designs/app/src/pages/UnitActions.tsx` proposes Guard (sentry posture, no stat bonus, wakes on enemy entering vision range) as distinct from Fortify (cumulative dig-in, wakes only on adjacency). The shipped game had only `Fortify`/`Unfortify`. Decision: add `ActionKind::Sentry` / `ActionKind::Unsentry` with wake-on-vision predicate in `processor.rs` turn-end phase.\n\nThis is the **canonical template objective** for p2-53. The pattern established here β€” Rust enum variant + `DisabledReason` + `UnitCapability` field + `legal_actions()` gate + `UnitState` field + turn-phase hook + JSON keyword map + GDScript signal + action handler + tests + objective file β€” is the exact pattern followed by every downstream child (53f, 53g, 53h, 53i).\n\nMechanical definition:\n- `Sentry`: unit enters sentry posture. No stat bonus (unlike Fortify's cumulative defense). Consumes movement. Mutually exclusive with Fortify and Patrol.\n- `Unsentry`: manual exit from sentry posture. Always enabled while sentrying.\n- **Wake-on-vision**: at turn-end phase (after fauna encounters, before PvP combat β€” Phase 5a-sentry), any sentrying unit with an enemy unit within 2 hex radius automatically wakes (`is_sentrying β†’ false`). This is the load-bearing distinction from Fortify." }, { "id": "p2-53a1", - "title": "Sentry bridge state pipe \u2014 extend `GdUnitActions` signature to pass `is_sentrying`", + "title": "Sentry bridge state pipe β€” extend `GdUnitActions` signature to pass `is_sentrying`", "priority": "p2", "status": "done", "scope": "game1", "owner": "simulator-infra", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "p2-53a shipped the Sentry/Unsentry ActionKind end-to-end EXCEPT the GDExtension bridge: `api-gdext/src/action.rs:58` and `:96` hardcode `is_sentrying: false` with TODO comments. This means the unit panel in Godot cannot show the Unsentry button (it never thinks the unit is sentrying), only the Sentry button, regardless of actual state.\n\nThis sub-objective closes the loop. Tiny scope \u2014 one signature extension, one GDScript caller update, one test." + "summary": "p2-53a shipped the Sentry/Unsentry ActionKind end-to-end EXCEPT the GDExtension bridge: `api-gdext/src/action.rs:58` and `:96` hardcode `is_sentrying: false` with TODO comments. This means the unit panel in Godot cannot show the Unsentry button (it never thinks the unit is sentrying), only the Sentry button, regardless of actual state.\n\nThis sub-objective closes the loop. Tiny scope β€” one signature extension, one GDScript caller update, one test." }, { "id": "p2-53b", - "title": "Building action registry \u2014 `BuildingActionKind`, `building_actions.json`, `GdBuildingActions` bridge", + "title": "Building action registry β€” `BuildingActionKind`, `building_actions.json`, `GdBuildingActions` bridge", "priority": "p2", "status": "done", "scope": "game1", "owner": "simulator-infra", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "Buildings have no action registry analogous to the unit pipeline (p1-20). Today, building-level player actions are scattered across `city_screen.gd` (Set Rally button), `world_map.gd` (production-queue clicks), and ad-hoc per-screen controls. There is no `BuildingActionKind` enum, no `building_actions.json` capability map, no `GdBuildingActions` bridge, and the unit panel has no visual analogue for buildings.\n\nThis objective lands the canonical building-action surface \u2014 exactly mirroring the unit-action pipeline (p1-20) so the patterns are uniform. After landing, every subsequent building action (Garrison, Repair, Toggle Active, Drill, Auto-Promote, Murder Holes, Gate, Raze, Annex, Stockpile, Overdrive, Research Aid, Invoke Ancestor, Inscribe Hero, Pack & March, Supply Aura, Claim Territory, Light Beacon \u2014 see p2-53d) ships as one enum variant + one keyword mapping + one handler + one signal." + "summary": "Buildings have no action registry analogous to the unit pipeline (p1-20). Today, building-level player actions are scattered across `city_screen.gd` (Set Rally button), `world_map.gd` (production-queue clicks), and ad-hoc per-screen controls. There is no `BuildingActionKind` enum, no `building_actions.json` capability map, no `GdBuildingActions` bridge, and the unit panel has no visual analogue for buildings.\n\nThis objective lands the canonical building-action surface β€” exactly mirroring the unit-action pipeline (p1-20) so the patterns are uniform. After landing, every subsequent building action (Garrison, Repair, Toggle Active, Drill, Auto-Promote, Murder Holes, Gate, Raze, Annex, Stockpile, Overdrive, Research Aid, Invoke Ancestor, Inscribe Hero, Pack & March, Supply Aura, Claim Territory, Light Beacon β€” see p2-53d) ships as one enum variant + one keyword mapping + one handler + one signal." }, { "id": "p2-53c", - "title": "Rally vocabulary expansion \u2014 Hold / Fortify / JoinFormation + two-waypoint Patrol", + "title": "Rally vocabulary expansion β€” Hold / Fortify / JoinFormation + two-waypoint Patrol", "priority": "p2", "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "`BuildingRallyPoint.command` (p0-41, `mc-turn/src/game_state.rs`) and `_FORMATION_COMMANDS` (`unit_panel.gd:56`) currently support three orders by name: `defend`, `patrol`, `advance`. The named `\"patrol\"` is a marker string \u2014 it does NOT carry the two-waypoint config the unit-side Patrol uses (p1-21 `IssuePatrol`/`EditPatrol`). Treat current rally-Patrol as a stub: `try_spawn_unit` issues a `PatrolOrder` PingPong from spawn-hex to rally-hex, with no way to configure the second waypoint.\n\nThis objective:\n1. Upgrades rally-Patrol to a real two-waypoint config matching unit-side Patrol.\n2. Adds three new rally orders: **Hold** (skip-turn idle, no engagement), **Fortify** (auto-fortify on arrival), **JoinFormation** (skip PatrolOrder issuance; rely on `auto_join` to link into destination hex's formation).\n3. Parks **Reinforce** until an army-identity primitive exists (open question; outside scope)." + "summary": "`BuildingRallyPoint.command` (p0-41, `mc-turn/src/game_state.rs`) and `_FORMATION_COMMANDS` (`unit_panel.gd:56`) currently support three orders by name: `defend`, `patrol`, `advance`. The named `\"patrol\"` is a marker string β€” it does NOT carry the two-waypoint config the unit-side Patrol uses (p1-21 `IssuePatrol`/`EditPatrol`). Treat current rally-Patrol as a stub: `try_spawn_unit` issues a `PatrolOrder` PingPong from spawn-hex to rally-hex, with no way to configure the second waypoint.\n\nThis objective:\n1. Upgrades rally-Patrol to a real two-waypoint config matching unit-side Patrol.\n2. Adds three new rally orders: **Hold** (skip-turn idle, no engagement), **Fortify** (auto-fortify on arrival), **JoinFormation** (skip PatrolOrder issuance; rely on `auto_join` to link into destination hex's formation).\n3. Parks **Reinforce** until an army-identity primitive exists (open question; outside scope)." }, { "id": "p2-53d", - "title": "Building specifics \u2014 Garrison, Repair, Toggle Active + 18 archetype-specific actions", + "title": "Building specifics β€” Garrison, Repair, Toggle Active + 18 archetype-specific actions", "priority": "p2", "status": "done", "scope": "game1", @@ -2231,7 +2231,7 @@ }, { "id": "p2-53f", - "title": "Infantry specifics \u2014 Shield Wall, Brace, Shove, Rage, Cleave, War Cry", + "title": "Infantry specifics β€” Shield Wall, Brace, Shove, Rage, Cleave, War Cry", "priority": "p2", "status": "done", "scope": "game1", @@ -2244,7 +2244,7 @@ }, { "id": "p2-53g", - "title": "Ranged specifics \u2014 Volley, Aimed Shot, Fire Arrows", + "title": "Ranged specifics β€” Volley, Aimed Shot, Fire Arrows", "priority": "p2", "status": "done", "scope": "game1", @@ -2253,11 +2253,11 @@ "blocked_by": [ "p2-53a" ], - "summary": "Three new `ActionKind` variants gating on `keywords: [\"ranged\"]`:\n\n- **Volley** \u2014 area attack: hits target hex's centre + 2 random edge slots; lower per-target damage, more chances to hit\n- **Aimed Shot** \u2014 skip-turn that ignores 50% of target's defence on next attack; sets `aimed_shot_pending = true`\n- **Fire Arrows** \u2014 toggle posture; ranged attacks ignite tile (uses Fire system from `mc-ecology` if available); persistent damage, smoke obscures vision" + "summary": "Three new `ActionKind` variants gating on `keywords: [\"ranged\"]`:\n\n- **Volley** β€” area attack: hits target hex's centre + 2 random edge slots; lower per-target damage, more chances to hit\n- **Aimed Shot** β€” skip-turn that ignores 50% of target's defence on next attack; sets `aimed_shot_pending = true`\n- **Fire Arrows** β€” toggle posture; ranged attacks ignite tile (uses Fire system from `mc-ecology` if available); persistent damage, smoke obscures vision" }, { "id": "p2-53h", - "title": "Cavalry specifics \u2014 Charge, Pursue, Wheel", + "title": "Cavalry specifics β€” Charge, Pursue, Wheel", "priority": "p2", "status": "done", "scope": "game1", @@ -2266,11 +2266,11 @@ "blocked_by": [ "p2-53a" ], - "summary": "Three new `ActionKind` variants gating on `keywords: [\"cavalry\"]`:\n\n- **Charge** \u2014 move 2+ hexes in straight line, then attack: +30% damage; may push target back; cancellable by Brace (p2-53f)\n- **Pursue** \u2014 if target dies/routs, advance into its hex without spending movement (passive trigger; manual confirm if multiple options)\n- **Wheel** \u2014 reorient to a new edge slot without leaving the hex; useful to dodge first-strike" + "summary": "Three new `ActionKind` variants gating on `keywords: [\"cavalry\"]`:\n\n- **Charge** β€” move 2+ hexes in straight line, then attack: +30% damage; may push target back; cancellable by Brace (p2-53f)\n- **Pursue** β€” if target dies/routs, advance into its hex without spending movement (passive trigger; manual confirm if multiple options)\n- **Wheel** β€” reorient to a new edge slot without leaving the hex; useful to dodge first-strike" }, { "id": "p2-53i", - "title": "Support specifics \u2014 Engineer, Pioneer, Medic, Scout", + "title": "Support specifics β€” Engineer, Pioneer, Medic, Scout", "priority": "p2", "status": "done", "scope": "game1", @@ -2281,7 +2281,7 @@ }, { "id": "p2-54", - "title": "Resource visibility \u2014 three-axis (visibility/yield_gate/improvement_gate) refactor", + "title": "Resource visibility β€” three-axis (visibility/yield_gate/improvement_gate) refactor", "priority": "p2", "status": "done", "scope": "game1", @@ -2299,11 +2299,11 @@ "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "`public/resources/resources.json` has been migrated to the three-axis schema\n(`visibility` / `yield_gate` / `improvement_gate`). However, the 30 individual deposit\nfiles in `public/resources/deposits/*.json` \u2014 which are the actual source read by\nGDScript (via `DataLoader`) and the guide-web (via Vite `import.meta.glob`) \u2014 still use\n`revealed_by_tech`.\n\nUntil this migration is done, all consumer code falls back to `revealed_by_tech` via\nthe dual-read fallback paths installed in p2-54. The new three-axis field path is\nforward-compatible but inert.\n\nThis is the **blocking precondition** for p2-54b (observation cache), p2-54c (renderer\ndecorations), and p2-54d (AI tech-priority hints) to work correctly.\n\n---" + "summary": "`public/resources/resources.json` has been migrated to the three-axis schema\n(`visibility` / `yield_gate` / `improvement_gate`). However, the 30 individual deposit\nfiles in `public/resources/deposits/*.json` β€” which are the actual source read by\nGDScript (via `DataLoader`) and the guide-web (via Vite `import.meta.glob`) β€” still use\n`revealed_by_tech`.\n\nUntil this migration is done, all consumer code falls back to `revealed_by_tech` via\nthe dual-read fallback paths installed in p2-54. The new three-axis field path is\nforward-compatible but inert.\n\nThis is the **blocking precondition** for p2-54b (observation cache), p2-54c (renderer\ndecorations), and p2-54d (AI tech-priority hints) to work correctly.\n\n---" }, { "id": "p2-54b", - "title": "Per-player tile observation cache \u2014 flora/fauna last-observed state", + "title": "Per-player tile observation cache β€” flora/fauna last-observed state", "priority": "p2", "status": "done", "scope": "game1", @@ -2321,7 +2321,7 @@ "owner": "terraformer", "updated_at": "2026-05-01", "blocked_by": [], - "summary": "After the three-axis schema (p2-54) and per-player observation cache (p2-54b) land, the renderer must:\n1. Render flora/fauna from the **player's PlayerObservations**, not the simulator's current state\n2. Render `indicator_decorations` on tech-gated resource tiles (rust-red iron-oxide soil for iron, malachite stains for copper, etc.) \u2014 these are visual cues that exist before the resource's HUD icon is unlocked\n3. Render the explicit resource icon when the player's tech satisfies `yield_gate`\n\nAffects both the design app's `MapCanvas` (TS/WASM consumer) and Godot's `hex_renderer.gd`." + "summary": "After the three-axis schema (p2-54) and per-player observation cache (p2-54b) land, the renderer must:\n1. Render flora/fauna from the **player's PlayerObservations**, not the simulator's current state\n2. Render `indicator_decorations` on tech-gated resource tiles (rust-red iron-oxide soil for iron, malachite stains for copper, etc.) β€” these are visual cues that exist before the resource's HUD icon is unlocked\n3. Render the explicit resource icon when the player's tech satisfies `yield_gate`\n\nAffects both the design app's `MapCanvas` (TS/WASM consumer) and Godot's `hex_renderer.gd`." }, { "id": "p2-54d", @@ -2419,7 +2419,7 @@ }, { "id": "p2-56a", - "title": "Worker category types \u2014 Sustenance / Construction / Wealth taxonomy", + "title": "Worker category types β€” Sustenance / Construction / Wealth taxonomy", "priority": "p2", "status": "done", "scope": "game1", @@ -2432,7 +2432,7 @@ }, { "id": "p2-56b", - "title": "Expertise tier progression \u2014 5-tier specialist XP ladder", + "title": "Expertise tier progression β€” 5-tier specialist XP ladder", "priority": "p2", "status": "done", "scope": "game1", @@ -2443,7 +2443,7 @@ }, { "id": "p2-56c", - "title": "Master / Grandmaster auras \u2014 adjacent-slot yield propagation", + "title": "Master / Grandmaster auras β€” adjacent-slot yield propagation", "priority": "p2", "status": "done", "scope": "game1", @@ -2456,7 +2456,7 @@ }, { "id": "p2-57", - "title": "Production-chain typed resources \u2014 raw \u2192 processed pipelines wired into mc-city", + "title": "Production-chain typed resources β€” raw β†’ processed pipelines wired into mc-city", "priority": "p2", "status": "done", "scope": "game1", @@ -2467,7 +2467,7 @@ }, { "id": "p2-57a", - "title": "Typed resource stockpile \u2014 raw vs processed taxonomy", + "title": "Typed resource stockpile β€” raw vs processed taxonomy", "priority": "p2", "status": "done", "scope": "game1", @@ -2478,7 +2478,7 @@ }, { "id": "p2-57b", - "title": "Building consume/produce edges \u2014 stockpile coupled to unit quality", + "title": "Building consume/produce edges β€” stockpile coupled to unit quality", "priority": "p2", "status": "done", "scope": "game1", @@ -2489,7 +2489,7 @@ }, { "id": "p2-57c-mc-units-quality-consumer", - "title": "mc-units quality consumer \u2014 turn QualityTier into unit stat deltas (gives quality_chain a contract)", + "title": "mc-units quality consumer β€” turn QualityTier into unit stat deltas (gives quality_chain a contract)", "priority": "p2", "status": "done", "scope": "game1", @@ -2500,7 +2500,7 @@ }, { "id": "p2-58", - "title": "Ambient encounter rolls per tile moved \u2014 fauna_density \u00d7 ecology_tier", + "title": "Ambient encounter rolls per tile moved β€” fauna_density Γ— ecology_tier", "priority": "p2", "status": "done", "scope": "game1", @@ -2511,7 +2511,7 @@ }, { "id": "p2-58a", - "title": "TileState fauna fields \u2014 fauna_density + fauna_index for AmbientTileCtx", + "title": "TileState fauna fields β€” fauna_density + fauna_index for AmbientTileCtx", "priority": "p2", "status": "done", "scope": "game1", @@ -2522,7 +2522,7 @@ }, { "id": "p2-58b", - "title": "Ambient encounter hook \u2014 mc-turn::movement calls roll_ambient_encounter per tile step", + "title": "Ambient encounter hook β€” mc-turn::movement calls roll_ambient_encounter per tile step", "priority": "p2", "status": "done", "scope": "game1", @@ -2533,7 +2533,7 @@ }, { "id": "p2-59", - "title": "Pioneer escort mechanic \u2014 protection rules vs ambient encounters", + "title": "Pioneer escort mechanic β€” protection rules vs ambient encounters", "priority": "p2", "status": "done", "scope": "game1", @@ -2555,7 +2555,7 @@ }, { "id": "p2-61", - "title": "Bind mc-observation gate_bits to player tech state \u2014 recording gates per-field", + "title": "Bind mc-observation gate_bits to player tech state β€” recording gates per-field", "priority": "p2", "status": "done", "scope": "game1", @@ -2566,7 +2566,7 @@ }, { "id": "p2-62", - "title": "Procedural unit/building renderer \u2014 alpha-only visual substitute", + "title": "Procedural unit/building renderer β€” alpha-only visual substitute", "priority": "p2", "status": "done", "scope": "game1", @@ -2588,7 +2588,7 @@ }, { "id": "p2-64", - "title": "Apricot async batch protocol \u2014 launch / status / fetch decoupling", + "title": "Apricot async batch protocol β€” launch / status / fetch decoupling", "priority": "p2", "status": "done", "scope": "game1", @@ -2621,7 +2621,7 @@ }, { "id": "p2-67", - "title": "Claude-driven player API \u2014 programmatic player + Agent-SDK adapter", + "title": "Claude-driven player API β€” programmatic player + Agent-SDK adapter", "priority": "p2", "status": "done", "scope": "game1", @@ -2632,7 +2632,7 @@ }, { "id": "p2-67-followup-legal-actions", - "title": "PlayerView.legal_actions \u2014 populate full per-unit / per-city / empire-level legal-action enumerators", + "title": "PlayerView.legal_actions β€” populate full per-unit / per-city / empire-level legal-action enumerators", "priority": "p2", "status": "done", "scope": "game1", @@ -2643,7 +2643,7 @@ }, { "id": "p2-67-followup-mcts-tactical-state-impl", - "title": "TreeState impl for TacticalState \u2014 wire real MCTS into the AI decision path", + "title": "TreeState impl for TacticalState β€” wire real MCTS into the AI decision path", "priority": "p2", "status": "done", "scope": "game1", @@ -2654,7 +2654,7 @@ }, { "id": "p2-68", - "title": "mc-ai headless turn driver \u2014 GameState projector/applicator + run_ai_turn", + "title": "mc-ai headless turn driver β€” GameState projector/applicator + run_ai_turn", "priority": "p2", "status": "done", "scope": "game1", @@ -2676,7 +2676,7 @@ }, { "id": "p2-70", - "title": "mc-vision \u2014 per-player tile visibility producer (Rust)", + "title": "mc-vision β€” per-player tile visibility producer (Rust)", "priority": "p2", "status": "done", "scope": "game1", @@ -2687,7 +2687,7 @@ }, { "id": "p2-71", - "title": "Bench projector enrichment \u2014 make MCTS see a real tactical surface", + "title": "Bench projector enrichment β€” make MCTS see a real tactical surface", "priority": "p2", "status": "done", "scope": "game1", @@ -2698,7 +2698,7 @@ }, { "id": "p2-71b", - "title": "Militarist starter widening \u2014 add a settler/founder unit so FoundCity fires", + "title": "Militarist starter widening β€” add a settler/founder unit so FoundCity fires", "priority": "p2", "status": "done", "scope": "game1", @@ -2709,7 +2709,7 @@ }, { "id": "p2-72", - "title": "GdPlayerApi \u2192 render bridge (visualise the API-held game world)", + "title": "GdPlayerApi β†’ render bridge (visualise the API-held game world)", "priority": "p2", "status": "done", "scope": "game1", @@ -2722,7 +2722,7 @@ }, { "id": "p2-72-option-b", - "title": "Option B render bridge \u2014 proof scene rehydrates GDScript from GdPlayerApi each turn", + "title": "Option B render bridge β€” proof scene rehydrates GDScript from GdPlayerApi each turn", "priority": "p2", "status": "done", "scope": "game1", @@ -2790,7 +2790,7 @@ }, { "id": "p2-73-ui-theme-token-pipeline", - "title": "UI theme pipeline \u2014 generate ui_theme.tres from design-tokens.json + apply globally", + "title": "UI theme pipeline β€” generate ui_theme.tres from design-tokens.json + apply globally", "priority": "p2", "status": "done", "scope": "game1", @@ -2801,7 +2801,7 @@ }, { "id": "p2-74-ui-dehardcode-to-tokens", - "title": "De-hardcode the Godot UI \u2014 route 45 scene scripts off raw Color() onto theme/tokens", + "title": "De-hardcode the Godot UI β€” route 45 scene scripts off raw Color() onto theme/tokens", "priority": "p2", "status": "done", "scope": "game1", @@ -2814,17 +2814,17 @@ }, { "id": "p2-75", - "title": "Improvement-completion effects subsystem in Rust \u2014 move completion side-effects out of GDScript", + "title": "Improvement-completion effects subsystem in Rust β€” move completion side-effects out of GDScript", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Improvements ship as data (`public/resources/improvements/*.json`) and\nhave a per-hex anchor (`p3-04`: sparse `BTreeMap<(u16,u16), TileImprovement>`\non `GameState`). But **no improvement effect is currently wired in the\nRust sim** \u2014 not even the fort's `defense_bonus` or the tunnel's\n`concealed_from_surface`. The `mc-core` improvement parser\n(`mc-core/src/improvement.rs`) only reads `hp` / `severable` / `flags`;\nthe `effects` object is dropped on the floor.\n\nWorse, improvement **placement** (completion) currently happens in the\nGDScript bridge (`ImprovementManager` / `_on_improvement_completed`),\nnot in Rust. `mc-state::set_improvement` has **no production callers** \u2014\nreal writes are direct `tile_improvements.insert(...)`. This violates\nRail 1 (Rust is the simulation source of truth) and is tech-debt\nrelated to the p0-26 AI-port debt.\n\nThis objective stands up the improvement-effects subsystem: move\nimprovement-completion into Rust, parse the `effects` object, and wire\nthe two baseline effects (`defense_bonus`, `concealed_from_surface`) as\nthe first cases. The bunker (`p2-76`) is the first *new* consumer and\ndepends on this." + "summary": "Improvements ship as data (`public/resources/improvements/*.json`) and\nhave a per-hex anchor (`p3-04`: sparse `BTreeMap<(u16,u16), TileImprovement>`\non `GameState`). But **no improvement effect is currently wired in the\nRust sim** β€” not even the fort's `defense_bonus` or the tunnel's\n`concealed_from_surface`. The `mc-core` improvement parser\n(`mc-core/src/improvement.rs`) only reads `hp` / `severable` / `flags`;\nthe `effects` object is dropped on the floor.\n\nWorse, improvement **placement** (completion) currently happens in the\nGDScript bridge (`ImprovementManager` / `_on_improvement_completed`),\nnot in Rust. `mc-state::set_improvement` has **no production callers** β€”\nreal writes are direct `tile_improvements.insert(...)`. This violates\nRail 1 (Rust is the simulation source of truth) and is tech-debt\nrelated to the p0-26 AI-port debt.\n\nThis objective stands up the improvement-effects subsystem: move\nimprovement-completion into Rust, parse the `effects` object, and wire\nthe two baseline effects (`defense_bonus`, `concealed_from_surface`) as\nthe first cases. The bunker (`p2-76`) is the first *new* consumer and\ndepends on this." }, { "id": "p2-76", - "title": "Bunker improvement \u2014 deposit-destroying fortified subterranean chamber", + "title": "Bunker improvement β€” deposit-destroying fortified subterranean chamber", "priority": "p2", "status": "done", "scope": "game1", @@ -2833,11 +2833,11 @@ "p2-75", "p2-80" ], - "summary": "The **bunker** is a Dwarf improvement on hills / mountains, gated behind\nthe era-7 tech `pneumatic_construction`. A shaped demolition charge is\nimplanted deep in the rock and detonated; the blast-heat fuses the\nshattered interior into a glass-hard armored chamber dug into the heart\nof the hill \u2014 near-impervious, concealed from the surface, held from\nwithin. The detonation collapses any ore body or seam beneath the tile,\nand the venting blast-slag salts the surface above, leaving it dead for\na span that scales with how rich the ruined deposit was.\n\nData is already authored at `public/resources/improvements/bunker.json`\n(registered in `public/games/age-of-dwarves/data/improvements/manifest.json`\nline 25). Effects: `defense_bonus: 100`, `concealed_from_surface: true`,\n`severable: false`, `hp: 75`, `destroys_deposit: true`, plus\n`surface_contamination` (`duration_basis: destroyed_deposit_tier`,\n`turns_per_tier: 10`, `min_turns: 10`,\n`tile_effect: yields_zeroed_and_unworkable`).\n\nThe bunker is the **first consumer** of the improvement-effects\nsubsystem (`p2-75`), and the first improvement to *destroy* tile\ncontents and *contaminate* a tile." + "summary": "The **bunker** is a Dwarf improvement on hills / mountains, gated behind\nthe era-7 tech `pneumatic_construction`. A shaped demolition charge is\nimplanted deep in the rock and detonated; the blast-heat fuses the\nshattered interior into a glass-hard armored chamber dug into the heart\nof the hill β€” near-impervious, concealed from the surface, held from\nwithin. The detonation collapses any ore body or seam beneath the tile,\nand the venting blast-slag salts the surface above, leaving it dead for\na span that scales with how rich the ruined deposit was.\n\nData is already authored at `public/resources/improvements/bunker.json`\n(registered in `public/games/age-of-dwarves/data/improvements/manifest.json`\nline 25). Effects: `defense_bonus: 100`, `concealed_from_surface: true`,\n`severable: false`, `hp: 75`, `destroys_deposit: true`, plus\n`surface_contamination` (`duration_basis: destroyed_deposit_tier`,\n`turns_per_tier: 10`, `min_turns: 10`,\n`tile_effect: yields_zeroed_and_unworkable`).\n\nThe bunker is the **first consumer** of the improvement-effects\nsubsystem (`p2-75`), and the first improvement to *destroy* tile\ncontents and *contaminate* a tile." }, { "id": "p2-77", - "title": "Deposit-destruction environmental taxonomy \u2014 destruction_effect classes + contamination engine", + "title": "Deposit-destruction environmental taxonomy β€” destruction_effect classes + contamination engine", "priority": "p2", "status": "done", "scope": "game1", @@ -2845,11 +2845,11 @@ "blocked_by": [ "p2-76" ], - "summary": "When a deposit is destroyed (by a bunker `p2-76`, and any future\ndeposit-destroying act), the environmental consequence depends on **what\nthe deposit was**. This objective adds a `destruction_effect` field to\ndeposit JSONs and the contamination-class engine that consumes it,\ngeneralising the single fixed-duration contamination from `p2-76`.\n\n**Verification (2026-06-23 drive, simulator-infra + cascade):** DestructionEffect taxonomy types in mc-core (per prior). Our contribution: speculation_epoch + bumps provide the dirty tracker for contam spread invalidation (game_state.rs:525,703,744); 1b/4b apply_pending_terraform + tick_contamination live and called from GD turn path (turn_manager.gd:330, api-gdext GdWorldSim:9649, mc-worldsim apply:345+ lib.rs:264); resolve_local wired (mc-mapgen/hydrology_resolve.rs:249, called from worldsim 1b); apply_migrations called in ecology engine (mc-ecology/src/engine.rs:366 per grep). p2-77 partial (types + dirty + live wiring); full JSON for coal etc + BFS spread engine + p2-79 e2e pending. Reads of p2-77 obj + cascade design + TECTONICS/HYDROLOGY + code greps. MCP objective_get/update. Evidence cites above + lib.rs:264.\n\nThe field is optional \u2014 omit it for inert deposits (the default). Shape:\n\n```json\n\"destruction_effect\": {\n \"class\": \"combustion\",\n \"contamination_turns\": 60,\n \"spreads_to_adjacent\": true,\n \"follows_hydrology\": false,\n \"clearable_by\": \"decontamination_tech_id_or_null\"\n}\n```\n\n### Class taxonomy (worked out for all 34 hills/mountains-valid deposits)\n\n- **inert** (~24: all gems \u2014 diamond / ruby / sapphire / emerald /\n amethyst / etc., gold_vein, iron_ore, stone, marble, mithril,\n magesteel, obsidian, quartz, calcite, selenite, fantasy crystals,\n wine, sheep) \u2192 deposit lost, **zero** contamination, tile reusable\n immediately. **Omit the field** (default = inert).\n- **combustion** (coal_seam) \u2192 ignites an underground coal fire: long\n contamination, spreads heat/smoke to adjacent tiles, self-extinguishes\n over time.\n- **acid_drainage** (pyrite; partial iron sulfides) \u2192 sulfuric acid\n runoff, **follows hydrology** downhill / downstream, kills yields on\n affected tiles.\n- **chemical** (saltpeter_deposit) \u2192 secondary explosion (extra damage)\n + nitrate leaching to water.\n- **toxic_mineral** (fluorite = fluoride, malachite = copper) \u2192\n localized heavy-metal / halide contamination.\n- **radioactive** (uranium \u2014 **NET-NEW deposit, does not yet exist**) \u2192\n extreme contamination, cleanup-tech-gated." + "summary": "When a deposit is destroyed (by a bunker `p2-76`, and any future\ndeposit-destroying act), the environmental consequence depends on **what\nthe deposit was**. This objective adds a `destruction_effect` field to\ndeposit JSONs and the contamination-class engine that consumes it,\ngeneralising the single fixed-duration contamination from `p2-76`.\n\n**Verification (2026-06-23 drive, simulator-infra + cascade):** DestructionEffect taxonomy types in mc-core (per prior). Our contribution: speculation_epoch + bumps provide the dirty tracker for contam spread invalidation (game_state.rs:525,703,744); 1b/4b apply_pending_terraform + tick_contamination live and called from GD turn path (turn_manager.gd:330, api-gdext GdWorldSim:9649, mc-worldsim apply:345+ lib.rs:264); resolve_local wired (mc-mapgen/hydrology_resolve.rs:249, called from worldsim 1b); apply_migrations called in ecology engine (mc-ecology/src/engine.rs:366 per grep). p2-77 partial (types + dirty + live wiring); full JSON for coal etc + BFS spread engine + p2-79 e2e pending. Reads of p2-77 obj + cascade design + TECTONICS/HYDROLOGY + code greps. MCP objective_get/update. Evidence cites above + lib.rs:264.\n\nThe field is optional β€” omit it for inert deposits (the default). Shape:\n\n```json\n\"destruction_effect\": {\n \"class\": \"combustion\",\n \"contamination_turns\": 60,\n \"spreads_to_adjacent\": true,\n \"follows_hydrology\": false,\n \"clearable_by\": \"decontamination_tech_id_or_null\"\n}\n```\n\n### Class taxonomy (worked out for all 34 hills/mountains-valid deposits)\n\n- **inert** (~24: all gems β€” diamond / ruby / sapphire / emerald /\n amethyst / etc., gold_vein, iron_ore, stone, marble, mithril,\n magesteel, obsidian, quartz, calcite, selenite, fantasy crystals,\n wine, sheep) β†’ deposit lost, **zero** contamination, tile reusable\n immediately. **Omit the field** (default = inert).\n- **combustion** (coal_seam) β†’ ignites an underground coal fire: long\n contamination, spreads heat/smoke to adjacent tiles, self-extinguishes\n over time.\n- **acid_drainage** (pyrite; partial iron sulfides) β†’ sulfuric acid\n runoff, **follows hydrology** downhill / downstream, kills yields on\n affected tiles.\n- **chemical** (saltpeter_deposit) β†’ secondary explosion (extra damage)\n + nitrate leaching to water.\n- **toxic_mineral** (fluorite = fluoride, malachite = copper) β†’\n localized heavy-metal / halide contamination.\n- **radioactive** (uranium β€” **NET-NEW deposit, does not yet exist**) β†’\n extreme contamination, cleanup-tech-gated." }, { "id": "p2-78", - "title": "Runtime localized hydrology re-solve \u2014 in-game flow/basin-fill triggered by terraforming", + "title": "Runtime localized hydrology re-solve β€” in-game flow/basin-fill triggered by terraforming", "priority": "p2", "status": "done", "scope": "game1", @@ -2858,37 +2858,37 @@ "p2-80", "p2-76" ], - "summary": "Rivers are currently baked at worldgen (`mc-mapgen` D6 flow +\nPlanchon-Darboux basin-fill); edges carry `river_edges`. **No in-game\nriver rerouting exists** \u2014 once the map is generated, hydrology is\nfrozen. The living-world USP needs hydrology to respond to terraforming:\nwhen a bunker (`p2-76`) dams a river-gap hill, water must re-route.\n\nThis objective adds a **localized in-game flow / basin-fill re-solve**:\na terraforming act (an obstruction such as a bunker on a river-gap tile)\nre-runs the D6 flow + basin-fill algorithm inside a bounded local window\naround the changed tile, producing updated `river_edges`,\n`riparian_distance`, and any new lake cells. Upstream of a dam floods\ninto a lake; downstream loses its river and `riparian_distance` rises.\nThe re-solve is invoked from the `mc-worldsim` per-turn step (`p2-80`),\nwhich already owns climate/ecology orchestration above `mc-turn` \u2014 the\nhydrology re-run is event-triggered within that same step.\n\nThis re-solve is the **shared dependency** behind two features:\n- **River damming** (bunker \u2192 flood upstream / parch downstream).\n- **`follows_hydrology` contamination** (`p2-77`: acid / chemical\n plumes travel the downhill / downstream flow path).\n\nLanding this removes the temporary river-gap build restriction `p2-76`\nimposes on the bunker." + "summary": "Rivers are currently baked at worldgen (`mc-mapgen` D6 flow +\nPlanchon-Darboux basin-fill); edges carry `river_edges`. **No in-game\nriver rerouting exists** β€” once the map is generated, hydrology is\nfrozen. The living-world USP needs hydrology to respond to terraforming:\nwhen a bunker (`p2-76`) dams a river-gap hill, water must re-route.\n\nThis objective adds a **localized in-game flow / basin-fill re-solve**:\na terraforming act (an obstruction such as a bunker on a river-gap tile)\nre-runs the D6 flow + basin-fill algorithm inside a bounded local window\naround the changed tile, producing updated `river_edges`,\n`riparian_distance`, and any new lake cells. Upstream of a dam floods\ninto a lake; downstream loses its river and `riparian_distance` rises.\nThe re-solve is invoked from the `mc-worldsim` per-turn step (`p2-80`),\nwhich already owns climate/ecology orchestration above `mc-turn` β€” the\nhydrology re-run is event-triggered within that same step.\n\nThis re-solve is the **shared dependency** behind two features:\n- **River damming** (bunker β†’ flood upstream / parch downstream).\n- **`follows_hydrology` contamination** (`p2-77`: acid / chemical\n plumes travel the downhill / downstream flow path).\n\nLanding this removes the temporary river-gap build restriction `p2-76`\nimposes on the bunker." }, { "id": "p2-79", - "title": "Environmental cascade integration \u2014 bunker \u2192 dam \u2192 flood/parch \u2192 flora dieback \u2192 fauna migration", + "title": "Environmental cascade integration β€” bunker β†’ dam β†’ flood/parch β†’ flora dieback β†’ fauna migration", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "This objective wires the full environmental cascade end-to-end \u2014 the\nheadline demonstration of the living-world USP. Each link already exists\nas its own objective; this one proves they compose into a single\nobservable chain:\n\n```\nbunker (p2-76) dams a river-gap hill\n \u2192 runtime localized hydrology re-solve (p2-78)\n \u2192 upstream floods into a lake\n \u2192 downstream loses its river, riparian_distance rises\n \u2192 the riparian forest dies back (flora lifecycle, g2-07)\n \u2192 fauna that needed it migrate out (g2-08 / g2-10)\n```\n\nPlus the contamination half: a destroyed deposit's `destruction_effect`\n(`p2-77`) sends acid / chemical plumes down the same re-solved flow path\n(`p2-78` `follows_hydrology`), compounding the downstream dieback. A\ndecontamination tech / worker-action reclaims the severe-class\ncontaminated tiles early; inert / short classes self-heal.\n\nThe entire chain runs inside the `mc-worldsim` per-turn step (`p2-80`):\nthe terraforming trigger (`p2-78` re-solve), the contamination decay\n(`p2-76` / `p2-77` overlays), and the per-turn flora/fauna response\n(`g2-07` / `g2-08` / `g2-10`) all advance in one deterministic\n`WorldSim::step` and persist via its caller-owned side-structures." + "summary": "This objective wires the full environmental cascade end-to-end β€” the\nheadline demonstration of the living-world USP. Each link already exists\nas its own objective; this one proves they compose into a single\nobservable chain:\n\n```\nbunker (p2-76) dams a river-gap hill\n β†’ runtime localized hydrology re-solve (p2-78)\n β†’ upstream floods into a lake\n β†’ downstream loses its river, riparian_distance rises\n β†’ the riparian forest dies back (flora lifecycle, g2-07)\n β†’ fauna that needed it migrate out (g2-08 / g2-10)\n```\n\nPlus the contamination half: a destroyed deposit's `destruction_effect`\n(`p2-77`) sends acid / chemical plumes down the same re-solved flow path\n(`p2-78` `follows_hydrology`), compounding the downstream dieback. A\ndecontamination tech / worker-action reclaims the severe-class\ncontaminated tiles early; inert / short classes self-heal.\n\nThe entire chain runs inside the `mc-worldsim` per-turn step (`p2-80`):\nthe terraforming trigger (`p2-78` re-solve), the contamination decay\n(`p2-76` / `p2-77` overlays), and the per-turn flora/fauna response\n(`g2-07` / `g2-08` / `g2-10`) all advance in one deterministic\n`WorldSim::step` and persist via its caller-owned side-structures." }, { "id": "p2-81", - "title": "Improvement effects authored-but-unwired \u2014 move moisture/wind/erosion/movement to Rust", + "title": "Improvement effects authored-but-unwired β€” move moisture/wind/erosion/movement to Rust", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "Follow-up to p2-75 (completion-effects subsystem) and its terrain_change extension. An audit of `src/game/engine/src/entities/improvement.gd` found four improvement-effect classes that are authored in canonical Game-1 data but reach NO simulation \u2014 a Rail-1 + feature-completeness gap.\n\nUNCONSUMED (accessor exists, zero callers in GDScript OR Rust \u2014 the improvement builds but does nothing):\n- `moisture_delta` \u2014 `irrigation` (+0.05), `drainage` (-0.05). `Improvement.get_moisture_delta` has no caller; Rust's `moisture_delta` (mc-climate `climate_effects.rs`/`weather.rs`) is WEATHER-event moisture, NOT improvement moisture.\n- `wind_speed_multiplier` \u2014 `windbreak` (0.5). `Improvement.get_wind_speed_multiplier` has no caller anywhere.\n- `prevents_erosion` \u2014 `terrace_farming` (true). `Improvement.prevents_erosion` has no caller anywhere.\n\nCOMPUTED IN GDSCRIPT (live Rail-1 violation \u2014 authority vs Rust must be traced before porting):\n- `movement_cost_modifier` \u2014 `road`/`tunnel`/`steam_track`/`hold_road`. Applied in `tile.gd:200 get_movement_cost()` in GDScript. Rust owns pathfinding (mc-pathfinding) \u2014 confirm whether this GDScript path is authoritative, a parallel/dead path, or a UI-only display before porting.\n- improvement `yields` (food/production/gold) \u2014 `Improvement.get_yield_bonus`, applied in `tile.gd:294` tile-yield computation. Rust owns city yields (mc-city/mc-economy) \u2014 same authority question.\n\nDESIGN CONSTRAINT (discovered, must be decided): `moisture_delta` and `wind_speed_multiplier` are FLOATS, but `mc_core::improvement::ImprovementEffects` deliberately derives `Eq` (\"every field is integral/bool/String, no float creeps into a BTreeMap-keyed save\"). Wiring these requires EITHER dropping `Eq` (cascades to `TileImprovement` Eq, the save round-trip + golden tests, and the `BTreeMap`-keyed determinism assumptions) OR integer-encoding the floats (e.g. milli-units: moisture_delta as i32 thousandths). The terrain_change extension (just landed) avoided this because it is a `String`.\n\nEffect-to-system bindings to implement in Rust (each Rail-1): moisture_delta \u2192 per-turn climate moisture for tiles carrying the improvement; wind_speed_multiplier \u2192 weather/wind; prevents_erosion \u2192 per-turn terrain-quality degradation guard; movement_cost_modifier \u2192 mc-pathfinding tile cost; yields \u2192 mc-city/mc-economy tile-yield fold. NOTE these are PER-TURN/standing effects (applied while the improvement exists), unlike the one-shot completion effects (defense/conceal/terrain_change) already in `complete_improvement` \u2014 they need per-turn hooks, not a completion-time write.\n\nEvidence captured 2026-06-08 during the p2-75 terrain_change follow-up audit. Do NOT delete the GDScript accessors as \"dead\" \u2014 they front authored, intended features; the fix is to wire the effects in Rust, not to remove the surface." + "summary": "Follow-up to p2-75 (completion-effects subsystem) and its terrain_change extension. An audit of `src/game/engine/src/entities/improvement.gd` found four improvement-effect classes that are authored in canonical Game-1 data but reach NO simulation β€” a Rail-1 + feature-completeness gap.\n\nUNCONSUMED (accessor exists, zero callers in GDScript OR Rust β€” the improvement builds but does nothing):\n- `moisture_delta` β€” `irrigation` (+0.05), `drainage` (-0.05). `Improvement.get_moisture_delta` has no caller; Rust's `moisture_delta` (mc-climate `climate_effects.rs`/`weather.rs`) is WEATHER-event moisture, NOT improvement moisture.\n- `wind_speed_multiplier` β€” `windbreak` (0.5). `Improvement.get_wind_speed_multiplier` has no caller anywhere.\n- `prevents_erosion` β€” `terrace_farming` (true). `Improvement.prevents_erosion` has no caller anywhere.\n\nCOMPUTED IN GDSCRIPT (live Rail-1 violation β€” authority vs Rust must be traced before porting):\n- `movement_cost_modifier` β€” `road`/`tunnel`/`steam_track`/`hold_road`. Applied in `tile.gd:200 get_movement_cost()` in GDScript. Rust owns pathfinding (mc-pathfinding) β€” confirm whether this GDScript path is authoritative, a parallel/dead path, or a UI-only display before porting.\n- improvement `yields` (food/production/gold) β€” `Improvement.get_yield_bonus`, applied in `tile.gd:294` tile-yield computation. Rust owns city yields (mc-city/mc-economy) β€” same authority question.\n\nDESIGN CONSTRAINT (discovered, must be decided): `moisture_delta` and `wind_speed_multiplier` are FLOATS, but `mc_core::improvement::ImprovementEffects` deliberately derives `Eq` (\"every field is integral/bool/String, no float creeps into a BTreeMap-keyed save\"). Wiring these requires EITHER dropping `Eq` (cascades to `TileImprovement` Eq, the save round-trip + golden tests, and the `BTreeMap`-keyed determinism assumptions) OR integer-encoding the floats (e.g. milli-units: moisture_delta as i32 thousandths). The terrain_change extension (just landed) avoided this because it is a `String`.\n\nEffect-to-system bindings to implement in Rust (each Rail-1): moisture_delta β†’ per-turn climate moisture for tiles carrying the improvement; wind_speed_multiplier β†’ weather/wind; prevents_erosion β†’ per-turn terrain-quality degradation guard; movement_cost_modifier β†’ mc-pathfinding tile cost; yields β†’ mc-city/mc-economy tile-yield fold. NOTE these are PER-TURN/standing effects (applied while the improvement exists), unlike the one-shot completion effects (defense/conceal/terrain_change) already in `complete_improvement` β€” they need per-turn hooks, not a completion-time write.\n\nEvidence captured 2026-06-08 during the p2-75 terrain_change follow-up audit. Do NOT delete the GDScript accessors as \"dead\" β€” they front authored, intended features; the fix is to wire the effects in Rust, not to remove the surface." }, { "id": "p2-82", - "title": "Climate-input save-fidelity \u2014 persist (or re-derive) worldgen-static grid inputs across save/load", + "title": "Climate-input save-fidelity β€” persist (or re-derive) worldgen-static grid inputs across save/load", "priority": "p2", "status": "done", "scope": "game1", "updated_at": "2026-06-09", "blocked_by": [], - "summary": "Discovered during **p2-80** verification (2026-06-09). The runtime climate\nphysics (`mc-climate::ClimatePhysics::process_step`, driven each turn via\n`Climate.process_turn` \u2192 `_sync_tiles_to_grid` \u2192 the Rust `GdGridState`) reads\nper-tile **inputs** that are set only at worldgen and are NOT preserved across a\nsave/load. The dominant one is **`tile.wind_direction`**:\n\n- Set at worldgen by `src/game/engine/src/generation/wind_calculator.gd`\n (and `atmosphere.gd`, which is not in the live turn chain).\n- Read by physics for temperature + moisture transport \u2014\n `mc-climate/src/physics.rs:336` and `:399` (`hex::upwind_offset(...,\n tile.wind_direction, ...)`), plus aerosol advection (`:284`).\n- **NOT** carried by `src/game/engine/src/map/tile_serializer.gd` (no\n `wind_direction` key in `to_dict` / `from_dict`).\n- **NOT** re-derived on load (`save_manager.gd` \u2192 `GameState.deserialize` \u2192\n `TileSerializer.from_dict` never touches it).\n\nOn load `wind_direction` therefore resets to its default (`0`), changing the\nclimate transport solution and so the per-turn evolution of `mean_temp` /\n`moisture` and, downstream, `surface_water` (runoff/flow,\n`physics.rs:631/682/687`). A continued climate trajectory after save/load is\n**not byte-identical on `surface_water`** purely because of this lost input \u2014\nindependent of the worldsim accumulator persistence closed in p2-80.\n\nThis is a **climate-INPUT save-fidelity** gap (a worldgen-static field physics\nreads), distinct from p2-80's worldsim-ACCUMULATOR persistence (the\nhistory-dependent counters physics writes), which is closed and verified." + "summary": "Discovered during **p2-80** verification (2026-06-09). The runtime climate\nphysics (`mc-climate::ClimatePhysics::process_step`, driven each turn via\n`Climate.process_turn` β†’ `_sync_tiles_to_grid` β†’ the Rust `GdGridState`) reads\nper-tile **inputs** that are set only at worldgen and are NOT preserved across a\nsave/load. The dominant one is **`tile.wind_direction`**:\n\n- Set at worldgen by `src/game/engine/src/generation/wind_calculator.gd`\n (and `atmosphere.gd`, which is not in the live turn chain).\n- Read by physics for temperature + moisture transport β€”\n `mc-climate/src/physics.rs:336` and `:399` (`hex::upwind_offset(...,\n tile.wind_direction, ...)`), plus aerosol advection (`:284`).\n- **NOT** carried by `src/game/engine/src/map/tile_serializer.gd` (no\n `wind_direction` key in `to_dict` / `from_dict`).\n- **NOT** re-derived on load (`save_manager.gd` β†’ `GameState.deserialize` β†’\n `TileSerializer.from_dict` never touches it).\n\nOn load `wind_direction` therefore resets to its default (`0`), changing the\nclimate transport solution and so the per-turn evolution of `mean_temp` /\n`moisture` and, downstream, `surface_water` (runoff/flow,\n`physics.rs:631/682/687`). A continued climate trajectory after save/load is\n**not byte-identical on `surface_water`** purely because of this lost input β€”\nindependent of the worldsim accumulator persistence closed in p2-80.\n\nThis is a **climate-INPUT save-fidelity** gap (a worldgen-static field physics\nreads), distinct from p2-80's worldsim-ACCUMULATOR persistence (the\nhistory-dependent counters physics writes), which is closed and verified." }, { "id": "p2-83", @@ -2899,22 +2899,22 @@ "owner": "simulator-infra", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "**Two coupled deliverables: (A) a first-class phase/round state machine, and (B) speculative parallel computation of the turn work that does not depend on the active player's pending actions \u2014 overlapping it with the player's deliberation instead of running it serially after End Turn.**" + "summary": "**Two coupled deliverables: (A) a first-class phase/round state machine, and (B) speculative parallel computation of the turn work that does not depend on the active player's pending actions β€” overlapping it with the player's deliberation instead of running it serially after End Turn.**" }, { "id": "p2-84", - "title": "Dev-only compute profiling \u2014 per-feature CPU/RAM/GPU cost over time, trigger-attributed, zero-cost in release", + "title": "Dev-only compute profiling β€” per-feature CPU/RAM/GPU cost over time, trigger-attributed, zero-cost in release", "priority": "p2", "status": "done", "scope": "game1", "owner": "simulator-infra", "updated_at": "2026-06-23", "blocked_by": [], - "summary": "**A development-time profiling layer that attributes compute cost (CPU / RAM / GPU) to the feature that incurred it, over game-time, tagged by what triggered it \u2014 so optimization effort is data-driven, not guessed. It MUST compile/gate out to zero overhead in the shipped game.**" + "summary": "**A development-time profiling layer that attributes compute cost (CPU / RAM / GPU) to the feature that incurred it, over game-time, tagged by what triggered it β€” so optimization effort is data-driven, not guessed. It MUST compile/gate out to zero overhead in the shipped game.**" }, { "id": "p2-86", - "title": "Claude-player MCP \u2014 rendered driver mode (drive UI + capture screenshots)", + "title": "Claude-player MCP β€” rendered driver mode (drive UI + capture screenshots)", "priority": "p2", "status": "done", "scope": "game1", @@ -2925,7 +2925,7 @@ }, { "id": "p2-87-single-color-system-sot", - "title": "Single game-wide colour system \u2014 one source of truth, layered tokens, every consumer derives from it", + "title": "Single game-wide colour system β€” one source of truth, layered tokens, every consumer derives from it", "priority": "p2", "status": "done", "scope": "game1", @@ -2936,48 +2936,48 @@ }, { "id": "g2-01", - "title": "Ley lines \u2014 Game 2 (Age of Kzzykt)", + "title": "Ley lines β€” Game 2 (Age of Kzzykt)", "priority": "p3", "status": "oos", "scope": "game2", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Leylines are intuitive terrain features that Kzzykt interact with instinctively \u2014 they don't cast leylines, they live alongside them. Leylines affect tile improvements and yields and influence Kzzykt AI behavior. There is no full mana economy in Game 2; leylines are environmental, not player-managed." + "summary": "Leylines are intuitive terrain features that Kzzykt interact with instinctively β€” they don't cast leylines, they live alongside them. Leylines affect tile improvements and yields and influence Kzzykt AI behavior. There is no full mana economy in Game 2; leylines are environmental, not player-managed." }, { "id": "g2-02", - "title": "Kzzykt playable race \u2014 Game 2 (Age of Kzzykt)", + "title": "Kzzykt playable race β€” Game 2 (Age of Kzzykt)", "priority": "p3", "status": "oos", "scope": "game2", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Kzzykt is the second playable race, added in Game 2. They are an insectoid bug civilization with Green MTG color affinity who intuitively use leylines. Game 1 ships with Dwarves only \u2014 Kzzykt and all subsequent races are out of scope until their respective games." + "summary": "Kzzykt is the second playable race, added in Game 2. They are an insectoid bug civilization with Green MTG color affinity who intuitively use leylines. Game 1 ships with Dwarves only β€” Kzzykt and all subsequent races are out of scope until their respective games." }, { "id": "g2-03", - "title": "Kzzykt Green school of magic \u2014 Game 2 (Age of Kzzykt)", + "title": "Kzzykt Green school of magic β€” Game 2 (Age of Kzzykt)", "priority": "p3", "status": "oos", "scope": "game2", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "The Green school is the only school of magic in Game 2 and is optional for the player \u2014 this is where spells first enter the series. It is tied to Kzzykt's Green MTG color affinity and draws on nature, instinct, and leyline resonance. Game 2 also introduces interplanetary and spacefaring late-game progression alongside the Green school tech path." + "summary": "The Green school is the only school of magic in Game 2 and is optional for the player β€” this is where spells first enter the series. It is tied to Kzzykt's Green MTG color affinity and draws on nature, instinct, and leyline resonance. Game 2 also introduces interplanetary and spacefaring late-game progression alongside the Green school tech path." }, { "id": "g2-04", - "title": "Multi-GPU sharding for batch_simulate_gpu \u2014 out-of-scope (Game 2)", + "title": "Multi-GPU sharding for batch_simulate_gpu β€” out-of-scope (Game 2)", "priority": "p3", "status": "oos", "scope": "game2", "owner": "warcouncil", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "`mc-ai::gpu::inner::GpuContext::shared()` (at `src/simulator/crates/mc-ai/src/gpu/inner.rs:189`) picks exactly ONE adapter via `instance.request_adapter(PowerPreference::HighPerformance)`. On multi-GPU hosts this leaves every adapter past #0 idle from our compute perspective.\n\napricot has 2\u00d7 NVIDIA RTX 3090. Right now `batch_simulate_gpu` uses one of them (whichever wgpu selects \u2014 typically GPU0). GPU1 sits at 0% compute util from our workload. p0-20 wall-time comparisons are therefore measured against a halved ceiling." + "summary": "`mc-ai::gpu::inner::GpuContext::shared()` (at `src/simulator/crates/mc-ai/src/gpu/inner.rs:189`) picks exactly ONE adapter via `instance.request_adapter(PowerPreference::HighPerformance)`. On multi-GPU hosts this leaves every adapter past #0 idle from our compute perspective.\n\napricot has 2Γ— NVIDIA RTX 3090. Right now `batch_simulate_gpu` uses one of them (whichever wgpu selects β€” typically GPU0). GPU1 sits at 0% compute util from our workload. p0-20 wall-time comparisons are therefore measured against a halved ceiling." }, { "id": "g2-11", - "title": "Vertical city floor stack (Game 2) \u2014 OOS", + "title": "Vertical city floor stack (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -2991,7 +2991,7 @@ }, { "id": "g2-12", - "title": "Underground layer stack (Game 2) \u2014 OOS", + "title": "Underground layer stack (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3002,7 +3002,7 @@ }, { "id": "g2-12a", - "title": "Underground \u2014 N-layer data model + save format (Game 2) \u2014 OOS", + "title": "Underground β€” N-layer data model + save format (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3013,7 +3013,7 @@ }, { "id": "g2-12b", - "title": "Underground \u2014 worldgen for L1/L2/L3 + cavern terrain (Game 2) \u2014 OOS", + "title": "Underground β€” worldgen for L1/L2/L3 + cavern terrain (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3026,7 +3026,7 @@ }, { "id": "g2-12c", - "title": "Underground \u2014 excavation action + cavern lifecycle (Game 2) \u2014 OOS", + "title": "Underground β€” excavation action + cavern lifecycle (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3040,7 +3040,7 @@ }, { "id": "g2-12d", - "title": "Underground \u2014 cross-layer movement, connection points + pathfinding (Game 2) \u2014 OOS", + "title": "Underground β€” cross-layer movement, connection points + pathfinding (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3054,7 +3054,7 @@ }, { "id": "g2-12e", - "title": "Underground \u2014 per-layer fog of war + vision (Game 2) \u2014 OOS", + "title": "Underground β€” per-layer fog of war + vision (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3068,7 +3068,7 @@ }, { "id": "g2-12f", - "title": "Underground \u2014 structural integrity + cross-layer collapse (Game 2) \u2014 OOS", + "title": "Underground β€” structural integrity + cross-layer collapse (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3082,7 +3082,7 @@ }, { "id": "g2-12g", - "title": "Underground \u2014 layer rendering + layer-switch UI (Game 2) \u2014 OOS", + "title": "Underground β€” layer rendering + layer-switch UI (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3097,7 +3097,7 @@ }, { "id": "g2-12h", - "title": "Underground \u2014 AI layer-awareness (Game 2) \u2014 OOS", + "title": "Underground β€” AI layer-awareness (Game 2) β€” OOS", "priority": "p3", "status": "oos", "scope": "game2", @@ -3113,7 +3113,7 @@ }, { "id": "g3-01", - "title": "Archons \u2014 Game 3 (Age of Elves)", + "title": "Archons β€” Game 3 (Age of Elves)", "priority": "p3", "status": "oos", "scope": "game3", @@ -3123,47 +3123,47 @@ }, { "id": "g3-02", - "title": "Life school spellbook \u2014 Game 3 (Age of Elves)", + "title": "Life school spellbook β€” Game 3 (Age of Elves)", "priority": "p3", "status": "oos", "scope": "game3", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "The Life school is one of four new magic schools added in Game 3 (\"Age of Elves\"). Life magic centers on healing, growth, sustenance, and binding \u2014 the nurturing force of the color pie. Spellbook TBD as Game 3 design progresses." + "summary": "The Life school is one of four new magic schools added 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": "g3-03", - "title": "Death school spellbook \u2014 Game 3 (Age of Elves)", + "title": "Death school spellbook β€” Game 3 (Age of Elves)", "priority": "p3", "status": "oos", "scope": "game3", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "The Death school is one of four new magic schools added in Game 3 (\"Age of Elves\"). Death magic centers on entropy, reanimation, drain, and endings \u2014 the consuming force of the color pie. Spellbook TBD as Game 3 design progresses." + "summary": "The Death school is one of four new magic schools added 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": "g3-04", - "title": "Chaos school spellbook \u2014 Game 3 (Age of Elves)", + "title": "Chaos school spellbook β€” Game 3 (Age of Elves)", "priority": "p3", "status": "oos", "scope": "game3", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "The Chaos school is one of four new magic schools added in Game 3 (\"Age of Elves\"). Chaos magic centers on disruption, transformation, wild chance, and unpredictability \u2014 the volatile force of the color pie. Spellbook TBD as Game 3 design progresses." + "summary": "The Chaos school is one of four new magic schools added 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": "g3-05", - "title": "Aether school spellbook \u2014 Game 3 (Age of Elves)", + "title": "Aether school spellbook β€” Game 3 (Age of Elves)", "priority": "p3", "status": "oos", "scope": "game3", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "The Aether school is one of four new magic schools added in Game 3 (\"Age of Elves\"). Aether magic centers on knowledge, artifice, time, and the arcane fabric itself \u2014 the intellectual force of the color pie. Spellbook TBD as Game 3 design progresses." + "summary": "The Aether school is one of four new magic schools added 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": "g3-06", - "title": "Arcane Ascension victory \u2014 Game 3 (Age of Elves)", + "title": "Arcane Ascension victory β€” Game 3 (Age of Elves)", "priority": "p3", "status": "oos", "scope": "game3", @@ -3173,27 +3173,27 @@ }, { "id": "g4-01", - "title": "Terran (Human) playable species \u2014 Game 4 (Age of Terrans)", + "title": "Terran (Human) playable species β€” Game 4 (Age of Terrans)", "priority": "p3", "status": "oos", "scope": "game4", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Terrans (Humans) are the Game 4 species, homeworld Terra. Drawn from Imanity (NGNL rank 16 \u2014 lowest-ranked Exceed, no innate magic). Their power comes not from magic but from adaptability, psionics, and religious organization. Game 4 is where the mundane-vs-magic tension peaks: humans have no arcane ability yet dominate through faith and mental force." + "summary": "Terrans (Humans) are the Game 4 species, homeworld Terra. Drawn from Imanity (NGNL rank 16 β€” lowest-ranked Exceed, no innate magic). Their power comes not from magic but from adaptability, psionics, and religious organization. Game 4 is where the mundane-vs-magic tension peaks: humans have no arcane ability yet dominate through faith and mental force." }, { "id": "g4-02", - "title": "Psionics ability system \u2014 Game 4 (Age of Terrans)", + "title": "Psionics ability system β€” Game 4 (Age of Terrans)", "priority": "p3", "status": "oos", "scope": "game4", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Psionics is the Terran-exclusive ability system \u2014 biological/mental in nature, not arcane. It operates outside the five magic schools introduced in Game 3. Humans have an innate psionic affinity (D&D / MoTM tradition). Psionics gives Terrans combat, diplomacy, and espionage capabilities that mirror but do not overlap with spellcasting. Bridges thematically into Game 5 (Gith are also psionic)." + "summary": "Psionics is the Terran-exclusive ability system β€” biological/mental in nature, not arcane. It operates outside the five magic schools introduced in Game 3. Humans have an innate psionic affinity (D&D / MoTM tradition). Psionics gives Terrans combat, diplomacy, and espionage capabilities that mirror but do not overlap with spellcasting. Bridges thematically into Game 5 (Gith are also psionic)." }, { "id": "g4-03", - "title": "Religious victory condition \u2014 Game 4 (Age of Terrans)", + "title": "Religious victory condition β€” Game 4 (Age of Terrans)", "priority": "p3", "status": "oos", "scope": "game4", @@ -3203,58 +3203,58 @@ }, { "id": "g5-01", - "title": "Phantasma playable species \u2014 Game 5 (Age of Ascension)", + "title": "Phantasma playable species β€” Game 5 (Age of Ascension)", "priority": "p3", "status": "oos", "scope": "game5", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Phantasma (NGNL rank 2) are pure spiritual beings native to the Ethereal Plane \u2014 entities of consciousness without physical form, aligned with Death school. They are ancient and immune to Terran religious spread. As rank-2 Exceed they are second only to the One True God, making them the most powerful conventional species in the series." + "summary": "Phantasma (NGNL rank 2) are pure spiritual beings native to the Ethereal Plane β€” entities of consciousness without physical form, aligned with Death school. They are ancient and immune to Terran religious spread. As rank-2 Exceed they are second only to the One True God, making them the most powerful conventional species in the series." }, { "id": "g5-02", - "title": "Fl\u00fcgel playable species \u2014 Game 5 (Age of Ascension)", + "title": "FlΓΌgel playable species β€” Game 5 (Age of Ascension)", "priority": "p3", "status": "oos", "scope": "game5", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Fl\u00fcgel (NGNL rank 5) are angel-weapons created by the One True God, aligned with Life school. Their existence directly conflicts with the Terran religious victory condition \u2014 humans spread faith in a divine, and Fl\u00fcgel *were made by* the closest thing to one. They are the central narrative tension of Game 5: the faith humans spread is real, but the Fl\u00fcgel are its answer. Immune to religious conversion; may have a competing religious spread mechanic of their own." + "summary": "FlΓΌgel (NGNL rank 5) are angel-weapons created by the One True God, aligned with Life school. Their existence directly conflicts with the Terran religious victory condition β€” humans spread faith in a divine, and FlΓΌgel *were made by* the closest thing to one. They are the central narrative tension of Game 5: the faith humans spread is real, but the FlΓΌgel are its answer. Immune to religious conversion; may have a competing religious spread mechanic of their own." }, { "id": "g5-03", - "title": "Gith playable species (Githyanki + Githzerai) \u2014 Game 5 (Age of Ascension)", + "title": "Gith playable species (Githyanki + Githzerai) β€” Game 5 (Age of Ascension)", "priority": "p3", "status": "oos", "scope": "game5", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Gith (MoTM / D&D) are psionic beings native to the Astral and Ethereal planes, aligned with Aether school. Two playable races: Githyanki (militant, planar conquerors) and Githzerai (monastic, psionic monks). Bridges Game 4's psionics system into Game 5 \u2014 Gith and Terrans share a psionic language, creating a unique diplomatic and conflict dynamic. Mid-to-high tier; resistant to but not immune to Terran religious spread." + "summary": "Gith (MoTM / D&D) are psionic beings native to the Astral and Ethereal planes, aligned with Aether school. Two playable races: Githyanki (militant, planar conquerors) and Githzerai (monastic, psionic monks). Bridges Game 4's psionics system into Game 5 β€” Gith and Terrans share a psionic language, creating a unique diplomatic and conflict dynamic. Mid-to-high tier; resistant to but not immune to Terran religious spread." }, { "id": "g5-04", - "title": "Demonia playable species \u2014 Game 5 (Age of Ascension)", + "title": "Demonia playable species β€” Game 5 (Age of Ascension)", "priority": "p3", "status": "oos", "scope": "game5", "updated_at": "2026-04-17", "blocked_by": [], - "summary": "Demonia (NGNL rank 11) are the demon species of the Ethereal Plane, aligned with Chaos school. Low-to-mid tier; they have no coherent belief system, making them the primary beachhead for Terran religious conversion \u2014 susceptible when isolated, violently opposed when organized. They are the \"wild card\" species of Game 5: unpredictable alliances, high aggression, and the most conventional military threat." + "summary": "Demonia (NGNL rank 11) are the demon species of the Ethereal Plane, aligned with Chaos school. Low-to-mid tier; they have no coherent belief system, making them the primary beachhead for Terran religious conversion β€” susceptible when isolated, violently opposed when organized. They are the \"wild card\" species of Game 5: unpredictable alliances, high aggression, and the most conventional military threat." }, { "id": "g6-01", - "title": "Naval combat \u2014 out-of-scope (post-v10)", + "title": "Naval combat β€” out-of-scope (post-v10)", "priority": "p3", "status": "oos", "scope": "post-v10", "owner": "", "updated_at": "2026-04-26", "blocked_by": [], - "summary": "Hex-based naval combat \u2014 water-tile movement for ships, ship unit types (transport / warship / etc.), naval-vs-naval and naval-vs-coastal-city combat, harbor / port buildings, sea-region map topology where applicable." + "summary": "Hex-based naval combat β€” water-tile movement for ships, ship unit types (transport / warship / etc.), naval-vs-naval and naval-vs-coastal-city combat, harbor / port buildings, sea-region map topology where applicable." }, { "id": "g6-02", - "title": "Caravan trade routes \u2014 out-of-scope (post-v10)", + "title": "Caravan trade routes β€” out-of-scope (post-v10)", "priority": "p3", "status": "oos", "scope": "post-v10", @@ -3265,23 +3265,23 @@ }, { "id": "p2-43a", - "title": "Rail-1 port \u2014 `_pick_culture_tradition` \u2192 mc-ai::tactical::culture_pick", + "title": "Rail-1 port β€” `_pick_culture_tradition` β†’ mc-ai::tactical::culture_pick", "priority": "p3", "status": "done", "scope": "game1", "updated_at": "2026-05-14", "blocked_by": [], - "summary": "Phase A of `p2-43` landed the AI culture-tradition picker as GDScript in\n`auto_play.gd::_pick_culture_tradition`. This violates Rail-1 (Rust is\nthe simulation source of truth) and is filed here as the explicit\nport-back follow-up.\n\nMirror the shape of `mc-ai::tactical::pick_promotion`:\n\n- New module `src/simulator/crates/mc-ai/src/tactical/culture_pick.rs`\n with `pub fn pick_culture_tradition(state: &PlayerState, available: &[TraditionId], priors: &PersonalityPriors) -> Option`.\n- Extend `PersonalityPriors` (in `policy.rs`) with `culture_pillar_weights: BTreeMap` and a single\n `culture_cost_bias: f32` knob \u2014 no parallel structs, no stringly maps.\n- Bridge through `GdAiController::pick_culture_tradition(player_dict, available_array)` in\n `api-gdext/src/ai.rs` (alongside the existing promotion bridge).\n- Replace the `_pick_culture_tradition` body in `auto_play.gd` with a\n one-liner delegating to the bridge. Delete the local scoring code \u2014\n Zero-Tech-Debt rail forbids leaving the GDScript shadow.\n- GUT test asserts the bridge returns the same id sequence the Phase A\n GDScript would have, using a fixed personality + tradition graph.\n- `cargo test -p mc-ai test_culture_pick_personality_weighting` green." + "summary": "Phase A of `p2-43` landed the AI culture-tradition picker as GDScript in\n`auto_play.gd::_pick_culture_tradition`. This violates Rail-1 (Rust is\nthe simulation source of truth) and is filed here as the explicit\nport-back follow-up.\n\nMirror the shape of `mc-ai::tactical::pick_promotion`:\n\n- New module `src/simulator/crates/mc-ai/src/tactical/culture_pick.rs`\n with `pub fn pick_culture_tradition(state: &PlayerState, available: &[TraditionId], priors: &PersonalityPriors) -> Option`.\n- Extend `PersonalityPriors` (in `policy.rs`) with `culture_pillar_weights: BTreeMap` and a single\n `culture_cost_bias: f32` knob β€” no parallel structs, no stringly maps.\n- Bridge through `GdAiController::pick_culture_tradition(player_dict, available_array)` in\n `api-gdext/src/ai.rs` (alongside the existing promotion bridge).\n- Replace the `_pick_culture_tradition` body in `auto_play.gd` with a\n one-liner delegating to the bridge. Delete the local scoring code β€”\n Zero-Tech-Debt rail forbids leaving the GDScript shadow.\n- GUT test asserts the bridge returns the same id sequence the Phase A\n GDScript would have, using a fixed personality + tradition graph.\n- `cargo test -p mc-ai test_culture_pick_personality_weighting` green." }, { "id": "p2-43a-followup-gdscript-delegation", - "title": "Shared infra \u2014 wire GdAiController into auto_play.gd so Rail-1 bridges can be one-liners", + "title": "Shared infra β€” wire GdAiController into auto_play.gd so Rail-1 bridges can be one-liners", "priority": "p3", "status": "done", "scope": "game1", "updated_at": "2026-05-15", "blocked_by": [], - "summary": "Rail-1 ports keep landing the Rust bridge (`GdAiController::pick_*`) but\ncannot collapse the GDScript call site to a one-liner because\n`auto_play.gd` does not currently instantiate a `GdAiController` \u2014 same\nconstraint that holds `_pick_research` inline today. This is a shared\ninfrastructure gap, not the responsibility of any individual port\nobjective.\n\nPer CLAUDE.md Rail-1 and `p0-26-ai-tactical-rust-port.md`, the resolution\nis a single wiring change: `auto_play.gd` (and any other AI driver\nscenes) grows a `GdAiController` member via the same path the tactical\nbridge will use. Once that lands, every accumulated `[ ] one-liner\ndelegation` bullet across the `p2-43*` family flips green in a single\nsweep." + "summary": "Rail-1 ports keep landing the Rust bridge (`GdAiController::pick_*`) but\ncannot collapse the GDScript call site to a one-liner because\n`auto_play.gd` does not currently instantiate a `GdAiController` β€” same\nconstraint that holds `_pick_research` inline today. This is a shared\ninfrastructure gap, not the responsibility of any individual port\nobjective.\n\nPer CLAUDE.md Rail-1 and `p0-26-ai-tactical-rust-port.md`, the resolution\nis a single wiring change: `auto_play.gd` (and any other AI driver\nscenes) grows a `GdAiController` member via the same path the tactical\nbridge will use. Once that lands, every accumulated `[ ] one-liner\ndelegation` bullet across the `p2-43*` family flips green in a single\nsweep." }, { "id": "p2-55f", @@ -3296,40 +3296,40 @@ }, { "id": "p3-01", - "title": "Courier-gated diplomacy \u2014 open borders + shared maps via tech-tiered courier units", + "title": "Courier-gated diplomacy β€” open borders + shared maps via tech-tiered courier units", "priority": "p3", "status": "done", "scope": "game1-stretch", "owner": "envoy", "updated_at": "2026-04-29", "blocked_by": [], - "summary": "Game 1 ships diplomacy-lite: peace/war toggle plus a single bilateral luxury\u2194gold\ntrade action (`mc-trade`). This objective expands the diplomatic surface with two\ntrade options gated on physical infrastructure rather than instant agreement, so\ninformation itself becomes a strategic resource that decays with distance and tech:\n\n1. **Open borders** \u2014 pay luxury or gold for the right to move units through\n another civ's territory for N turns. Instant effect; pure trade.\n\n2. **Shared map** \u2014 pay luxury or gold for the other civ's explored map for N\n turns. **Not instant**: the deal is gated on a courier link between capitals.\n Knowledge propagates at the courier's movement speed; the courier is killable\n mid-route (intercept = no map delivered, payment already made). The Courier\n unit family has tech-gated upgrade tiers, one per era from era_2 onward; later\n tiers shrink the delay window and shift the intercept surface from\n killing-the-unit to severing-the-infrastructure.\n\nThis is **scope: game1-stretch** \u2014 Game 1's stated scope is \"diplomacy-lite\", so\nthis objective is post-Early-Access content unless explicitly pulled forward." + "summary": "Game 1 ships diplomacy-lite: peace/war toggle plus a single bilateral luxury↔gold\ntrade action (`mc-trade`). This objective expands the diplomatic surface with two\ntrade options gated on physical infrastructure rather than instant agreement, so\ninformation itself becomes a strategic resource that decays with distance and tech:\n\n1. **Open borders** β€” pay luxury or gold for the right to move units through\n another civ's territory for N turns. Instant effect; pure trade.\n\n2. **Shared map** β€” pay luxury or gold for the other civ's explored map for N\n turns. **Not instant**: the deal is gated on a courier link between capitals.\n Knowledge propagates at the courier's movement speed; the courier is killable\n mid-route (intercept = no map delivered, payment already made). The Courier\n unit family has tech-gated upgrade tiers, one per era from era_2 onward; later\n tiers shrink the delay window and shift the intercept surface from\n killing-the-unit to severing-the-infrastructure.\n\nThis is **scope: game1-stretch** β€” Game 1's stated scope is \"diplomacy-lite\", so\nthis objective is post-Early-Access content unless explicitly pulled forward." }, { "id": "p3-03", - "title": "Courier route resolver \u2014 real hex pathfinding, per-tier movement, severable infrastructure", + "title": "Courier route resolver β€” real hex pathfinding, per-tier movement, severable infrastructure", "priority": "p3", "status": "done", "scope": "game1-stretch", "owner": "envoy", "updated_at": "2026-04-28", "blocked_by": [], - "summary": "p3-01 cycle 4 landed the **types** for courier-gated diplomacy\n(`DiplomaticAgreement` enum, `OpenBordersAgreement`, `SharedMapAgreement`,\n`CourierRoute`, `CourierMapView` trait, `step_shared_map_agreements` driver, six\nevent payloads). It also landed three lifecycle integration tests against a\n`MockMap` fixture that hard-codes intercept probability.\n\nThis objective owns the **physics layer** that lets those types resolve against\nthe actual game world: hex pathfinding from sender capital to receiver capital,\nper-tier movement-speed table feeding ETA calculations, and integration with\nseverable improvements (Steam Track, Resonance Wire, Beacon Tower) so that a\nmid-route pillage actually intercepts the courier.\n\nSplitting this out of p3-01 lets the parent objective close at \"data + types +\nlifecycle\" once AI/UI/tests/proof scenes land, while the route-resolver work\nstays its own bounded chunk of mc-trade \u2194 mc-map glue." + "summary": "p3-01 cycle 4 landed the **types** for courier-gated diplomacy\n(`DiplomaticAgreement` enum, `OpenBordersAgreement`, `SharedMapAgreement`,\n`CourierRoute`, `CourierMapView` trait, `step_shared_map_agreements` driver, six\nevent payloads). It also landed three lifecycle integration tests against a\n`MockMap` fixture that hard-codes intercept probability.\n\nThis objective owns the **physics layer** that lets those types resolve against\nthe actual game world: hex pathfinding from sender capital to receiver capital,\nper-tier movement-speed table feeding ETA calculations, and integration with\nseverable improvements (Steam Track, Resonance Wire, Beacon Tower) so that a\nmid-route pillage actually intercepts the courier.\n\nSplitting this out of p3-01 lets the parent objective close at \"data + types +\nlifecycle\" once AI/UI/tests/proof scenes land, while the route-resolver work\nstays its own bounded chunk of mc-trade ↔ mc-map glue." }, { "id": "p3-04", - "title": "Per-hex improvement layer in `mc-core` / `mc-turn` \u2014 anchor improvements at (col,row)", + "title": "Per-hex improvement layer in `mc-core` / `mc-turn` β€” anchor improvements at (col,row)", "priority": "p3", "status": "done", "scope": "game1-stretch", "owner": "envoy", "updated_at": "2026-04-28", "blocked_by": [], - "summary": "Improvements ship as data files (`public/resources/improvements/*.json`) but the\nsimulation has no per-hex anchor for them. Improvements currently live on\n`PlayerState.city_improvements: Vec>` (per-player / per-city,\nunanchored). The grid's per-tile struct stores terrain only \u2014 no improvement\nslot.\n\nThis blocks p3-03 acceptance bullets 4 (severance: pillaging Steam Track at hex\n(c,r) intercepts a courier whose route includes (c,r)), 5 (Hold-Network reroute\nwhen a Steam Track is severed), and the infrastructure-gating half of bullet 2\n(Steam Messenger requires Steam Track tiles on its route, Resonance\nTelegrapher requires Resonance Wire tiles).\n\nThis is also a foundational gap that will block other Game-1-stretch features\nbeyond couriers (tile-improvement pillaging, road network bonuses,\nfortification ZOC, defensive towers)." + "summary": "Improvements ship as data files (`public/resources/improvements/*.json`) but the\nsimulation has no per-hex anchor for them. Improvements currently live on\n`PlayerState.city_improvements: Vec>` (per-player / per-city,\nunanchored). The grid's per-tile struct stores terrain only β€” no improvement\nslot.\n\nThis blocks p3-03 acceptance bullets 4 (severance: pillaging Steam Track at hex\n(c,r) intercepts a courier whose route includes (c,r)), 5 (Hold-Network reroute\nwhen a Steam Track is severed), and the infrastructure-gating half of bullet 2\n(Steam Messenger requires Steam Track tiles on its route, Resonance\nTelegrapher requires Resonance Wire tiles).\n\nThis is also a foundational gap that will block other Game-1-stretch features\nbeyond couriers (tile-improvement pillaging, road network bonuses,\nfortification ZOC, defensive towers)." }, { "id": "p3-05a", - "title": "Civic state wrapper \u2014 typed CivicState added to PlayerState", + "title": "Civic state wrapper β€” typed CivicState added to PlayerState", "priority": "p3", "status": "done", "scope": "game1", @@ -3340,7 +3340,7 @@ }, { "id": "p3-05a-gdext-bridge", - "title": "GDExt bridge for CivicState \u2014 GdPlayer::civic query surface", + "title": "GDExt bridge for CivicState β€” GdPlayer::civic query surface", "priority": "p3", "status": "done", "scope": "game1", @@ -3393,7 +3393,7 @@ }, { "id": "p3-05e", - "title": "Civic modifier propagation \u2014 apply civic effects to per-city yields", + "title": "Civic modifier propagation β€” apply civic effects to per-city yields", "priority": "p3", "status": "done", "scope": "game1", @@ -3404,7 +3404,7 @@ }, { "id": "p3-06", - "title": "Civic anarchy \u2014 5-turn anarchy on axis switch", + "title": "Civic anarchy β€” 5-turn anarchy on axis switch", "priority": "p3", "status": "done", "scope": "game1", @@ -3415,7 +3415,7 @@ }, { "id": "p3-07a", - "title": "CV-of-wealth + Authority amplifier \u2192 inequality stat", + "title": "CV-of-wealth + Authority amplifier β†’ inequality stat", "priority": "p3", "status": "done", "scope": "game1", @@ -3428,7 +3428,7 @@ }, { "id": "p3-07b", - "title": "Four damage channels \u2014 Land/Water/Magic/Air emission from inequality", + "title": "Four damage channels β€” Land/Water/Magic/Air emission from inequality", "priority": "p3", "status": "done", "scope": "game1", @@ -3441,7 +3441,7 @@ }, { "id": "p3-10a", - "title": "Lair assault mode \u2014 enter-and-clear", + "title": "Lair assault mode β€” enter-and-clear", "priority": "p3", "status": "done", "scope": "game1", @@ -3454,7 +3454,7 @@ }, { "id": "p3-10b", - "title": "Lair siege mode \u2014 multi-turn pressure from adjacent", + "title": "Lair siege mode β€” multi-turn pressure from adjacent", "priority": "p3", "status": "done", "scope": "game1", @@ -3467,7 +3467,7 @@ }, { "id": "p3-10c", - "title": "Lair raid mode \u2014 grab-and-exit", + "title": "Lair raid mode β€” grab-and-exit", "priority": "p3", "status": "done", "scope": "game1", @@ -3491,7 +3491,7 @@ }, { "id": "p3-12", - "title": "Fauna combat stat derivation \u2014 regenerate from traits", + "title": "Fauna combat stat derivation β€” regenerate from traits", "priority": "p3", "status": "done", "scope": "game1", @@ -3502,7 +3502,7 @@ }, { "id": "p3-13a", - "title": "Extend meteorological events \u2014 drought, flood, dust_storm", + "title": "Extend meteorological events β€” drought, flood, dust_storm", "priority": "p3", "status": "done", "scope": "game1", @@ -3515,7 +3515,7 @@ }, { "id": "p3-13b", - "title": "Geological events \u2014 earthquake, volcanic_eruption, landslide", + "title": "Geological events β€” earthquake, volcanic_eruption, landslide", "priority": "p3", "status": "done", "scope": "game1", @@ -3526,7 +3526,7 @@ }, { "id": "p3-13c", - "title": "Biological events \u2014 plague, bloom, migration_pulse", + "title": "Biological events β€” plague, bloom, migration_pulse", "priority": "p3", "status": "done", "scope": "game1", @@ -3537,7 +3537,7 @@ }, { "id": "p3-13d", - "title": "Anomalous events \u2014 aurora, fog_bank, thermal_anomaly", + "title": "Anomalous events β€” aurora, fog_bank, thermal_anomaly", "priority": "p3", "status": "done", "scope": "game1", @@ -3548,7 +3548,7 @@ }, { "id": "p3-14", - "title": "Declarative game-start script + runner \u2014 data-driven, moddable opening sequence", + "title": "Declarative game-start script + runner β€” data-driven, moddable opening sequence", "priority": "p3", "status": "done", "scope": "game1", @@ -3559,7 +3559,7 @@ }, { "id": "p3-15", - "title": "Local hotseat multiplayer \u2014 multiple humans alternating on one device", + "title": "Local hotseat multiplayer β€” multiple humans alternating on one device", "priority": "p3", "status": "done", "scope": "game1", @@ -3577,7 +3577,7 @@ "owner": "warcouncil", "updated_at": "2026-06-25", "blocked_by": [], - "summary": "The canonical courier-diplomacy model (`COMMUNICATIONS.md` \u00a7\"War declaration\nsemantics\") assumes a player **dispatches a war-dec envelope** to enter war: the\n**sender** enters `War` immediately on dispatch and its units can attack the same\nturn; the **recipient** flips to `War` on delivery/interception; **defenders\nalways retaliate when struck** regardless of formal `RelationState`. Pairs start\nat **peace** (`mc-player-api/src/projection.rs::project_tactical_relations` defaults\nunset pairs to 0 = peace; `mc-player-api/src/comms_dispatch.rs` flips the shared\n`RelationState` cell to `War` only on delivery).\n\n**The war-declaration decision is now SHIPPED.** A new per-turn diplomacy step\n`decide_diplomacy` (`src/simulator/crates/mc-ai/src/tactical/diplomacy.rs:30`)\nevaluates each *discovered* rival against the clan's `aggression` axis and the\nown-vs-perceived military balance, and emits `Action::DeclareWar { target }`\n(`diplomacy.rs:69`) when conditions warrant. That action is routed through\n`mc_player_api::comms_dispatch::dispatch_war_declaration`\n(`mc-player-api/src/comms_dispatch.rs:59`) \u2014 the same envelope path the human\nwar-dec uses \u2014 so the **sender** enters `War` on dispatch and the **recipient**\nflips on delivery, exactly per `COMMUNICATIONS.md` \u00a7\"War declaration semantics\".\n\nIt is **unit-tested and proven to fire**:\n- 5 `mc-ai` diplomacy cases (`diplomacy.rs:140\u2013185`) cover discovered-weaker\n (declares), undiscovered (holds), already-at-war (holds), cautious-at-parity\n vs warmonger-strikes (axis differentiation), and no-army (holds).\n- The dispatch round-trip is covered by\n `ai_declare_war_maps_to_player_declare_war` (`mc-player-api/src/dispatch.rs:3068`):\n the AI's `DeclareWar` maps to the same `RelationState` mutation the human path\n produces.\n- **Live proof:** in hotseat self-play (seed 42) war-decs dispatch on first\n contact at turns 17/18.\n\n**Why this is still `partial` \u2014 the war-dec does not yet transform AI-vs-AI\ngames.** The decision-to-declare half is shipped, but full aggressive AI play is\nblocked on a **separate upstream gap: the AI does not scout/explore** (tracked by\n**p3-17**, AI exploration / frontier-seeking). Two consequences:\n1. War-decs fire only on a fleeting enemy-**unit** sighting, which is rare because\n idle military units never push toward unexplored territory.\n2. Even once at war, `collect_enemy_city_positions` (`tactical/movement.rs:402`)\n returns only **visible** enemy cities \u2014 which the AIs almost never see, because\n idle military units fall to `score_patrol_for_military` (`movement.rs:285`,\n scout-sweep of already-known cities / garrison next to friendly cities) with\n **no frontier-seeking**. So the army declares war but cannot find the enemy\n capital to march on.\n\nThe downstream at-war combat path (`is_at_war` gate, `locked_target` maneuver,\n`aggression`-tuned posture per `mc-ai/src/tactical/thresholds.rs`) already works\nonce a pair is at war and an enemy city is known \u2014 but discovery starves it.\n**p3-17 must land before personality `aggression` materially differentiates\nAI-vs-AI war outcomes.**" + "summary": "The canonical courier-diplomacy model (`COMMUNICATIONS.md` Β§\"War declaration\nsemantics\") assumes a player **dispatches a war-dec envelope** to enter war: the\n**sender** enters `War` immediately on dispatch and its units can attack the same\nturn; the **recipient** flips to `War` on delivery/interception; **defenders\nalways retaliate when struck** regardless of formal `RelationState`. Pairs start\nat **peace** (`mc-player-api/src/projection.rs::project_tactical_relations` defaults\nunset pairs to 0 = peace; `mc-player-api/src/comms_dispatch.rs` flips the shared\n`RelationState` cell to `War` only on delivery).\n\n**The war-declaration decision is now SHIPPED.** A new per-turn diplomacy step\n`decide_diplomacy` (`src/simulator/crates/mc-ai/src/tactical/diplomacy.rs:30`)\nevaluates each *discovered* rival against the clan's `aggression` axis and the\nown-vs-perceived military balance, and emits `Action::DeclareWar { target }`\n(`diplomacy.rs:69`) when conditions warrant. That action is routed through\n`mc_player_api::comms_dispatch::dispatch_war_declaration`\n(`mc-player-api/src/comms_dispatch.rs:59`) β€” the same envelope path the human\nwar-dec uses β€” so the **sender** enters `War` on dispatch and the **recipient**\nflips on delivery, exactly per `COMMUNICATIONS.md` Β§\"War declaration semantics\".\n\nIt is **unit-tested and proven to fire**:\n- 5 `mc-ai` diplomacy cases (`diplomacy.rs:140–185`) cover discovered-weaker\n (declares), undiscovered (holds), already-at-war (holds), cautious-at-parity\n vs warmonger-strikes (axis differentiation), and no-army (holds).\n- The dispatch round-trip is covered by\n `ai_declare_war_maps_to_player_declare_war` (`mc-player-api/src/dispatch.rs:3068`):\n the AI's `DeclareWar` maps to the same `RelationState` mutation the human path\n produces.\n- **Live proof:** in hotseat self-play (seed 42) war-decs dispatch on first\n contact at turns 17/18.\n\n**Why this is still `partial` β€” the war-dec does not yet transform AI-vs-AI\ngames.** The decision-to-declare half is shipped, but full aggressive AI play is\nblocked on a **separate upstream gap: the AI does not scout/explore** (tracked by\n**p3-17**, AI exploration / frontier-seeking). Two consequences:\n1. War-decs fire only on a fleeting enemy-**unit** sighting, which is rare because\n idle military units never push toward unexplored territory.\n2. Even once at war, `collect_enemy_city_positions` (`tactical/movement.rs:402`)\n returns only **visible** enemy cities β€” which the AIs almost never see, because\n idle military units fall to `score_patrol_for_military` (`movement.rs:285`,\n scout-sweep of already-known cities / garrison next to friendly cities) with\n **no frontier-seeking**. So the army declares war but cannot find the enemy\n capital to march on.\n\nThe downstream at-war combat path (`is_at_war` gate, `locked_target` maneuver,\n`aggression`-tuned posture per `mc-ai/src/tactical/thresholds.rs`) already works\nonce a pair is at war and an enemy city is known β€” but discovery starves it.\n**p3-17 must land before personality `aggression` materially differentiates\nAI-vs-AI war outcomes.**" }, { "id": "p3-17", @@ -3592,36 +3592,36 @@ }, { "id": "p3-18", - "title": "Water crossing \u2014 land-unit embarkation + naval transport", + "title": "Water crossing β€” land-unit embarkation + naval transport", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", "blocked_by": [], - "summary": "Land armies are permanently confined to their starting landmass: `mc-pathfinding`\ngates water purely by `UnitDomain` (`is_passable(\"ocean\", Land)` is hard-`false`),\nwith no tech override and no way to embark or be ferried. On maps where the two\ncapitals sit on separate landmasses (common \u2014 the start-balancer maximally\nseparates capitals on the 40\u00d724 `duel` map, often ocean-separated), conquest by a\nland army is impossible. This is the Civ \"embarkation\" tech gap.\n\nThe scaffolding is **half-built** (existing tech debt): `mc_combat::siege::\nembarked_defence_penalty` (halves an embarked unit's defence \u2014 the Civ-V/VI\nvulnerability rule) and a `transport` keyword in `combat.json` (\"carry up to 2\nland units across water\", on `dwarf_fortress_ship`) both exist, but neither is\nwired into movement/pathfinding, so they are dead." + "summary": "Land armies are permanently confined to their starting landmass: `mc-pathfinding`\ngates water purely by `UnitDomain` (`is_passable(\"ocean\", Land)` is hard-`false`),\nwith no tech override and no way to embark or be ferried. On maps where the two\ncapitals sit on separate landmasses (common β€” the start-balancer maximally\nseparates capitals on the 40Γ—24 `duel` map, often ocean-separated), conquest by a\nland army is impossible. This is the Civ \"embarkation\" tech gap.\n\nThe scaffolding is **half-built** (existing tech debt): `mc_combat::siege::\nembarked_defence_penalty` (halves an embarked unit's defence β€” the Civ-V/VI\nvulnerability rule) and a `transport` keyword in `combat.json` (\"carry up to 2\nland units across water\", on `dwarf_fortress_ship`) both exist, but neither is\nwired into movement/pathfinding, so they are dead." }, { "id": "p3-19", - "title": "Player \u2192 ecology feedback \u2014 harvesting & hunting deplete live populations (over-harvest \u2192 extinction)", + "title": "Player β†’ ecology feedback β€” harvesting & hunting deplete live populations (over-harvest β†’ extinction)", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", "blocked_by": [], - "summary": "The living-world ecology sim (mc-ecology / mc-worldsim) ticks per played turn\n(populations grow/collapse, migrate, emerge \u2014 see g2-08/g2-10, all done), and the\nplayer can harvest flora (chop, `mc-city::harvest`) and kill fauna (combat /\nlair clearing, `mc-combat::loot`). **But the two are decoupled:** harvesting and\nhunting do NOT reduce the ecology `PopulationSlot::population`\n(`mc-ecology/src/population.rs:9`) \u2014 fauna products are a read-only yield\n(`fauna_product.rs`), and chopping flips terrain + grants a one-shot yield without\ntouching the live population. Only the sim's own tier dynamics cut populations\n(`evolution.rs:1119 ecological_t5_reduces_fauna_populations`).\n\nNet: **you cannot over-harvest or over-hunt a species toward local extinction**,\nand abundance doesn't respond to player pressure \u2014 which undercuts the\n\"living-world is the Game-1 USP\" promise (the world should react to the player)." + "summary": "The living-world ecology sim (mc-ecology / mc-worldsim) ticks per played turn\n(populations grow/collapse, migrate, emerge β€” see g2-08/g2-10, all done), and the\nplayer can harvest flora (chop, `mc-city::harvest`) and kill fauna (combat /\nlair clearing, `mc-combat::loot`). **But the two are decoupled:** harvesting and\nhunting do NOT reduce the ecology `PopulationSlot::population`\n(`mc-ecology/src/population.rs:9`) β€” fauna products are a read-only yield\n(`fauna_product.rs`), and chopping flips terrain + grants a one-shot yield without\ntouching the live population. Only the sim's own tier dynamics cut populations\n(`evolution.rs:1119 ecological_t5_reduces_fauna_populations`).\n\nNet: **you cannot over-harvest or over-hunt a species toward local extinction**,\nand abundance doesn't respond to player pressure β€” which undercuts the\n\"living-world is the Game-1 USP\" promise (the world should react to the player)." }, { "id": "p3-20", - "title": "Weather affects scouting \u2014 vision/LoS penalty under storms, blizzards, dust", + "title": "Weather affects scouting β€” vision/LoS penalty under storms, blizzards, dust", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", "blocked_by": [], - "summary": "The runtime weather system (`mc-climate::weather`, six Game-1 types: Storm, Heat\nWave, Blizzard, Drought, Flood, Dust Storm) derives events per turn and applies a\n`movement_penalty` to units, with real unit HP damage wired into the played turn\n(`climate_effects.gd:125`). **But `WeatherEvent` carries no vision/line-of-sight\neffect** \u2014 weather slows movement and hurts units, but does not reduce scouting\nrange. So storms/blizzards/dust have no effect on exploration, fog reveal, or\nsurprise \u2014 a missed gameplay lever for the weather system." + "summary": "The runtime weather system (`mc-climate::weather`, six Game-1 types: Storm, Heat\nWave, Blizzard, Drought, Flood, Dust Storm) derives events per turn and applies a\n`movement_penalty` to units, with real unit HP damage wired into the played turn\n(`climate_effects.gd:125`). **But `WeatherEvent` carries no vision/line-of-sight\neffect** β€” weather slows movement and hurts units, but does not reduce scouting\nrange. So storms/blizzards/dust have no effect on exploration, fog reveal, or\nsurprise β€” a missed gameplay lever for the weather system." }, { "id": "p3-21", @@ -3632,7 +3632,7 @@ "owner": "warcouncil", "updated_at": "2026-06-25", "blocked_by": [], - "summary": "Fauna/flora migration EXISTS and ticks per played turn\n(`mc-ecology/src/biological.rs:59 MigrationPulse`, `engine.rs` per-turn migration;\ng2-10 done), but it is **population-pressure / carrying-capacity driven only** \u2014\nit does not respond to **weather/climate**. Drought should push herds toward\nwater, blizzards/heat-waves should thin or relocate populations, blooms (already\nclimate-linked) should pull flora spread. Today the living world migrates by\ncrowding, not by climate \u2014 a partial realization of the weather\u2194ecology coupling." + "summary": "Fauna/flora migration EXISTS and ticks per played turn\n(`mc-ecology/src/biological.rs:59 MigrationPulse`, `engine.rs` per-turn migration;\ng2-10 done), but it is **population-pressure / carrying-capacity driven only** β€”\nit does not respond to **weather/climate**. Drought should push herds toward\nwater, blizzards/heat-waves should thin or relocate populations, blooms (already\nclimate-linked) should pull flora spread. Today the living world migrates by\ncrowding, not by climate β€” a partial realization of the weather↔ecology coupling." }, { "id": "p3-22", @@ -3643,84 +3643,84 @@ "owner": "warcouncil", "updated_at": "2026-06-25", "blocked_by": [], - "summary": "The AI explores via frontier-seek with idle military units\n(`mc-ai/src/tactical/movement.rs:892`) and gives scout-typed units a special\nenemy-city patrol sweep \u2014 but it **never builds `dwarf_scout`**: the production\nladder (`mc-ai/src/tactical/production.rs:387`) queues founders, military, and\nbuildings, with workers as fallback, and no scout branch. Early-game discovery /\nfirst-contact therefore relies on diverting combat units, which is slower and\nweaker than a cheap dedicated scout." + "summary": "The AI explores via frontier-seek with idle military units\n(`mc-ai/src/tactical/movement.rs:892`) and gives scout-typed units a special\nenemy-city patrol sweep β€” but it **never builds `dwarf_scout`**: the production\nladder (`mc-ai/src/tactical/production.rs:387`) queues founders, military, and\nbuildings, with workers as fallback, and no scout branch. Early-game discovery /\nfirst-contact therefore relies on diverting combat units, which is slower and\nweaker than a cheap dedicated scout." }, { "id": "p3-23", - "title": "Trade richness \u2014 gold & strategic-resource trades with opponents", + "title": "Trade richness β€” gold & strategic-resource trades with opponents", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-26", "blocked_by": [], - "summary": "> **DISCOVERY (2026-06-25, verify-first):** the original premise was wrong. The\n> inter-player trade turn-integration is **DISABLED in the played game**, not\n> \"luxury-only\". `turn_manager.gd:287` has `Diplomacy.process_turn()` commented out\n> with a stale \"empty stub module\" note (diplomacy.gd was rebuilt but never\n> re-enabled). The *only* writer of `GameState.trade_ledger_json` is diplomacy.gd:32\n> inside that disabled call, so **no inter-player trades run at all** (luxury, strategic,\n> or gold). Worse, the `diplomacy.gd \u2194 GdTrade.process_trades` contract has drifted\n> in 3 places: (1) `_serialize_players` emits `{index, traded_luxuries, personality}`\n> but `process_trades` deserializes `Vec` (`player_index`,\n> `tile_luxuries`, `tile_strategics`, `trade_willingness`); (2) `process_trades`\n> returns `{ledger}` but diplomacy.gd reads `result[\"trade_ledger_json\"]`,\n> `[\"relation_changes\"]`, `[\"new_trades\"]`, `[\"broken_trades\"]`; (3) relations\n> advancement isn't returned. So enabling is NOT a one-line uncomment.\n\nThe **simulation logic is complete + cargo-tested** (luxury/strategic swaps + gold\nsales in `mc-trade`, 66/0) and the gold-flow application is wired (part A) \u2014 but it\nis all **inert in-game until the diplomacy turn-integration is revived**. The\n`tribute.rs` path stays Game-2 deferred." + "summary": "> **DISCOVERY (2026-06-25, verify-first):** the original premise was wrong. The\n> inter-player trade turn-integration is **DISABLED in the played game**, not\n> \"luxury-only\". `turn_manager.gd:287` has `Diplomacy.process_turn()` commented out\n> with a stale \"empty stub module\" note (diplomacy.gd was rebuilt but never\n> re-enabled). The *only* writer of `GameState.trade_ledger_json` is diplomacy.gd:32\n> inside that disabled call, so **no inter-player trades run at all** (luxury, strategic,\n> or gold). Worse, the `diplomacy.gd ↔ GdTrade.process_trades` contract has drifted\n> in 3 places: (1) `_serialize_players` emits `{index, traded_luxuries, personality}`\n> but `process_trades` deserializes `Vec` (`player_index`,\n> `tile_luxuries`, `tile_strategics`, `trade_willingness`); (2) `process_trades`\n> returns `{ledger}` but diplomacy.gd reads `result[\"trade_ledger_json\"]`,\n> `[\"relation_changes\"]`, `[\"new_trades\"]`, `[\"broken_trades\"]`; (3) relations\n> advancement isn't returned. So enabling is NOT a one-line uncomment.\n\nThe **simulation logic is complete + cargo-tested** (luxury/strategic swaps + gold\nsales in `mc-trade`, 66/0) and the gold-flow application is wired (part A) β€” but it\nis all **inert in-game until the diplomacy turn-integration is revived**. The\n`tribute.rs` path stays Game-2 deferred." }, { "id": "p3-24", - "title": "Rail-1 \u2014 port per-turn economy/happiness/climate glue logic from GDScript to Rust", + "title": "Rail-1 β€” port per-turn economy/happiness/climate glue logic from GDScript to Rust", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-06-25", "blocked_by": [], - "summary": "The core economy/happiness/event MATH is in Rust (mc-economy, mc-happiness,\nmc-city, mc-climate, mc-trade) \u2014 good Rail-1 health. **But real per-turn game\nlogic still lives in GDScript**, violating \"GDScript is presentation only\":\n\n- `economy.gd:66-72` computes gold *in GDScript* (building-gold sum, gold-per-pop,\n gold-from-mines) before/around the `GdEconomy` call \u2014 not a thin wrapper.\n- `happiness.gd` assembles + partially computes the happiness inputs (luxury map,\n building-effect collection) in GDScript.\n- `climate_effects.gd:125` mutates game state in GDScript (`unit.hp -= hp_loss`).\n- `turn_processor.gd` / `turn_manager.gd` own the per-turn ORCHESTRATION (sequence\n the Rust crates, dispatch events, apply results) in GDScript.\n\nThis is the same class of debt as the AI port (p0-26, done) \u2014 but for the\neconomy/happiness/event/turn surface." + "summary": "The core economy/happiness/event MATH is in Rust (mc-economy, mc-happiness,\nmc-city, mc-climate, mc-trade) β€” good Rail-1 health. **But real per-turn game\nlogic still lives in GDScript**, violating \"GDScript is presentation only\":\n\n- `economy.gd:66-72` computes gold *in GDScript* (building-gold sum, gold-per-pop,\n gold-from-mines) before/around the `GdEconomy` call β€” not a thin wrapper.\n- `happiness.gd` assembles + partially computes the happiness inputs (luxury map,\n building-effect collection) in GDScript.\n- `climate_effects.gd:125` mutates game state in GDScript (`unit.hp -= hp_loss`).\n- `turn_processor.gd` / `turn_manager.gd` own the per-turn ORCHESTRATION (sequence\n the Rust crates, dispatch events, apply results) in GDScript.\n\nThis is the same class of debt as the AI port (p0-26, done) β€” but for the\neconomy/happiness/event/turn surface." }, { "id": "p3-25", - "title": "Rail-1 \u2014 unify dual city model so view_json carries territory + trades (GDScript = view only)", + "title": "Rail-1 β€” unify dual city model so view_json carries territory + trades (GDScript = view only)", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", - "updated_at": "2026-06-26", + "updated_at": "2026-06-28", "blocked_by": [], - "summary": "> **Owner directive (2026-06-26):** \"gd should only be UI view of simulation\" +\n> \"simulator should provide everything\" + \"no stubs \u2014 prod code only\". A player-like\n> the headless adapter (or any UI) must get the *full* game state from the simulator's\n> projected view (`GdPlayerApi.view_json` \u2192 `PlayerView`), not by GDScript re-deriving\n> simulation facts.\n\n**Root cause (verify-first investigation).** The simulator holds **two parallel city\nmodels** ([api-gdext/src/city_slot.rs:3-6](../../src/simulator/api-gdext/src/city_slot.rs)):\n\n- `GdGameState.presentation_cities: Vec>` \u2014 the **authoritative**\n rich model: `owned_tiles`, `worked_tiles`, `position`, `culture_stored`, buildings\n ([mc-city/src/city.rs](../../src/simulator/crates/mc-city/src/city.rs)).\n- `GameState.players[pi].cities: Vec` \u2014 a **bench** model with NO\n territory ([mc-city/src/lib.rs:126](../../src/simulator/crates/mc-city/src/lib.rs)),\n explicitly *\"left untouched\"*. City position lives in the parallel\n `PlayerState.city_positions`.\n\n`project_view` ([mc-player-api/src/projection.rs](../../src/simulator/crates/mc-player-api/src/projection.rs))\nreads the **bench** model, so `view_json` is structurally blind to territory \u2014 and thus\nto worked tiles and to inter-player trades (which need owned-tile resource sourcing).\n`GdPlayerApi` (the headless harness) holds only `GameState` \u2014 no `presentation_cities` \u2014\nso the fix for the headless path is to give the bench state real territory, not to reach\ninto the rich `City`.\n\nConsequences observed: `CityView.owned_tiles` and `DiplomacyView.{open_borders,\nshared_map,agreements_active}` were hardcoded stubs in the projection; there are no\ntrade-deal fields on `DiplomacyView` at all; `mc-turn::process_trade_phase` sources trade\ninputs from bench proxies (`tile_strategics: Vec::new()`, `tile_luxuries` proxy) and does\nnot persist its computed ledger to `state.trade_ledger`. The live game's working trade\npath (p3-23) is GDScript-orchestrated and parses `trade_ledger_json` itself \u2014 a\npresentation-layer workaround that this objective supersedes for the headless/sim path.\n\nThe data needed *does* exist in Rust: `GridState.tile(col,row)\u2192{biome,quality}` +\n`mc_core::collectibles::tile_collectibles(biome,quality,rng)` resolve a tile's resources\ndeterministically. What's missing is (a) owned-tile territory in the bench state, (b) a\nresource-category catalog (luxury/strategic) in Rust, (c) persistence of swap/sale deals\nto `state.trade_ledger`, (d) projecting it all." + "summary": "> **Owner directive (2026-06-26):** \"gd should only be UI view of simulation\" +\n> \"simulator should provide everything\" + \"no stubs β€” prod code only\". A player-like\n> the headless adapter (or any UI) must get the *full* game state from the simulator's\n> projected view (`GdPlayerApi.view_json` β†’ `PlayerView`), not by GDScript re-deriving\n> simulation facts.\n\n**Root cause (verify-first investigation).** The simulator holds **two parallel city\nmodels** ([api-gdext/src/city_slot.rs:3-6](../../src/simulator/api-gdext/src/city_slot.rs)):\n\n- `GdGameState.presentation_cities: Vec>` β€” the **authoritative**\n rich model: `owned_tiles`, `worked_tiles`, `position`, `culture_stored`, buildings\n ([mc-city/src/city.rs](../../src/simulator/crates/mc-city/src/city.rs)).\n- `GameState.players[pi].cities: Vec` β€” a **bench** model with NO\n territory ([mc-city/src/lib.rs:126](../../src/simulator/crates/mc-city/src/lib.rs)),\n explicitly *\"left untouched\"*. City position lives in the parallel\n `PlayerState.city_positions`.\n\n`project_view` ([mc-player-api/src/projection.rs](../../src/simulator/crates/mc-player-api/src/projection.rs))\nreads the **bench** model, so `view_json` is structurally blind to territory β€” and thus\nto worked tiles and to inter-player trades (which need owned-tile resource sourcing).\n`GdPlayerApi` (the headless harness) holds only `GameState` β€” no `presentation_cities` β€”\nso the fix for the headless path is to give the bench state real territory, not to reach\ninto the rich `City`.\n\nConsequences observed: `CityView.owned_tiles` and `DiplomacyView.{open_borders,\nshared_map,agreements_active}` were hardcoded stubs in the projection; there are no\ntrade-deal fields on `DiplomacyView` at all; `mc-turn::process_trade_phase` sources trade\ninputs from bench proxies (`tile_strategics: Vec::new()`, `tile_luxuries` proxy) and does\nnot persist its computed ledger to `state.trade_ledger`. The live game's working trade\npath (p3-23) is GDScript-orchestrated and parses `trade_ledger_json` itself β€” a\npresentation-layer workaround that this objective supersedes for the headless/sim path.\n\nThe data needed *does* exist in Rust: `GridState.tile(col,row)β†’{biome,quality}` +\n`mc_core::collectibles::tile_collectibles(biome,quality,rng)` resolve a tile's resources\ndeterministically. What's missing is (a) owned-tile territory in the bench state, (b) a\nresource-category catalog (luxury/strategic) in Rust, (c) persistence of swap/sale deals\nto `state.trade_ledger`, (d) projecting it all." }, { "id": "p3-26", - "title": "Complete the headless simulator \u2014 close the live-vs-headless system gaps (loop done-criterion)", + "title": "Complete the headless simulator β€” close the live-vs-headless system gaps (loop done-criterion)", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", - "updated_at": "2026-06-26", + "updated_at": "2026-06-28", "blocked_by": [], - "summary": "> **Owner directive (2026-06-26):** the /loop \"continue until Game-1 done\" is **not\n> finished until the SIMULATOR is complete** \u2014 the headless Rust sim\n> (`mc-turn::TurnProcessor`, driven via `GdPlayerApi`/`magic_civ_*`) must play a full\n> self-play game with ALL systems, not the reduced subset shipped so far. See\n> [[project_loop_done_means_simulator_complete]].\n\nThe headless `step()` ([processor.rs:392+](../../src/simulator/crates/mc-turn/src/processor.rs))\ncurrently runs: trade (p3-25), economy, city_production (single queue), culture+border\nexpansion, tech/science, fauna encounters, combat/siege, diplomacy. Verified live via\n`magic_civ_view` (e.g. border expansion fired turn 0\u21921: `owned_tiles [[1,6]]\u2192[[1,6],[0,6]]`).\n\n**Gaps (each verified absent from the headless turn):**" + "summary": "> **Owner directive (2026-06-26):** the /loop \"continue until Game-1 done\" is **not\n> finished until the SIMULATOR is complete** β€” the headless Rust sim\n> (`mc-turn::TurnProcessor`, driven via `GdPlayerApi`/`magic_civ_*`) must play a full\n> self-play game with ALL systems, not the reduced subset shipped so far. See\n> [[project_loop_done_means_simulator_complete]].\n\nThe headless `step()` ([processor.rs:392+](../../src/simulator/crates/mc-turn/src/processor.rs))\ncurrently runs: trade (p3-25), economy, city_production (single queue), culture+border\nexpansion, tech/science, fauna encounters, combat/siege, diplomacy. Verified live via\n`magic_civ_view` (e.g. border expansion fired turn 0β†’1: `owned_tiles [[1,6]]β†’[[1,6],[0,6]]`).\n\n**Gaps (each verified absent from the headless turn):**" }, { "id": "p3-27", - "title": "Biosphere in the headless sim \u2014 ecology population + flora succession + marine ecology", + "title": "Biosphere in the headless sim β€” ecology population + flora succession + marine ecology", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", - "updated_at": "2026-06-26", + "updated_at": "2026-06-28", "blocked_by": [], - "summary": "Split from [[p3-26-complete-headless-simulator]]. The biological simulators exist as full\nRust crates \u2014 `mc-ecology` (`EcologyEngine`: per-tile fauna populations, predator/prey,\nevolution) and `mc-flora` (`FloraEngine`: per-tile vegetation + succession) \u2014 and the LIVE\ngame ticks them every turn (`EcologyState.tick` + `take_flora_transitions`,\nturn_manager.gd:315). But the **headless `mc-turn` step does NOT tick them** (verified: `0\nhits` for EcologyEngine/FloraEngine/flora in `processor.rs`). Only fauna *encounters*\n(combat) run headless. So the headless sim has no living biosphere for events/economy to\ninteract with." + "summary": "Split from [[p3-26-complete-headless-simulator]]. The biological simulators exist as full\nRust crates β€” `mc-ecology` (`EcologyEngine`: per-tile fauna populations, predator/prey,\nevolution) and `mc-flora` (`FloraEngine`: per-tile vegetation + succession) β€” and the LIVE\ngame ticks them every turn (`EcologyState.tick` + `take_flora_transitions`,\nturn_manager.gd:315). But the **headless `mc-turn` step does NOT tick them** (verified: `0\nhits` for EcologyEngine/FloraEngine/flora in `processor.rs`). Only fauna *encounters*\n(combat) run headless. So the headless sim has no living biosphere for events/economy to\ninteract with." }, { "id": "p3-28", - "title": "Modular turn architecture \u2014 break dep cycle, phase registry, boot-config DRY", + "title": "Modular turn architecture β€” break dep cycle, phase registry, boot-config DRY", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", - "updated_at": "2026-06-27", + "updated_at": "2026-06-28", "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." }, { "id": "p3-29", - "title": "Rail-1 turn unification \u2014 live game calls the Rust turn, delete GDScript orchestration", + "title": "Rail-1 turn unification β€” live game calls the Rust turn, delete GDScript orchestration", "priority": "p3", "status": "done", "scope": "game1", "owner": "warcouncil", - "updated_at": "2026-06-27", + "updated_at": "2026-06-28", "blocked_by": [], - "summary": "**The DRY / Rail-1 violation (verified 2026-06-27).** There are TWO turn orchestrations:\n- LIVE: `turn_manager.gd` \u2192 `turn_processor.gd::_process_*` (GDScript) + `EcologyState.tick` +\n `WorldsimState` \u2014 GDScript orchestrating the turn.\n- HEADLESS: `GdPlayerApi` \u2192 `mc_turn::TurnProcessor::step` (Rust).\n\nThe system *math* lives once in the Rust crates (DRY). The turn *orchestration* is duplicated \u2014\nand the p3-26/p3-27 work this session added happiness/healing/improvements/recipes/equipment/\necology to `mc-turn` while the live game still runs its GDScript copies (e.g. `EcologyState.tick`\nduplicates the new Rust `ecology_phase`). This session BUILT `mc-turn::step` into the complete\nsingle source of truth; this objective is the capstone that makes it actually single.\n\nThe bridge already exists: `GdTurnProcessor::step(GdGameState)` (api-gdext/src/lib.rs:6354) runs\n`mc_turn::TurnProcessor::step` on the LIVE game's state. The live turn just doesn't call it." + "summary": "**The DRY / Rail-1 violation (verified 2026-06-27).** There are TWO turn orchestrations:\n- LIVE: `turn_manager.gd` β†’ `turn_processor.gd::_process_*` (GDScript) + `EcologyState.tick` +\n `WorldsimState` β€” GDScript orchestrating the turn.\n- HEADLESS: `GdPlayerApi` β†’ `mc_turn::TurnProcessor::step` (Rust).\n\nThe system *math* lives once in the Rust crates (DRY). The turn *orchestration* is duplicated β€”\nand the p3-26/p3-27 work this session added happiness/healing/improvements/recipes/equipment/\necology to `mc-turn` while the live game still runs its GDScript copies (e.g. `EcologyState.tick`\nduplicates the new Rust `ecology_phase`). This session BUILT `mc-turn::step` into the complete\nsingle source of truth; this objective is the capstone that makes it actually single.\n\nThe bridge already exists: `GdTurnProcessor::step(GdGameState)` (api-gdext/src/lib.rs:6354) runs\n`mc_turn::TurnProcessor::step` on the LIVE game's state. The live turn just doesn't call it." }, { "id": "p3-30", @@ -3731,11 +3731,11 @@ "owner": "warcouncil", "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)` \u2192\n`src/game/engine/src/modules/ai/wild_creature_ai.gd` (302 LOC \u2014 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`) \u2014 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." + "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 \u2014 live games archive a GameHistory (per-turn TurnSnapshot + events) on game-over", + "title": "Replay recording β€” live games archive a GameHistory (per-turn TurnSnapshot + events) on game-over", "priority": "p3", "status": "missing", "scope": "game1-stretch", @@ -3746,7 +3746,7 @@ }, { "id": "p3-32", - "title": "Replay rendering \u2014 visual map playback (terrain + city/unit markers) from the archive", + "title": "Replay rendering β€” visual map playback (terrain + city/unit markers) from the archive", "priority": "p3", "status": "missing", "scope": "game1-stretch", @@ -4009,13 +4009,9 @@ } ], "remaining_by_lead": [ - { - "owner": "warcouncil", - "remaining": 7 - }, { "owner": "shipwright", "remaining": 2 } ] -} \ No newline at end of file +}