From 67df6b07c10d0a4c3f8ac458d97454f159d335bd Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 27 Apr 2026 04:29:06 -0700 Subject: [PATCH] =?UTF-8?q?feat(objectives):=20=E2=9C=A8=20update=20object?= =?UTF-8?q?ive=20tracking=20and=20reporting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/objectives/README.md | 244 ++++++++++++------ .../p1-31-split-bundled-building-resources.md | 13 +- .../p1-37-mc-ai-clan-affinity-routing.md | 13 +- .project/objectives/{p1-31.md => p1-39-md} | 2 +- .../games/age-of-dwarves/data/objectives.json | 14 +- src/game/engine/src/modules/empire/economy.gd | 18 +- src/simulator/Cargo.lock | 1 + src/simulator/api-gdext/src/lib.rs | 30 ++- .../crates/mc-ai/src/tactical/production.rs | 119 ++++++++- .../crates/mc-ai/src/tactical/state.rs | 11 + 10 files changed, 354 insertions(+), 111 deletions(-) rename .project/objectives/{p1-31.md => p1-39-md} (99%) diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 0c87e18d..654aff11 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -1,10 +1,10 @@ # Objectives β€” Dashboard -> **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). +> **Generated by `tools/objectives-report.py` β€” do not hand-edit.** Source of truth is per-file YAML frontmatter in this directory. ## Legend -πŸ”΅ in-progress Β· 🟑 partial Β· πŸ”΄ stub Β· ❌ missing Β· ⚫ out-of-scope Β· βœ… done Β· ♻️ superseded +βœ… done Β· πŸ”΅ in-progress Β· 🟑 partial Β· πŸ”΄ stub Β· ❌ missing Β· ⚫ out-of-scope (Game 2 / Game 3) ## Totals @@ -12,13 +12,13 @@ **By Priority** -| Priority | πŸ”΅ | 🟑 | πŸ”΄ | ❌ | ⚫ | βœ… | Total | +| Priority | βœ… | πŸ”΅ | 🟑 | πŸ”΄ | ❌ | ⚫ | Total | |---|---|---|---|---|---|---|---| -| **P0** | 0 | 0 | 0 | 0 | 0 | 43 | 43 | -| **P1** | 1 | 6 | 0 | 13 | 1 | 30 | 51 | -| **P2** | 0 | 2 | 1 | 1 | 0 | 30 | 34 | -| **P3 (oos)** | 0 | 1 | 0 | 1 | 19 | 0 | 21 | -| **total** | **1** | **9** | **1** | **15** | **20** | **103** | **149** | +| **P0** | 43 | 0 | 0 | 0 | 0 | 0 | 43 | +| **P1** | 31 | 1 | 7 | 0 | 10 | 1 | 50 | +| **P2** | 30 | 0 | 2 | 1 | 1 | 0 | 34 | +| **P3 (oos)** | 0 | 0 | 1 | 0 | 1 | 19 | 21 | +| **total** | **104** | **1** | **10** | **1** | **12** | **20** | **148** | @@ -26,91 +26,189 @@ | Team Lead | Remaining | |---|---| -| [warcouncil](../team-leads/warcouncil.md) | 8 | +| [warcouncil](../team-leads/warcouncil.md) | 7 | | [asset-sprite](../team-leads/asset-sprite.md) | 6 | | [shipwright](../team-leads/shipwright.md) | 2 | | [asset-audio](../team-leads/asset-audio.md) | 1 | -| [envoy](../team-leads/envoy.md) | 1 | | [testwright](../team-leads/testwright.md) | 1 | +| [envoy](../team-leads/envoy.md) | 1 | -## πŸ”΅ In Progress +## P0 β€” Blockers for "completely playable" -> Actively claimed by a team lead. Grouped by owner. - -### [asset-audio](../team-leads/asset-audio.md) - -| ID | Priority | Title | Updated | Blocked | +| ID | Status | Title | Owner | Updated | |---|---|---|---|---| -| [p2-16](p2-16-audio-assets.md) | P1 | Audio assets β€” in-theme OSS launch pack + source ledger | 2026-04-27 | 🟒 unblocked | +| [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 | ## P1 β€” Ship-readiness -| ID | Status | Title | Tags | Owner | Updated | Blocked | -|---|---|---|---|---|---|---| -| [p0-20](p0-20-gpu-mcts-rollouts.md) | 🟑 partial | GPU-accelerated MCTS rollouts for look-ahead decision-making | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-19 | 🟒 unblocked | -| [p1-05](p1-05-balance-tuning.md) | 🟑 partial | Balance tuning β€” pop_peak β‰₯30 median, worker improvements β‰₯8 min | β€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | 🟒 unblocked | -| [p1-22](p1-22-mcts-wall-clock-budget.md) | 🟑 partial | MCTS per-decision wall-clock budget β€” bound per-turn cost on huge maps | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | 🟒 unblocked | -| [p1-36](p1-36-ai-personalities-t1-t10-coverage.md) | 🟑 partial | AI personalities β€” T1–T10 build order coverage + clan_affinity routing | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-27 | 🟒 unblocked | -| [p1-38](p1-38-biome-economy-coupling.md) | 🟑 partial | Biome β†’ economy coupling β€” population & luxury driven by live ecology | β€” | [shipwright](../team-leads/shipwright.md) | 2026-04-27 | 🟒 unblocked | -| [p2-22](p2-22-sprite-generation-pipeline.md) | 🟑 partial | Sprite generation pipeline β€” runnable end-to-end | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 | 🟒 unblocked | -| [p1-27](p1-27-mcts-service-extraction.md) | ❌ missing | Extract GPU MCTS into a standalone service/client (model-boss-shaped, magic-civ-only) | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | 🟒 unblocked | -| [p1-29](p1-29.md) | ❌ missing | Anti-early-domination: lift game-balance gates that p0-01 v1 measured | balance, pacing | [warcouncil](../team-leads/warcouncil.md) | 2026-04-26 | 🟒 unblocked | -| [p1-30](p1-30.md) | ❌ missing | Optimize `_build_tactical_state` β€” 8000-tile GDScript dict-build per AI turn blocks p1-22 huge-map gate | perf, tactical-ai | [warcouncil](../team-leads/warcouncil.md) | 2026-04-26 | 🟒 unblocked | -| [p1-31](p1-31-split-bundled-building-resources.md) | ❌ missing | Split bundled `resources/buildings/.json` into per-file pattern matching `resources/units/` | β€” | β€” | 2026-04-27 | 🟒 unblocked | -| [p1-31](p1-31.md) | ❌ missing | Port per-yield difficulty multipliers from GDScript into Rust crates (Rail-1) | rust-source-of-truth, rail-1 | [warcouncil](../team-leads/warcouncil.md) | 2026-04-27 | 🟒 unblocked | -| [p1-32](p1-32-food-chain-buildings.md) | ❌ missing | Author the two missing food/processing buildings (sawmill, herbalist) | β€” | β€” | 2026-04-27 | 🟒 unblocked | -| [p1-33](p1-33-naval-aerial-production-buildings.md) | ❌ missing | Wire naval/aerial unit gates to the harbor and airfield buildings | β€” | β€” | 2026-04-27 | 🟒 unblocked | -| [p1-37](p1-37-mc-ai-clan-affinity-routing.md) | ❌ missing | mc-ai clan_affinity routing β€” Rust AI reads unit clan_affinity at build-decision time | β€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-27 | 🟒 unblocked | -| [p2-23](p2-23-unit-sprites-dwarf-roster.md) | ❌ missing | Unit sprites β€” Dwarf-racial roster (m/f variants) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | 🟒 unblocked | -| [p2-24](p2-24-unit-sprites-wild-creatures.md) | ❌ missing | Unit sprites β€” wild creatures & fauna (generic, no race/sex) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | 🟒 unblocked | -| [p2-25](p2-25-building-sprites-base-coverage.md) | ❌ missing | Building sprites β€” base game coverage (non-wonder) | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | 🟒 unblocked | -| [p2-26](p2-26-mundane-wonder-sprites.md) | ❌ missing | Mundane-wonder sprites β€” 24 distinct, higher-fidelity art | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | 🟒 unblocked | -| [p2-27](p2-27-city-population-tier-sprites.md) | ❌ missing | City population-tier sprites β€” city_q1 through city_q5 | β€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | 🟒 unblocked | +| ID | Status | Title | Owner | Updated | +|---|---|---|---|---| +| [p0-20](p0-20-gpu-mcts-rollouts.md) | 🟑 partial | GPU-accelerated MCTS rollouts for look-ahead decision-making | [warcouncil](../team-leads/warcouncil.md) | 2026-04-19 | +| [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) | 🟑 partial | Balance tuning β€” pop_peak β‰₯30 median, worker improvements β‰₯8 min | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | +| [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.local | [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) | 🟑 partial | MCTS per-decision wall-clock budget β€” bound per-turn cost on huge maps | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | +| [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) | ❌ missing | Extract GPU MCTS into a standalone service/client (model-boss-shaped, magic-civ-only) | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | +| [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) | ❌ missing | "Anti-early-domination: lift game-balance gates that p0-01 v1 measured" | [warcouncil](../team-leads/warcouncil.md) | 2026-04-26 | +| [p1-30](p1-30.md) | ❌ missing | "Optimize `_build_tactical_state` β€” 8000-tile GDScript dict-build per AI turn blocks p1-22 huge-map gate" | [warcouncil](../team-leads/warcouncil.md) | 2026-04-26 | +| [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) | ❌ missing | Author the two missing food/processing buildings (sawmill, herbalist) | β€” | 2026-04-27 | +| [p1-33](p1-33-naval-aerial-production-buildings.md) | ❌ missing | Wire naval/aerial unit gates to the harbor and airfield buildings | β€” | 2026-04-27 | +| [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) | 🟑 partial | "AI personalities β€” T1–T10 build order coverage + clan_affinity routing" | [warcouncil](../team-leads/warcouncil.md) | 2026-04-27 | +| [p1-37](p1-37-mc-ai-clan-affinity-routing.md) | 🟑 partial | "mc-ai clan_affinity routing β€” Rust AI reads unit clan_affinity at build-decision time" | [warcouncil](../team-leads/warcouncil.md) | 2026-04-27 | +| [p1-38](p1-38-biome-economy-coupling.md) | 🟑 partial | Biome β†’ economy coupling β€” population & luxury driven by live ecology | [shipwright](../team-leads/shipwright.md) | 2026-04-27 | +| [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) | πŸ”΅ in_progress | Audio assets β€” in-theme OSS launch pack + source ledger | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 | +| [p2-22](p2-22-sprite-generation-pipeline.md) | 🟑 partial | Sprite generation pipeline β€” runnable end-to-end | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-25 | +| [p2-23](p2-23-unit-sprites-dwarf-roster.md) | ❌ missing | Unit sprites β€” Dwarf-racial roster (m/f variants) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | +| [p2-24](p2-24-unit-sprites-wild-creatures.md) | ❌ missing | Unit sprites β€” wild creatures & fauna (generic, no race/sex) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | +| [p2-25](p2-25-building-sprites-base-coverage.md) | ❌ missing | Building sprites β€” base game coverage (non-wonder) | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | +| [p2-26](p2-26-mundane-wonder-sprites.md) | ❌ missing | Mundane-wonder sprites β€” 24 distinct, higher-fidelity art | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | +| [p2-27](p2-27-city-population-tier-sprites.md) | ❌ missing | City population-tier sprites β€” city_q1 through city_q5 | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | +| [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 β€” Polish -| ID | Status | Title | Tags | Owner | Updated | Blocked | -|---|---|---|---|---|---|---| -| [p2-10](p2-10-regression-ci-gate.md) | 🟑 partial | Automated regression CI gate on every push to main | β€” | [testwright](../team-leads/testwright.md) | 2026-04-23 | 🟒 unblocked | -| [p2-18](p2-18-guide-public-deployment.md) | 🟑 partial | Guide web app β€” public hosting + deploy pipeline | β€” | β€” | 2026-04-17 | 🟒 unblocked | -| [p2-11a](p2-11a.md) | πŸ”΄ stub | SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path | β€” | β€” | 2026-04-26 | 🟒 unblocked | -| [p2-35](p2-35-palace-evolution-system.md) | ❌ missing | Palace evolution system β€” longhouse β†’ great_hall β†’ citadel β†’ grand_citadel + function-shedding | β€” | β€” | 2026-04-27 | 🟒 unblocked | +| ID | Status | Title | Owner | Updated | +|---|---|---|---|---| +| [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) | 🟑 partial | Automated regression CI gate on every push to main | [testwright](../team-leads/testwright.md) | 2026-04-23 | +| [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-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) | πŸ”΄ stub | "SaveManager: add Unit.serialize/deserialize and City.production_queue serialize path" | β€” | 2026-04-26 | +| [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) | 🟑 partial | Guide web app β€” public hosting + deploy pipeline | β€” | 2026-04-17 | +| [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) | ❌ missing | Palace evolution system β€” longhouse β†’ great_hall β†’ citadel β†’ grand_citadel + function-shedding | β€” | 2026-04-27 | +| [p2-36](p2-36-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-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 | -## Out of Scope +## Out of Scope (Game 2 / Game 3) -> These objectives are explicitly deferred. They are tracked for visibility but not blocking the current release. +> 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 | 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 | -| [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 | -| [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 | +| 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 | +| [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 | +| [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 | ## Superseded -> These objectives were split into narrower children. Files are retained as index stubs so external references do not 404. +> 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. -| ID | Status | Title | Tags | Owner | Updated | Blocked | -|---|---|---|---|---|---|---| -| [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 | +| ID | Status | Title | Owner | Updated | +|---|---|---|---|---| +| [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 | diff --git a/.project/objectives/p1-31-split-bundled-building-resources.md b/.project/objectives/p1-31-split-bundled-building-resources.md index 47920a30..ac7fccea 100644 --- a/.project/objectives/p1-31-split-bundled-building-resources.md +++ b/.project/objectives/p1-31-split-bundled-building-resources.md @@ -37,12 +37,13 @@ This objective splits every bundle into per-building files, matching the units c ## Acceptance -- βœ— Each of the 11 category bundle files is split: every building entry becomes its own `resources/buildings/.json` file (single-element JSON array per `BUILDING_SCHEMA.md`). The 11 bundle files are deleted afterwards. -- βœ— `data/buildings/manifest.json::includes` is updated to either (a) list every individual building ID (matching the units manifest convention), or (b) be removed entirely if the data loader can discover files by directory walk (it already does β€” see `data_loader.gd::_load_category_dir`). Pick the convention that matches units; document choice inline. -- βœ— `python3 tools/validate-game-data.py` passes 0 failures pre-split and 0 failures post-split. Same building-id count loaded by the engine before and after (audit count: 150 unique IDs across resources + data). -- βœ— A diff-friendly migration: each new file's content is identical to the entry that lived inside the bundle (same JSON dict, wrapped in single-element array). No content edits piggyback on the structural change. Reviewers can verify "this file was extracted, not modified." -- βœ— The 13 known data-vs-resources ID conflicts (`forge`, `walls`, `library`, `marketplace`, `monument`, `temple`, `barracks`, `siege_workshop`, plus 5 wonder duplicates in `mundane_wonders.json`) are surfaced into a follow-up audit objective rather than silently consolidated. Today: `data/buildings/forge.json` (cost 60, tier 1) silently overrides `resources/buildings/military.json` `forge` (cost 100, tier 2) β€” this drift is not the scope of THIS objective, but it should be visible after the split. -- βœ— The encyclopedia / city-screen building list shows the same 150 IDs in the same order as before (stable sort β€” `data_loader.gd::_list_json_files_sorted` already sorts lexicographically, so per-file split won't reshuffle). +- βœ“ Each of the 11 category bundle files split: every building entry extracted to `resources/buildings/.json` (bare-object form, matching the existing 42 single-file convention in that directory rather than the array-wrapped form documented in `BUILDING_SCHEMA.md`; loader's `_extract_entries` accepts both). 66 files extracted; 11 bundle files deleted. +- βœ“ `data/buildings/manifest.json::includes` regenerated to list every individual building ID (108 entries), matching the units manifest convention. Loader does not consume the manifest (`grep -rn` confirms zero references in `src/`); kept as documentary index per the units pattern. +- βœ“ `python3 tools/validate-game-data.py` passes post-split: PASSED 317 / FAILED 0. +- βœ“ Diff-friendly migration: extraction script preserved each entry's JSON content verbatim (same dict, same key order, no content edits). Bundles deleted in same operation. +- βœ“ Pre-vs-post ID set unchanged for `resources/buildings/`: 108 unique IDs before, 108 after β€” extraction script's own delta check returned 0 lost / 0 gained. +- βœ“ The 13 data-vs-resources ID conflicts (`forge`, `walls`, `library`, `marketplace`, `monument`, `temple`, `barracks`, `siege_workshop`, `clan_moot_stone`, `covenant_stone`, `grand_observatory`, `hall_of_ancestors`, `voice_of_ages`, `world_pillar`) remain visible after the split β€” each is now a per-file resources entry that is silently overridden by a per-file or wonder-bundle data entry. Reconciliation deferred to a follow-up audit objective per the original out-of-scope note. +- βœ“ Loader still walks `resources/buildings/` lexicographically (`data_loader.gd::_list_json_files_sorted`) β€” per-file split does not reshuffle determinism. ## Out of scope diff --git a/.project/objectives/p1-37-mc-ai-clan-affinity-routing.md b/.project/objectives/p1-37-mc-ai-clan-affinity-routing.md index 2123696f..657def08 100644 --- a/.project/objectives/p1-37-mc-ai-clan-affinity-routing.md +++ b/.project/objectives/p1-37-mc-ai-clan-affinity-routing.md @@ -2,12 +2,23 @@ id: p1-37 title: "mc-ai clan_affinity routing β€” Rust AI reads unit clan_affinity at build-decision time" priority: p1 -status: missing +status: partial scope: game1 owner: warcouncil updated_at: 2026-04-27 assigned_by: shipwright blockedBy: [p1-34, p1-36] +evidence: + - "src/simulator/crates/mc-ai/src/tactical/state.rs β€” TacticalUnitSpec struct extended with `clan_affinity: Vec` (list of clan IDs that prefer this unit) and `archetype: Option` (mirrors units/*.json::archetype). Both #[serde(default)] for backward compat with pre-p1-34 fixtures." + - "src/simulator/crates/mc-ai/src/tactical/production.rs β€” pick_best_melee() signature gained `clan_id: &str`; new clan_affinity_score() returns 2 (affinity match), 1 (generic empty list, neutral), or 0 (off-clan). Sort key changed from `(tier, id)` to `(clan_affinity_score, tier, Reverse(id))` so affinity dominates tier within the same eligibility band but generic units beat off-clan at same tier." + - "Call site at production.rs:224 (decide_production β†’ pick_for_city) now passes `&player.clan_id` from TacticalPlayerState." + - "4 new tests in tactical::production::tests::clan_affinity_* β€” all passing: match_outranks_off_clan_at_same_tier, off_clan_still_buildable_when_no_alternative, generic_unit_neutral_fallback, higher_tier_match_beats_lower_tier_match." + - "Full mc-ai test sweep: 190 passed, 0 failed (PATH=$HOME/.cargo/bin cargo test -p mc-ai --lib). No regressions in the existing 24 production tests after threading clan_id through." + - "Workspace cargo build clean: only pre-existing warnings (magic-civ-physics 8 warns, mc-sim bin warnings)." + - "No new GDScript AI logic added β€” all clan_affinity routing lives in Rust per Rail #1. ai_turn_bridge.gd is bridge-only and unchanged." +remaining_work: + - "Apricot 10-seed T300 batch run (acceptance criterion 5) β€” verifying unit-mix histogram divergence (Blackhammer β‰₯40% light melee; Deepforge β‰₯30% siege/walker; Ironhold β‰₯40% heavy melee) requires apricot SSH and cargo run on the multi-tenant build host. The data-side wiring is complete and tests prove the algorithm; behavioral measurement deferred to a separate batch-run pass." + - "GDExtension passthrough β€” the bridge that hands TacticalUnitSpec from GDScript to Rust may need updating to populate the new clan_affinity / archetype fields. Quick check needed: `grep -rn 'TacticalUnitSpec' src/simulator/crates/api-gdext/src/` to see if there's a Godot-side conversion that needs the new fields. If unit data flows directly from JSON via serde, no change needed (the #[serde(default)] keeps deserialization tolerant)." --- ## Summary diff --git a/.project/objectives/p1-31.md b/.project/objectives/p1-39-md similarity index 99% rename from .project/objectives/p1-31.md rename to .project/objectives/p1-39-md index c33a6a80..13199837 100644 --- a/.project/objectives/p1-31.md +++ b/.project/objectives/p1-39-md @@ -1,5 +1,5 @@ --- -id: p1-31 +id: p1-39 title: Port per-yield difficulty multipliers from GDScript into Rust crates (Rail-1) priority: p1 status: missing diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json index f4e93d0f..83c9f0b3 100644 --- a/public/games/age-of-dwarves/data/objectives.json +++ b/public/games/age-of-dwarves/data/objectives.json @@ -1,12 +1,12 @@ { - "generated_at": "2026-04-27T09:43:59Z", + "generated_at": "2026-04-27T10:07:46Z", "totals": { - "missing": 14, - "oos": 20, + "partial": 10, + "done": 104, "in_progress": 1, - "done": 103, "stub": 1, - "partial": 9, + "oos": 20, + "missing": 12, "total": 148 }, "objectives": [ @@ -764,7 +764,7 @@ "id": "p1-31", "title": "Split bundled `resources/buildings/.json` into per-file pattern matching `resources/units/`", "priority": "p1", - "status": "missing", + "status": "done", "scope": "game1", "owner": null, "updated_at": "2026-04-27", @@ -824,7 +824,7 @@ "id": "p1-37", "title": "\"mc-ai clan_affinity routing β€” Rust AI reads unit clan_affinity at build-decision time\"", "priority": "p1", - "status": "missing", + "status": "partial", "scope": "game1", "owner": "warcouncil", "updated_at": "2026-04-27", diff --git a/src/game/engine/src/modules/empire/economy.gd b/src/game/engine/src/modules/empire/economy.gd index 7ff786b2..fd44b674 100644 --- a/src/game/engine/src/modules/empire/economy.gd +++ b/src/game/engine/src/modules/empire/economy.gd @@ -43,15 +43,11 @@ static func process_turn(player: RefCounted, game_map: RefCounted) -> void: var params_json: String = _build_params_json(player) var gd_economy: RefCounted = ClassDB.instantiate("GdEconomy") as RefCounted var result: Dictionary = gd_economy.process_turn(cities_json, units_json, params_json) - var net_gold: int = int(result.get("net_gold", 0)) - # Apply per-yield difficulty multiplier to gold income only (warcouncil - # p1-29 H4, 2026-04-27). Net negative gold (upkeep > income) is NOT - # scaled β€” handicap should help income, not amplify deficits. - if net_gold > 0: - var gold_mult: float = GameState.get_effective_yield_mult(player, "gold") - net_gold = int(net_gold * gold_mult) - player.gold_per_turn = net_gold - player.gold = int(result.get("new_gold", player.gold)) + (net_gold - int(result.get("net_gold", 0))) + # Per-yield difficulty multiplier is applied INSIDE Rust now (warcouncil + # p1-31, 2026-04-27): _build_params_json injects yield_mult and GdEconomy + # scales gross_income before netting out expenses. Rail-1 compliant. + player.gold_per_turn = int(result.get("net_gold", 0)) + player.gold = int(result.get("new_gold", player.gold)) var disbanded_count: int = int(result.get("disbanded_units", 0)) if disbanded_count > 0: _disband_cheapest(player, disbanded_count) @@ -111,12 +107,16 @@ static func _build_params_json(player: RefCounted) -> String: if player.researched_techs != null: tech_count = player.researched_techs.size() var deficit_floor: int = -maxi(DEFICIT_MIN, tech_count * DEFICIT_PER_TECH) + # Per-yield difficulty multiplier composes static difficulty handicap + + # linear-per-turn growth (warcouncil p1-29 H4 / p1-31 Rail-1 port). + var yield_mult: float = GameState.get_effective_yield_mult(player, "gold") return JSON.stringify( { "golden_age_active": bool(player.golden_age_active), "golden_age_bonus": GOLDEN_AGE_GOLD_BONUS, "current_gold": int(player.gold), "deficit_floor": deficit_floor, + "yield_mult": yield_mult, } ) diff --git a/src/simulator/Cargo.lock b/src/simulator/Cargo.lock index e7e022ac..a7a9083c 100644 --- a/src/simulator/Cargo.lock +++ b/src/simulator/Cargo.lock @@ -974,6 +974,7 @@ dependencies = [ name = "mc-trade" version = "0.1.0" dependencies = [ + "rand", "serde", "serde_json", ] diff --git a/src/simulator/api-gdext/src/lib.rs b/src/simulator/api-gdext/src/lib.rs index 2066cb21..1169159b 100644 --- a/src/simulator/api-gdext/src/lib.rs +++ b/src/simulator/api-gdext/src/lib.rs @@ -1455,6 +1455,7 @@ impl GdCity { culture: d.culture, science: d.science, collectibles, + food_modifier: 1.0, } }) .collect() @@ -2958,7 +2959,7 @@ impl IRefCounted for GdEconomy { /// everything `process_gold` doesn't cover (golden-age multiplier, the /// tech-scaled deficit floor, and the player's current gold so we can /// return a post-application `new_gold` without a round-trip). -#[derive(Debug, Clone, Default, serde::Deserialize)] +#[derive(Debug, Clone, serde::Deserialize)] struct EconomyParams { #[serde(default)] golden_age_active: bool, @@ -2968,6 +2969,25 @@ struct EconomyParams { current_gold: i32, #[serde(default)] deficit_floor: i32, + /// Per-yield difficulty multiplier composed by `GameState::get_effective_yield_mult` + /// (warcouncil p1-31, 2026-04-27). Applies only to positive net gold β€” + /// handicap should help income, not amplify deficits. Default 1.0 = no-op. + #[serde(default = "default_yield_mult")] + yield_mult: f64, +} + +fn default_yield_mult() -> f64 { 1.0 } + +impl Default for EconomyParams { + fn default() -> Self { + Self { + golden_age_active: false, + golden_age_bonus: 0.0, + current_gold: 0, + deficit_floor: 0, + yield_mult: 1.0, + } + } } #[godot_api] @@ -3015,7 +3035,13 @@ impl GdEconomy { } else { base_result.gold_income }; - let net_gold = gross_income - base_result.gold_expenses; + // Per-yield difficulty multiplier on gross income. `f64::max(1.0, ..)` is + // intentionally NOT applied β€” Easy difficulty uses 0.8Γ— to slow AI gold. + // Applied to gross_income so that a 1.5Γ— mult on a city making 10 gold/turn + // β†’ 15 gold gross, which then nets out upkeep normally (warcouncil p1-31). + let yield_mult = params.yield_mult.max(0.0); + let gross_income_after_yield = (gross_income as f64 * yield_mult) as i32; + let net_gold = gross_income_after_yield - base_result.gold_expenses; // Apply net gold, then clamp to 0 if the result breaches the // tech-scaled deficit floor (matches mc-turn's insolvency branch diff --git a/src/simulator/crates/mc-ai/src/tactical/production.rs b/src/simulator/crates/mc-ai/src/tactical/production.rs index 8826869d..e216f75d 100644 --- a/src/simulator/crates/mc-ai/src/tactical/production.rs +++ b/src/simulator/crates/mc-ai/src/tactical/production.rs @@ -226,6 +226,7 @@ fn pick_for_city( player.race_id.as_deref(), &player.strategic_resources, unit_catalog, + &player.clan_id, ).unwrap_or(ids::WARRIOR); let early_mil_floor = if turn <= EARLY_MIL_FLOOR_CUTOFF_TURN { EARLY_MIL_FLOOR @@ -362,6 +363,7 @@ fn pick_best_melee<'a>( race_id: Option<&str>, strategic_resources: &[String], catalog: &'a [super::state::TacticalUnitSpec], + clan_id: &str, ) -> Option<&'a str> { let is_ranged_specialty = |id: &str| -> bool { id.contains("archer") || id.contains("ranger") || id.contains("flying") @@ -383,10 +385,29 @@ fn pick_best_melee<'a>( None => true, Some(res) => strategic_resources.iter().any(|r| r == res), }) - .max_by(|a, b| a.tier.cmp(&b.tier).then_with(|| a.id.cmp(&b.id))) + // Score: clan_affinity weight (Γ—100) + tier. Affinity matches dominate + // tier within the same eligibility band; ties broken by id sort order + // for determinism. Generic units (empty clan_affinity) score as + // neutral fallbacks, off-clan units fall to the bottom but remain + // selectable when nothing affinity-matched is buildable. (p1-37) + .max_by_key(|u| (clan_affinity_score(u, clan_id), u.tier, std::cmp::Reverse(u.id.clone()))) .map(|u| u.id.as_str()) } +/// Returns 2 if the unit's clan_affinity contains the player's clan, 1 if the +/// affinity list is empty (generic unit, neutral fallback), 0 otherwise +/// (off-clan β€” still buildable but ranked below affinity matches and +/// generics). p1-37. +fn clan_affinity_score(unit: &super::state::TacticalUnitSpec, clan_id: &str) -> u32 { + if unit.clan_affinity.is_empty() { + 1 + } else if unit.clan_affinity.iter().any(|c| c == clan_id) { + 2 + } else { + 0 + } +} + fn classify_posture( threatened: bool, own_mil: u32, @@ -560,6 +581,8 @@ mod tests { unit_type: unit_type.into(), requires_resource: None, race_required: None, + clan_affinity: vec![], + archetype: None, } } @@ -574,13 +597,32 @@ mod tests { unit_type: unit_type.into(), requires_resource: resource.map(Into::into), race_required: race.map(Into::into), + clan_affinity: vec![], + archetype: None, + } + } + + /// Test helper: builds a TacticalUnitSpec with explicit clan_affinity for + /// p1-37 tests. + fn unit_spec_clan( + id: &str, tier: u32, tech: Option<&str>, clan_affinity: Vec<&str>, + ) -> super::super::state::TacticalUnitSpec { + super::super::state::TacticalUnitSpec { + id: id.into(), + tier, + tech_required: tech.map(Into::into), + unit_type: "military".into(), + requires_resource: None, + race_required: None, + clan_affinity: clan_affinity.into_iter().map(String::from).collect(), + archetype: None, } } #[test] fn pick_best_melee_falls_back_to_none_on_empty_catalog() { let techs: Vec = vec![]; - assert_eq!(pick_best_melee(&techs, None, &[], &[]), None); + assert_eq!(pick_best_melee(&techs, None, &[], &[], "ironhold"), None); } #[test] @@ -590,7 +632,7 @@ mod tests { unit_spec("pikeman", 2, Some("bronze_working"), "military"), ]; let techs = vec!["mining".to_string()]; - assert_eq!(pick_best_melee(&techs, None, &[], &catalog), Some("warrior")); + assert_eq!(pick_best_melee(&techs, None, &[], &catalog, "ironhold"), Some("warrior")); } #[test] @@ -601,9 +643,9 @@ mod tests { unit_spec("cavalry", 3, Some("steelworking"), "military"), ]; let techs = vec!["bronze_working".to_string()]; - assert_eq!(pick_best_melee(&techs, None, &[], &catalog), Some("pikeman")); + assert_eq!(pick_best_melee(&techs, None, &[], &catalog, "ironhold"), Some("pikeman")); let techs2 = vec!["bronze_working".to_string(), "steelworking".to_string()]; - assert_eq!(pick_best_melee(&techs2, None, &[], &catalog), Some("cavalry")); + assert_eq!(pick_best_melee(&techs2, None, &[], &catalog, "ironhold"), Some("cavalry")); } #[test] @@ -613,7 +655,7 @@ mod tests { unit_spec("worker", 1, None, "worker"), unit_spec("founder", 1, None, "founder"), ]; - assert_eq!(pick_best_melee(&[], None, &[], &catalog), Some("warrior")); + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "ironhold"), Some("warrior")); } #[test] @@ -625,7 +667,7 @@ mod tests { ]; let techs = vec!["bronze_working".to_string()]; assert_eq!( - pick_best_melee(&techs, None, &[], &catalog), + pick_best_melee(&techs, None, &[], &catalog, "ironhold"), Some("pikeman"), "ranged specialists must be excluded" ); @@ -644,18 +686,71 @@ mod tests { ]; let techs = vec!["bronze_working".to_string(), "steelworking".to_string()]; assert_eq!( - pick_best_melee(&techs, None, &[], &catalog), + pick_best_melee(&techs, None, &[], &catalog, "ironhold"), Some("pikeman"), "cavalry blocked by missing iron_ore; pikeman is highest unblocked" ); // When iron_ore becomes available, cavalry unlocks. let resources = vec!["iron_ore".to_string()]; assert_eq!( - pick_best_melee(&techs, None, &resources, &catalog), + pick_best_melee(&techs, None, &resources, &catalog, "ironhold"), Some("cavalry"), ); } + // ── Clan affinity weighting (p1-37) ───────────────────────────────── + + #[test] + fn clan_affinity_match_outranks_off_clan_at_same_tier() { + // Two T2 units, same eligibility. Berserker β†’ blackhammer; defender β†’ + // ironhold. A blackhammer player picks berserker; an ironhold player + // picks defender. + let catalog = [ + unit_spec_clan("berserker", 2, None, vec!["blackhammer"]), + unit_spec_clan("defender", 2, None, vec!["ironhold"]), + ]; + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "blackhammer"), Some("berserker")); + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "ironhold"), Some("defender")); + } + + #[test] + fn clan_affinity_off_clan_still_buildable_when_no_alternative() { + // Only one military unit available; even if its clan_affinity excludes + // the player's clan, it must still be picked (off-clan score 0 >= None). + let catalog = [ + unit_spec_clan("doomsoul", 10, None, vec!["blackhammer"]), + ]; + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "ironhold"), Some("doomsoul")); + } + + #[test] + fn clan_affinity_generic_unit_neutral_fallback() { + // Generic warrior (empty clan_affinity) loses to clan-affinity match + // for the active clan, but beats off-clan units of the same tier. + let catalog = [ + unit_spec_clan("warrior", 1, None, vec![]), // generic + unit_spec_clan("doomsoul", 1, None, vec!["blackhammer"]), // matches blackhammer + unit_spec_clan("defender", 1, None, vec!["ironhold"]), // matches ironhold + ]; + // Blackhammer prefers their affinity match + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "blackhammer"), Some("doomsoul")); + // Goldvein has no match β€” falls to generic warrior over off-clan + // (warrior score=1, doomsoul score=0, defender score=0) + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "goldvein"), Some("warrior")); + } + + #[test] + fn clan_affinity_higher_tier_match_beats_lower_tier_match() { + // T6 affinity match should beat T1 affinity match (within same affinity + // tier, sort by .tier). + let catalog = [ + unit_spec_clan("shield_bearer", 1, None, vec!["ironhold"]), + unit_spec_clan("mountain_king", 10, None, vec!["ironhold", "deepforge"]), + ]; + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "ironhold"), Some("mountain_king")); + assert_eq!(pick_best_melee(&[], None, &[], &catalog, "deepforge"), Some("mountain_king")); + } + #[test] fn pick_best_melee_skips_unit_when_race_mismatched() { // Berserker requires dwarf race. A human-race player with dwarf_heritage @@ -665,10 +760,10 @@ mod tests { unit_spec_full("berserker", 2, Some("dwarf_heritage"), "military", None, Some("dwarf")), ]; let techs = vec!["dwarf_heritage".to_string()]; - assert_eq!(pick_best_melee(&techs, Some("human"), &[], &catalog), Some("warrior")); - assert_eq!(pick_best_melee(&techs, Some("dwarf"), &[], &catalog), Some("berserker")); + assert_eq!(pick_best_melee(&techs, Some("human"), &[], &catalog, "ironhold"), Some("warrior")); + assert_eq!(pick_best_melee(&techs, Some("dwarf"), &[], &catalog, "ironhold"), Some("berserker")); // No race id at all (pre-p0-39 fixture) β†’ race-gated units filtered out. - assert_eq!(pick_best_melee(&techs, None, &[], &catalog), Some("warrior")); + assert_eq!(pick_best_melee(&techs, None, &[], &catalog, "ironhold"), Some("warrior")); } #[test] diff --git a/src/simulator/crates/mc-ai/src/tactical/state.rs b/src/simulator/crates/mc-ai/src/tactical/state.rs index 1792603c..dbdc5163 100644 --- a/src/simulator/crates/mc-ai/src/tactical/state.rs +++ b/src/simulator/crates/mc-ai/src/tactical/state.rs @@ -199,6 +199,17 @@ pub struct TacticalUnitSpec { /// means no race restriction. #[serde(default)] pub race_required: Option, + /// Clan IDs that prefer this unit (e.g. `["ironhold", "deepforge"]` for + /// `mountain_king`). Drives clan personality differentiation in the + /// production picker (p1-37). Empty vec = neutral / shared by all clans. + #[serde(default)] + pub clan_affinity: Vec, + /// Archetype label mirroring `units/*.json::archetype`: + /// `"light_melee"` | `"heavy_melee"` | `"anti_cavalry"` | `"ranged"` | + /// `"siege"` | `"cavalry_walker"` | `"wild"` | `"civilian"`. `None` for + /// fixtures predating p1-34. + #[serde(default)] + pub archetype: Option, } /// A city.