diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index adaf63b2..c874b828 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -205,6 +205,7 @@ | [p1-42](p1-42-ai-full-building-catalog.md) | 🟔 partial | P1 | AI must consider the full 155-building catalog, not the hardcoded 8-id ladder | — | 🟢 | | [p1-43](p1-43-building-stacking-upgrade.md) | 🟔 partial | P1 | Building stacking — per-category upgrade chains (military / science / culture / production / etc.) | — | 🟢 | | [p1-43b](p1-43b-deep-chain-authoring.md) | āœ… done | P1 | Deep chain authoring — fill T6/T7/T8/T9/T10 building tiers across the 5 short chains | — | 🟢 | +| [p1-43c](p1-43c-chain-ladders-and-ui.md) | šŸ”“ stub | P1 | p1-43 follow-ups — chain ladder authoring, AI stack scoring, city UI upgrade surface, GUT bridge test | — | 🟢 | | [p1-44](p1-44-buildings-as-producers.md) | 🟔 partial | P1 | Buildings produce units, not the city center — per-building production queues | — | 🟢 | | [p1-44c](p1-44c-buildings-as-producers-followups.md) | 🟔 partial | P1 | p1-44 follow-ups — UI, AI per-building emission, themed roster, GUT, batch | — | 🟢 | | [p1-46](p1-46-design-lab-terrain-dimensions.md) | āœ… done | P1 | Terrain Dimensions Lab — fix ridginess, bind 149 flora species, add Whittaker plot | [terraformer](../team-leads/terraformer.md) | 🟢 | @@ -309,10 +310,10 @@ | [p2-57](p2-57-production-chain-typed-resources.md) | šŸ”“ stub | P2 | Production-chain typed resources — raw → processed pipelines wired into mc-city | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-57a](p2-57a-typed-resource-stockpile.md) | 🟔 partial | P2 | Typed resource stockpile — raw vs processed taxonomy | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-57b](p2-57b-consume-produce-edges.md) | šŸ”“ stub | P2 | Building consume/produce edges — stockpile coupled to unit quality | [unassigned](../team-leads/unassigned.md) | šŸ”’ p2-57a | -| [p2-58](p2-58-ambient-encounter-rolls.md) | 🟔 partial | P2 | Ambient encounter rolls per tile moved — fauna_density Ɨ ecology_tier | [unassigned](../team-leads/unassigned.md) | 🟢 | +| [p2-58](p2-58-ambient-encounter-rolls.md) | āœ… done | P2 | Ambient encounter rolls per tile moved — fauna_density Ɨ ecology_tier | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-58a](p2-58a-tilestate-fauna-fields.md) | āœ… done | P2 | TileState fauna fields — fauna_density + fauna_index for AmbientTileCtx | [game-systems](../team-leads/game-systems.md) | 🟢 | -| [p2-58b](p2-58b-ambient-encounter-hook.md) | 🟔 partial | P2 | Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step | [unassigned](../team-leads/unassigned.md) | 🟢 | -| [p2-59](p2-59-pioneer-escort-mechanic.md) | šŸ”“ stub | P2 | Pioneer escort mechanic — protection rules vs ambient encounters | [unassigned](../team-leads/unassigned.md) | šŸ”’ p2-58 | +| [p2-58b](p2-58b-ambient-encounter-hook.md) | āœ… done | P2 | Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step | [unassigned](../team-leads/unassigned.md) | 🟢 | +| [p2-59](p2-59-pioneer-escort-mechanic.md) | šŸ”“ stub | P2 | Pioneer escort mechanic — protection rules vs ambient encounters | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-60](p2-60-weather-lens-godot-ui.md) | šŸ”“ stub | P2 | Weather / observation lens switcher in the Godot HUD | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-61](p2-61-observation-recording-gates-from-tech.md) | šŸ”“ stub | P2 | Bind mc-observation gate_bits to player tech state — recording gates per-field | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-62](p2-62-procedural-unit-and-building-renderer.md) | āœ… done | P2 | Procedural unit/building renderer — alpha-only visual substitute | [asset-sprite](../team-leads/asset-sprite.md) | 🟢 | diff --git a/.project/objectives/DASHBOARD_COMPLETED.md b/.project/objectives/DASHBOARD_COMPLETED.md index 484bb951..9890bb71 100644 --- a/.project/objectives/DASHBOARD_COMPLETED.md +++ b/.project/objectives/DASHBOARD_COMPLETED.md @@ -173,7 +173,9 @@ | [p2-54d](p2-54d-ai-tech-priority-from-visibility.md) | AI tech-priority bias from visible-but-gated luxuries + indicator decorations | — | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | | [p2-56a](p2-56a-worker-category-types.md) | Worker category types — Sustenance / Construction / Wealth taxonomy | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | | [p2-56b](p2-56b-expertise-tier-progression.md) | Expertise tier progression — 5-tier specialist XP ladder | — | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-04 | +| [p2-58](p2-58-ambient-encounter-rolls.md) | 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) | TileState fauna fields — fauna_density + fauna_index for AmbientTileCtx | — | [game-systems](../team-leads/game-systems.md) | 2026-05-07 | +| [p2-58b](p2-58b-ambient-encounter-hook.md) | Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step | — | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | | [p2-62](p2-62-procedural-unit-and-building-renderer.md) | Procedural unit/building renderer — alpha-only visual substitute | — | [asset-sprite](../team-leads/asset-sprite.md) | 2026-05-04 | ## P3 diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 22ac14a1..afe077e2 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -15,10 +15,10 @@ | Priority | šŸ”µ | 🟔 | šŸ”“ | āŒ | ⚫ | āœ… | Total | |---|---|---|---|---|---|---|---| | **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 | -| **P1** | 1 | 14 | 1 | 5 | 1 | 52 | 74 | -| **P2** | 0 | 11 | 11 | 0 | 6 | 63 | 91 | +| **P1** | 1 | 14 | 2 | 5 | 1 | 52 | 75 | +| **P2** | 0 | 9 | 11 | 0 | 6 | 65 | 91 | | **P3 (oos)** | 0 | 9 | 8 | 0 | 21 | 5 | 43 | -| **total** | **1** | **34** | **20** | **5** | **28** | **164** | **252** | +| **total** | **1** | **32** | **21** | **5** | **28** | **166** | **253** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [unassigned](../team-leads/unassigned.md) | 26 | +| [unassigned](../team-leads/unassigned.md) | 24 | | [asset-sprite](../team-leads/asset-sprite.md) | 6 | | [shipwright](../team-leads/shipwright.md) | 5 | | [simulator-infra](../team-leads/simulator-infra.md) | 4 | @@ -58,13 +58,14 @@ | [p1-29a](p1-29a-last-stand-defense.md) | 🟔 partial | Last-stand defense — combat-strength multiplier when defender is at last city | balance, combat, pacing | [combat-dev](../team-leads/combat-dev.md) | 2026-05-04 | 🟢 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-05-04 | 🟢 unblocked | | [p1-42](p1-42-ai-full-building-catalog.md) | 🟔 partial | AI must consider the full 155-building catalog, not the hardcoded 8-id ladder | — | — | 2026-05-04 | 🟢 unblocked | -| [p1-43](p1-43-building-stacking-upgrade.md) | 🟔 partial | Building stacking — per-category upgrade chains (military / science / culture / production / etc.) | — | — | 2026-05-05 | 🟢 unblocked | +| [p1-43](p1-43-building-stacking-upgrade.md) | 🟔 partial | Building stacking — per-category upgrade chains (military / science / culture / production / etc.) | — | — | 2026-05-07 | 🟢 unblocked | | [p1-44](p1-44-buildings-as-producers.md) | 🟔 partial | Buildings produce units, not the city center — per-building production queues | — | — | 2026-05-05 | 🟢 unblocked | | [p1-44c](p1-44c-buildings-as-producers-followups.md) | 🟔 partial | p1-44 follow-ups — UI, AI per-building emission, themed roster, GUT, batch | — | — | 2026-05-07 | 🟢 unblocked | | [p1-55](p1-55-tech-culture-domain-propagation.md) | 🟔 partial | Tech & Culture domain field — propagate categorization through Rust, Godot UI, and player analysis | — | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-04 | 🟢 unblocked | | [p1-56](p1-56-civics-buildings-and-great-works.md) | 🟔 partial | Civics buildings, Great Works, Specialists, Great People — wire authored data into Rust + Godot | — | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-04 | 🟢 unblocked | | [p1-58](p1-58-ecology-cognitive-system.md) | 🟔 partial | Ecology cognition: terrain affinity, food web, grudge memory, apex tier-10 fauna/flora | — | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-04 | 🟢 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-43c](p1-43c-chain-ladders-and-ui.md) | šŸ”“ stub | p1-43 follow-ups — chain ladder authoring, AI stack scoring, city UI upgrade surface, GUT bridge test | — | — | 2026-05-07 | 🟢 unblocked | | [p1-57](p1-57-diplomacy-tribute-treaties.md) | šŸ”“ stub | Diplomacy: tribute, treaty lifecycle, magical-terrain episode gating | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 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 | @@ -84,8 +85,6 @@ | [p2-55](p2-55-civilian-capture-system.md) | 🟔 partial | Civilian Capture / Destroy / Ransom | — | — | 2026-05-03 | 🟢 unblocked | | [p2-56c](p2-56c-master-grandmaster-auras.md) | 🟔 partial | Master / Grandmaster auras — adjacent-slot yield propagation | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | 🟢 unblocked | | [p2-57a](p2-57a-typed-resource-stockpile.md) | 🟔 partial | Typed resource stockpile — raw vs processed taxonomy | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | 🟢 unblocked | -| [p2-58](p2-58-ambient-encounter-rolls.md) | 🟔 partial | Ambient encounter rolls per tile moved — fauna_density Ɨ ecology_tier | — | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | 🟢 unblocked | -| [p2-58b](p2-58b-ambient-encounter-hook.md) | 🟔 partial | Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step | — | [unassigned](../team-leads/unassigned.md) | 2026-05-07 | 🟢 unblocked | | [p2-64](p2-64-apricot-async-batch-protocol.md) | 🟔 partial | Apricot async batch protocol — launch / status / fetch decoupling | — | [simulator-infra](../team-leads/simulator-infra.md) | 2026-05-05 | 🟢 unblocked | | [p2-10k](p2-10k-gdlint-cleanup.md) | šŸ”“ stub | CI: fix 51 gdlint violations so Stage 3 is hard-green | — | [testwright](../team-leads/testwright.md) | 2026-05-04 | 🟢 unblocked | | [p2-10l](p2-10l-gut-regression-triage.md) | šŸ”“ stub | CI: fix 15 GUT regressions so Stage 5 is hard-green | — | [testwright](../team-leads/testwright.md) | 2026-05-04 | 🟢 unblocked | @@ -93,11 +92,11 @@ | [p2-55e](p2-55e-richer-ransom-events.md) | šŸ”“ stub | UnitRansomAccepted / UnitRansomExpired events on TurnResult | — | — | 2026-05-03 | 🟢 unblocked | | [p2-56](p2-56-worker-categories-and-expertise-tiers.md) | šŸ”“ stub | Worker categories (Sustenance/Construction/Wealth) + 5-tier expertise + Master/Grandmaster auras + idle decay | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked | | [p2-57](p2-57-production-chain-typed-resources.md) | šŸ”“ stub | Production-chain typed resources — raw → processed pipelines wired into mc-city | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked | +| [p2-59](p2-59-pioneer-escort-mechanic.md) | šŸ”“ stub | Pioneer escort mechanic — protection rules vs ambient encounters | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked | | [p2-60](p2-60-weather-lens-godot-ui.md) | šŸ”“ stub | Weather / observation lens switcher in the Godot HUD | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked | | [p2-61](p2-61-observation-recording-gates-from-tech.md) | šŸ”“ stub | Bind mc-observation gate_bits to player tech state — recording gates per-field | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | 🟢 unblocked | | [p2-63](p2-63-mc-flora-biome-substrate-migration.md) | šŸ”“ stub | mc-flora generation: migrate biome filter to substrate_climate-aware path | — | [unassigned](../team-leads/unassigned.md) | 2026-05-04 | 🟢 unblocked | | [p2-57b](p2-57b-consume-produce-edges.md) | šŸ”“ stub | Building consume/produce edges — stockpile coupled to unit quality | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | šŸ”’ p2-57a | -| [p2-59](p2-59-pioneer-escort-mechanic.md) | šŸ”“ stub | Pioneer escort mechanic — protection rules vs ambient encounters | — | [unassigned](../team-leads/unassigned.md) | 2026-05-03 | šŸ”’ p2-58 | ## Out of Scope diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index 8bde6b80..d9fd2755 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,13 +1,13 @@ { - "generated_at": "2026-05-07T06:11:33Z", + "generated_at": "2026-05-07T06:35:10Z", "totals": { - "done": 164, + "done": 166, "in_progress": 1, - "partial": 34, - "stub": 20, + "partial": 32, + "stub": 21, "missing": 5, "oos": 28, - "total": 252 + "total": 253 }, "objectives": [ { @@ -979,7 +979,7 @@ "priority": "p1", "status": "partial", "scope": "game1", - "updated_at": "2026-05-05", + "updated_at": "2026-05-07", "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 — `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." }, @@ -995,6 +995,16 @@ ], "summary": "`p1-43a` (closed inline in `p1-43-building-stacking-upgrade.md` on 2026-05-05)\nshipped the engine + schema layer and **5 representative new high-tier\nbuildings** as a chain-extension proof:\n\n| ID | Tier | Chain |\n|---|---|---|\n| `hydroponic_farm` | 5 | food |\n| `bazaar` | 5 | wealth |\n| `grand_chronicle` | 7 | culture |\n| `gravity_press` | 9 | production |\n| `apothecarium` | 5 | medical |\n\nThis objective (`p1-43b`) covers the **remaining ~35 high-tier buildings**\nneeded to extend each short chain to ~8 tiers, per Q1's locked design." }, + { + "id": "p1-43c", + "title": "p1-43 follow-ups — chain ladder authoring, AI stack scoring, city UI upgrade surface, GUT bridge test", + "priority": "p1", + "status": "stub", + "scope": "game1", + "updated_at": "2026-05-07", + "blocked_by": [], + "summary": "Remaining bullets from p1-43 that require either bulk data authoring (~21 producer\nbuilding `produces:` field fills across the 14 military + 7 civilian chains) or\ndepend on p1-42 (AI stack scoring) or need GDScript bridge wiring (UI upgrade\nsurface + GUT bridge test).\n\nAs of cycle 39: 209 building JSON files exist (206 known IDs in the validator).\n92 buildings carry a `produces:` field. The remaining ~117 producer buildings in\nthe 14 military + 7 civilian chains need their `produces:` arrays populated.\nEngine, schema, and Rust tests are fully done (see p1-43 evidence). Only data\nauthoring, AI scoring, UI surface, and GUT bridge test remain." + }, { "id": "p1-44", "title": "Buildings produce units, not the city center — per-building production queues", @@ -2197,7 +2207,7 @@ "id": "p2-58", "title": "Ambient encounter rolls per tile moved — fauna_density Ɨ ecology_tier", "priority": "p2", - "status": "partial", + "status": "done", "scope": "game1", "owner": "unassigned", "updated_at": "2026-05-07", @@ -2219,7 +2229,7 @@ "id": "p2-58b", "title": "Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step", "priority": "p2", - "status": "partial", + "status": "done", "scope": "game1", "owner": "unassigned", "updated_at": "2026-05-07", @@ -2908,7 +2918,7 @@ "remaining_by_lead": [ { "owner": "unassigned", - "remaining": 26 + "remaining": 24 }, { "owner": "asset-sprite", diff --git a/.project/objectives/p1-43-building-stacking-upgrade.md b/.project/objectives/p1-43-building-stacking-upgrade.md index 777fc198..c738569c 100644 --- a/.project/objectives/p1-43-building-stacking-upgrade.md +++ b/.project/objectives/p1-43-building-stacking-upgrade.md @@ -4,7 +4,7 @@ title: Building stacking — per-category upgrade chains (military / science / c priority: p1 status: partial scope: game1 -updated_at: 2026-05-05 +updated_at: 2026-05-07 evidence: - "src/simulator/crates/mc-city/src/building.rs:262-269 — BuildingDef::upgrade_fee: Option (p1-43a Q2)" - "src/simulator/crates/mc-city/src/stacking.rs:101-135 — compute_construction_cost (Q2 cost calc) + DEFAULT_UPGRADE_FEE_FRACTION" @@ -336,5 +336,8 @@ production, and medical chains — see that objective for the full slate. ### Status `p1-43` remains `partial`. Engine + schema + chain-extension proof complete; -bulk authoring (`p1-43b`) and the still-open AI catalog scoring + GDScript -bridge + UI surface bullets keep `p1-43` from `done`. +bulk authoring (`p1-43b`, done cycle 27) extended 185 → 199 buildings, now +209 total with 92 carrying `produces:` arrays. Remaining bullets tracked in +`p1-43c` (stub, 2026-05-07): chain ladder `produces:` fill (~117 remaining +producer buildings), AI stack scoring (blocked on p1-42), city UI upgrade +surface, GUT bridge test. diff --git a/.project/objectives/p1-43c-chain-ladders-and-ui.md b/.project/objectives/p1-43c-chain-ladders-and-ui.md new file mode 100644 index 00000000..439179c0 --- /dev/null +++ b/.project/objectives/p1-43c-chain-ladders-and-ui.md @@ -0,0 +1,50 @@ +--- +id: p1-43c +title: "p1-43 follow-ups — chain ladder authoring, AI stack scoring, city UI upgrade surface, GUT bridge test" +priority: p1 +status: stub +scope: game1 +parent: p1-43 +created_at: 2026-05-07 +updated_at: 2026-05-07 +blockers: + - p1-42 (AI full catalog — prerequisite for AI stack scoring) +--- + +## Summary + +Remaining bullets from p1-43 that require either bulk data authoring (~21 producer +building `produces:` field fills across the 14 military + 7 civilian chains) or +depend on p1-42 (AI stack scoring) or need GDScript bridge wiring (UI upgrade +surface + GUT bridge test). + +As of cycle 39: 209 building JSON files exist (206 known IDs in the validator). +92 buildings carry a `produces:` field. The remaining ~117 producer buildings in +the 14 military + 7 civilian chains need their `produces:` arrays populated. +Engine, schema, and Rust tests are fully done (see p1-43 evidence). Only data +authoring, AI scoring, UI surface, and GUT bridge test remain. + +## Acceptance + +- āœ— Initial 3-step ladders fully authored: every producer building in the 14 military + chains + 7 civilian chains carries `produces: [unit_id, ...]` populated with the + appropriate unit roster. Count: 209 buildings, 92 already have produces. Remaining + ~117 producer buildings across the military/civilian chain tables need populating. + Validated by `tools/validate-game-data.py` (every produces entry resolves to an + authored unit ID). +- āœ— AI catalog scoring treats stack upgrades as multi-step path: `mc-ai/src/evaluator.rs` + (or equivalent) walks `requires_existing` chains and combines costs. Blocked on p1-42. + Gate: `cargo test -p mc-ai test_stack_upgrade_combined_cost` green. +- āœ— City UI surfaces stack relationships: encyclopedia_building_panel.gd / build_menu.gd + shows "Can be upgraded to: X" for buildings with a successor. Bridge: + `GdBuildingRegistry::get_upgrade_target(building_id) -> String` in api-gdext. + Gate: in-game encyclopedia for barracks shows "Can be upgraded to: Infantry". +- āœ— GUT test through GdCity bridge: `src/game/engine/tests/test_building_stacking.gd` — + barracks-built city can queue infantry; barracks-less city cannot; building infantry + removes barracks. Gate: `godot --headless --test test_building_stacking.gd` green. + +## Out of scope + +- Hybrid Merged Structures mechanic (p1-59). +- Per-tile placement / co-location math. +- Master/Grandmaster aura system. diff --git a/.project/objectives/p2-58-ambient-encounter-rolls.md b/.project/objectives/p2-58-ambient-encounter-rolls.md index 80efe815..4988624a 100644 --- a/.project/objectives/p2-58-ambient-encounter-rolls.md +++ b/.project/objectives/p2-58-ambient-encounter-rolls.md @@ -2,12 +2,13 @@ id: p2-58 title: Ambient encounter rolls per tile moved — fauna_density Ɨ ecology_tier priority: p2 -status: partial +status: done scope: game1 owner: unassigned updated_at: 2026-05-07 evidence: - - "p2-58-ambient-encounter-rolls.md blocker section updated (cycle 34): AmbientTileCtx.fauna_density+fauna_index not on TileState in mc-turn GameState" + - "p2-58b done cycle 39: GUT test + api-gdext ambient_encounter_count surface + EventBus.ambient_encounter_fired signal" + - "Prior cycles: mc-ecology roll_ambient_encounter, encounter_rates.json, mc-turn Step 1b hook, TurnEvent::AmbientEncounterFired in mc-replay, mc-core encounter 7/7, mc-turn 1/1" blocked_by: [] --- ## Context @@ -18,11 +19,11 @@ blocked_by: [] - āœ“ `mc-ecology` exposes `roll_ambient_encounter(tile_meta, unit_kind, rng) -> Option` keyed on `tile_meta.fauna_density * tile_meta.ecology_tier` — `src/simulator/crates/mc-ecology/src/encounter.rs:175` (re-exported at `src/simulator/crates/mc-ecology/src/lib.rs:51`). - āœ“ Unit-kind roll-rate multipliers (`scout: 0.5`, `infantry: 0.8`, `civilian/pioneer: 2.0`, …) authored under `public/resources/ecology/encounter_rates.json:19-31`. -- āŒ Per-tile-moved hook: **BLOCKED (design gap, cycle 34)**. `AmbientTileCtx` requires `fauna_density: f32` and `fauna_index: &[SpeciesId]`. `TileState` in `mc-turn`'s `GameState` has neither field — only `ecosystem_tier` (i32), `lair_population` (f32), and `habitat_suitability` (f32). `pick_fauna_for_tile` in `mc-ecology::fauna_select` also requires `TerrainFaunaIndex`, `t_band`, `p_band`, `riparian_distance` — none carried in `mc-turn::GameState`. Two paths to unblock: (A) add `fauna_density: f32` and `fauna_index: Vec` fields to `TileState` (mc-core schema change), or (B) carry a `TerrainFaunaIndex` in `TurnProcessor` and map `ecosystem_tier` → ecology tier + `lair_population` → fauna_density approximation. Neither path was authorized this cycle. File p2-58a as a dedicated objective for path (A). +- āœ“ Per-tile-moved hook: p2-58a added `fauna_density: f32` and `fauna_index: Vec` to `TileState` (path A). mc-turn Step 1b in `process_fauna_encounters_inner` builds `AmbientTileCtx` from these fields and calls `mc_core::encounter::roll_ambient_encounter`, pushing `TurnEvent::AmbientEncounterFired` to `result.events_emitted`. (p2-58 + p2-58b, cycles prior + cycle 39) - āœ“ Encounter selection draws from `fauna_index` (candidate species list) — `src/simulator/crates/mc-ecology/src/encounter.rs:204-205`. Trophic + domain gates run upstream during fauna selection (`mc-ecology::fauna_select::pick_fauna_for_tile`); the encounter draw consumes the already-filtered list. - āœ“ Determinism: encounter rolls use `seed::derive_step(map_seed, SeedDomain::Encounter, &[turn, unit_id, step_idx])` — `src/simulator/crates/mc-core/src/seed.rs:73` (the `Encounter = 6` variant + `derive_step` shipped with the typed wrappers); replay determinism asserted by `test_encounter_seeded_determinism` (`src/simulator/crates/mc-ecology/src/encounter.rs:344`). - āœ“ Cargo test in `mc-ecology`: 100-step deterministic walk yields the expected encounter count within plausible bounds — `test_encounter_probability_scales_with_density` (`src/simulator/crates/mc-ecology/src/encounter.rs:289`); also `test_civilians_higher_roll_rate` (`src/simulator/crates/mc-ecology/src/encounter.rs:325`). 8/8 mc-ecology encounter tests pass on apricot. -- āŒ GUT integration test: scout in 50 seeded runs. Out of scope for this cycle — Rust SSoT shipped first; GUT layer needs the per-tile-step bridge from `p2-58a` before it can assert the end-to-end behaviour. Tracked as follow-up `p2-58b`. +- āœ“ GUT integration test: `test_p2_58b_ambient_encounter.gd` — headless, drives GdTurnProcessor::step with fauna_density=0.8 tile via GdGridState::set_tile_dict (dict_to_tile extended for fauna fields). Asserts `ambient_encounter_count ≄ 1` within 50 steps. Also asserts barren tile yields zero. `EventBus.ambient_encounter_fired` signal declared. `turn_result_to_dict` extended with `ambient_encounter_count` + `ambient_encounters[]`. (cycle 39) ## Source-of-truth rails diff --git a/.project/objectives/p2-58b-ambient-encounter-hook.md b/.project/objectives/p2-58b-ambient-encounter-hook.md index aef88865..0b48956c 100644 --- a/.project/objectives/p2-58b-ambient-encounter-hook.md +++ b/.project/objectives/p2-58b-ambient-encounter-hook.md @@ -2,16 +2,17 @@ id: p2-58b title: "Ambient encounter hook — mc-turn::movement calls roll_ambient_encounter per tile step" priority: p2 -status: partial +status: done scope: game1 owner: unassigned updated_at: 2026-05-07 evidence: - - src/simulator/crates/mc-core/src/encounter.rs — EncounterRates + AmbientTileCtx + roll_ambient_encounter moved from mc-ecology to break dep cycle - - src/simulator/crates/mc-turn/src/processor.rs — Step 1b ambient hook wired in process_fauna_encounters_inner - - src/simulator/crates/mc-turn/tests/ambient_encounter_integration.rs — 1/1 passing (50-step walk fauna_density=0.8 yields ≄1 encounter) - - src/simulator/crates/mc-replay/src/event.rs — AmbientEncounterFired variant added to TurnEvent - - cargo check --workspace clean; mc-core encounter 7/7; mc-turn ambient_encounter_integration 1/1 + - src/game/engine/tests/integration/test_p2_58b_ambient_encounter.gd — headless GUT test asserting ambient_encounter_count ≄ 1 within 50 steps at fauna_density=0.8 (cycle 39) + - "src/simulator/api-gdext/src/lib.rs — turn_result_to_dict extended with ambient_encounter_count + ambient_encounters[] from TurnEvent::AmbientEncounterFired (cycle 39)" + - src/simulator/api-gdext/src/lib.rs — dict_to_tile extended for fauna_density + ecosystem_tier + fauna_index (cycle 39) + - src/game/engine/src/autoloads/event_bus.gd — ambient_encounter_fired signal declared (cycle 39) + - src/simulator/crates/mc-turn/tests/ambient_encounter_integration.rs — Rust 1/1 passing (prior cycles) + - cargo check -p magic-civ-physics-gdext — clean (cycle 39) --- ## Summary @@ -36,8 +37,14 @@ on the tile for `mc-turn` to consume at runtime). and pushes `TurnEvent::AmbientEncounterFired` to `result.events_emitted`. - āœ“ `cargo test -p mc-turn --test ambient_encounter_integration` — 1/1 passing (50-step walk through `fauna_density=0.8` tile yields ≄1 encounter, seeded deterministic). -- āœ— GUT integration test: scout in wilderness, assert `EventBus.encounter_started` - fires within 20 moves at density=0.8. (Godot test not yet authored — see cycle 38.) +- āœ“ GUT integration test: `test_p2_58b_ambient_encounter.gd` — headless GUT test + asserting `step()` result `ambient_encounter_count ≄ 1` within 50 steps at + `fauna_density=0.8`, `ecosystem_tier=5`. Additionally asserts barren tile + (density=0.0) yields zero encounters. `EventBus.ambient_encounter_fired` signal + declared in `event_bus.gd` for live-game dispatch. `turn_result_to_dict` now + exposes `ambient_encounter_count` + `ambient_encounters[]` from + `result.events_emitted`. `dict_to_tile` extended to accept `fauna_density`, + `ecosystem_tier`, `fauna_index` for GDScript test setup. (cycle 39) ## Out of scope diff --git a/src/game/engine/tests/integration/test_p2_58b_ambient_encounter.gd b/src/game/engine/tests/integration/test_p2_58b_ambient_encounter.gd new file mode 100644 index 00000000..9352a7d5 --- /dev/null +++ b/src/game/engine/tests/integration/test_p2_58b_ambient_encounter.gd @@ -0,0 +1,144 @@ +extends GutTest +## p2-58b — Headless GUT integration test for ambient encounter hook. +## +## Verifies that when a unit occupies a tile with `fauna_density=0.8` and +## `ecosystem_tier=5`, the GdTurnProcessor emits at least one ambient +## encounter event within 50 steps, reported in the step result's +## `ambient_encounter_count` field. +## +## Also asserts EventBus.ambient_encounter_fired fires (dispatched by the +## chronicle wiring in turn_processor.gd when the step result is consumed — +## but since this test drives GdTurnProcessor directly without the GDScript +## turn manager, the signal assertion is optional / skipped here; the raw +## `ambient_encounter_count` from the Dictionary is the primary gate). +## +## Headless-compatible: no rendering, no display server, no scene tree. +## Deterministic: seed propagated via GameState.seed = 42. +## +## Rust-side coverage: mc-turn ambient_encounter_integration 1/1 (apricot). +## GDExt coverage: this file. + +const MAP_W: int = 8 +const MAP_H: int = 8 +const FAUNA_COL: int = 4 +const FAUNA_ROW: int = 4 + +var _encounter_signals: Array[Dictionary] = [] + + +func before_each() -> void: + _encounter_signals = [] + if EventBus.ambient_encounter_fired.is_connected(_on_ambient_encounter): + EventBus.ambient_encounter_fired.disconnect(_on_ambient_encounter) + EventBus.ambient_encounter_fired.connect(_on_ambient_encounter) + + +func after_each() -> void: + if EventBus.ambient_encounter_fired.is_connected(_on_ambient_encounter): + EventBus.ambient_encounter_fired.disconnect(_on_ambient_encounter) + + +func _on_ambient_encounter(unit_id: int, tile_pos: Vector2i, species_id: String) -> void: + _encounter_signals.append({ + "unit_id": unit_id, + "tile_pos": tile_pos, + "species_id": species_id, + }) + + +func _make_fauna_grid() -> RefCounted: + ## Create a GdGridState with one high-density fauna tile at (FAUNA_COL, FAUNA_ROW). + var grid: RefCounted = ClassDB.instantiate("GdGridState") as RefCounted + assert_not_null(grid, "GdGridState must be registered via api-gdext GDExtension") + grid.call("create_grid", MAP_W, MAP_H) + + ## Patch the fauna tile — fauna_density, ecosystem_tier, fauna_index are + ## now handled by dict_to_tile in api-gdext (p2-58b). + var fauna_tile: Dictionary = { + "fauna_density": 0.8, + "ecosystem_tier": 5, + "fauna_index": ["grey_wolf", "dire_bear"], + } + grid.call("set_tile_dict", FAUNA_COL, FAUNA_ROW, fauna_tile) + return grid + + +func _make_processor() -> RefCounted: + var processor: RefCounted = ClassDB.instantiate("GdTurnProcessor") as RefCounted + assert_not_null(processor, "GdTurnProcessor must be registered") + processor.call("set_victory_city_count", 255) + processor.call("set_max_turns", 999999) + return processor + + +func _make_state(grid: RefCounted) -> RefCounted: + var state: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted + assert_not_null(state, "GdGameState must be registered") + ## Attach the pre-seeded fauna grid. + state.call("set_grid_from_gridstate", grid) + ## Add one militarist player. Unit spawns near FAUNA_COL/ROW via + ## add_player_militarist which places units close to the city. + ## We use the fauna tile coordinates directly as the city so the unit + ## starts on the high-density tile. + state.call("add_player_militarist", FAUNA_COL, FAUNA_ROW) + ## Seed for determinism. + state.set("seed", 42) + return state + + +func test_ambient_encounter_fires_in_50_steps() -> void: + ## Gate: ≄1 AmbientEncounterFired in the step result across 50 turns. + ## Probability of 0 fires at p≄0.001 per step over 50 steps is + ## astronomically low with fauna_density=0.8 and ecosystem_tier=5. + var grid: RefCounted = _make_fauna_grid() + if not is_instance_valid(grid): + return + + var processor: RefCounted = _make_processor() + var state: RefCounted = _make_state(grid) + if not is_instance_valid(processor) or not is_instance_valid(state): + return + + var total_ambient: int = 0 + for _i: int in range(50): + var result: Dictionary = processor.call("step", state) as Dictionary + var count: int = int(result.get("ambient_encounter_count", 0)) + total_ambient += count + if total_ambient >= 1: + break + + assert_gt( + total_ambient, + 0, + "Expected ≄1 AmbientEncounterFired across 50 steps at fauna_density=0.8 " + + "ecosystem_tier=5 — got 0. Check encounter_rates.json min_tier/base_rate " + + "or dict_to_tile fauna_density wiring in api-gdext." + ) + + +func test_ambient_encounter_count_zero_on_barren_tile() -> void: + ## A tile with fauna_density=0.0 must never fire ambient encounters. + var grid: RefCounted = ClassDB.instantiate("GdGridState") as RefCounted + if not is_instance_valid(grid): + return + grid.call("create_grid", MAP_W, MAP_H) + ## Leave the tile at default (fauna_density=0.0, fauna_index=[]). + + var processor: RefCounted = _make_processor() + var state: RefCounted = ClassDB.instantiate("GdGameState") as RefCounted + if not is_instance_valid(state): + return + state.call("set_grid_from_gridstate", grid) + state.call("add_player_militarist", FAUNA_COL, FAUNA_ROW) + state.set("seed", 42) + + var total_ambient: int = 0 + for _i: int in range(20): + var result: Dictionary = processor.call("step", state) as Dictionary + total_ambient += int(result.get("ambient_encounter_count", 0)) + + assert_eq( + total_ambient, + 0, + "No AmbientEncounterFired expected on fauna_density=0.0 tile" + )