diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index eb8477ff..4b2a981e 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -271,9 +271,9 @@ | [p2-39](p2-39-chronicle-hall-phantom-unlock.md) | ✅ done | P2 | Resolve `chronicle_hall` phantom unlock in `chronicle_keeping` culture tech | — | 🟢 | | [p2-43](p2-43-culture-research-completion-event.md) | 🟡 partial | P2 | Culture research live-game pipeline — per-turn GDExt bridge + `culture_researched` emit | — | 🟢 | | [p2-43a](p2-43a-rust-port-culture-pick.md) | 🔴 stub | P3 | Rail-1 port — `_pick_culture_tradition` → mc-ai::tactical::culture_pick | — | 🟢 | -| [p2-44](p2-44-ai-promotion-selection.md) | 🟡 partial | P2 | AI promotion selection — auto-pick + emit unit_promoted for AI units | — | 🟢 | +| [p2-44](p2-44-ai-promotion-selection.md) | ✅ done | P2 | AI promotion selection — auto-pick + emit unit_promoted for AI units | — | 🟢 | | [p2-44a](p2-44a-dataloader-promotion-trees-path.md) | ✅ done | P2 | DataLoader path mismatch — `get_promotion(\"trees\")` returns empty | [unassigned](../team-leads/unassigned.md) | 🟢 | -| [p2-44b](p2-44b-promotion-dispatch-instrumentation.md) | 🟡 partial | P2 | AI promotion dispatch — instrumentation pass to identify the silent gate | [unassigned](../team-leads/unassigned.md) | 🟢 | +| [p2-44b](p2-44b-promotion-dispatch-instrumentation.md) | ✅ done | P2 | AI promotion dispatch — instrumentation pass to identify the silent gate | [unassigned](../team-leads/unassigned.md) | 🟢 | | [p2-45](p2-45-elimination-reconciliation.md) | ✅ done | P2 | Player elimination reconciliation — emit `player_eliminated` on every transition | — | 🟢 | | [p2-46](p2-46-past-games-archive-replay-viewer.md) | 🟡 partial | P2 | Past-games archive & replay viewer — `mc-replay` crate, on-disk archive, projection-based playback | [shipwright](../team-leads/shipwright.md) | 🟢 | | [p2-47](p2-47-in-game-statistics-screens.md) | 🟡 partial | P2 | In-game statistics screens — Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | [shipwright](../team-leads/shipwright.md) | 🟢 | diff --git a/.project/objectives/DASHBOARD_COMPLETED.md b/.project/objectives/DASHBOARD_COMPLETED.md index 6cf01287..c93f8bf5 100644 --- a/.project/objectives/DASHBOARD_COMPLETED.md +++ b/.project/objectives/DASHBOARD_COMPLETED.md @@ -145,7 +145,9 @@ | [p2-37](p2-37-react-calculator-metadata-surface.md) | React calculator UI — surface flavor, lore, clan_affinity, archetype filter | — | [tourguide](../team-leads/tourguide.md) | 2026-04-27 | | [p2-38](p2-38-unit-audio-cues-stubs.md) | Unit audio_cues stub strings — selection/move/attack lines for the dwarven roster | — | [asset-audio](../team-leads/asset-audio.md) | 2026-04-27 | | [p2-39](p2-39-chronicle-hall-phantom-unlock.md) | Resolve `chronicle_hall` phantom unlock in `chronicle_keeping` culture tech | — | — | 2026-04-27 | +| [p2-44](p2-44-ai-promotion-selection.md) | AI promotion selection — auto-pick + emit unit_promoted for AI units | — | — | 2026-05-06 | | [p2-44a](p2-44a-dataloader-promotion-trees-path.md) | DataLoader path mismatch — `get_promotion(\"trees\")` returns empty | — | [unassigned](../team-leads/unassigned.md) | 2026-05-06 | +| [p2-44b](p2-44b-promotion-dispatch-instrumentation.md) | AI promotion dispatch — instrumentation pass to identify the silent gate | — | [unassigned](../team-leads/unassigned.md) | 2026-05-06 | | [p2-45](p2-45-elimination-reconciliation.md) | Player elimination reconciliation — emit `player_eliminated` on every transition | — | — | 2026-04-30 | | [p2-49](p2-49-climate-axes-latitude-continentality.md) | Climate axes refactor — latitude + continentality + zonal winds as first-class per-hex inputs | — | [terraformer](../team-leads/terraformer.md) | 2026-04-30 | | [p2-50](p2-50-rng-determinism-pin.md) | Deterministic RNG + seed-derivation pin across mc-mapgen / mc-climate / mc-ecology | — | [terraformer](../team-leads/terraformer.md) | 2026-05-01 | diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 9282e87b..dcd51b8b 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -16,9 +16,9 @@ |---|---|---|---|---|---|---|---| | **P0** | 0 | 0 | 0 | 0 | 0 | 44 | 44 | | **P1** | 1 | 13 | 2 | 6 | 1 | 51 | 74 | -| **P2** | 0 | 13 | 11 | 0 | 6 | 59 | 89 | +| **P2** | 0 | 11 | 11 | 0 | 6 | 61 | 89 | | **P3 (oos)** | 0 | 9 | 8 | 0 | 21 | 5 | 43 | -| **total** | **1** | **35** | **21** | **6** | **28** | **159** | **250** | +| **total** | **1** | **33** | **21** | **6** | **28** | **161** | **250** | @@ -26,7 +26,7 @@ | Team Lead | Remaining | |---|---| -| [unassigned](../team-leads/unassigned.md) | 26 | +| [unassigned](../team-leads/unassigned.md) | 25 | | [asset-sprite](../team-leads/asset-sprite.md) | 6 | | [shipwright](../team-leads/shipwright.md) | 5 | | [simulator-infra](../team-leads/simulator-infra.md) | 4 | @@ -80,8 +80,6 @@ | [p2-10](p2-10-regression-ci-gate.md) | 🟡 partial | Automated regression CI gate on every push to main | — | [testwright](../team-leads/testwright.md) | 2026-05-04 | 🟢 unblocked | | [p2-18](p2-18-guide-public-deployment.md) | 🟡 partial | Guide web app — public hosting + deploy pipeline | — | — | 2026-04-17 | 🟢 unblocked | | [p2-43](p2-43-culture-research-completion-event.md) | 🟡 partial | Culture research live-game pipeline — per-turn GDExt bridge + `culture_researched` emit | — | — | 2026-05-05 | 🟢 unblocked | -| [p2-44](p2-44-ai-promotion-selection.md) | 🟡 partial | AI promotion selection — auto-pick + emit unit_promoted for AI units | — | — | 2026-05-05 | 🟢 unblocked | -| [p2-44b](p2-44b-promotion-dispatch-instrumentation.md) | 🟡 partial | AI promotion dispatch — instrumentation pass to identify the silent gate | — | [unassigned](../team-leads/unassigned.md) | 2026-05-06 | 🟢 unblocked | | [p2-46](p2-46-past-games-archive-replay-viewer.md) | 🟡 partial | Past-games archive & replay viewer — `mc-replay` crate, on-disk archive, projection-based playback | — | [shipwright](../team-leads/shipwright.md) | 2026-05-05 | 🟢 unblocked | | [p2-47](p2-47-in-game-statistics-screens.md) | 🟡 partial | In-game statistics screens — Civ-style 5-tab modal (Demographics / Graphs / Rankings / Replay / Histories) | — | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | 🟢 unblocked | | [p2-48](p2-48-end-of-game-summary-screen.md) | 🟡 partial | End-of-game summary screen — outcome banner, standings, score graph, awards, timeline, footer actions | — | [shipwright](../team-leads/shipwright.md) | 2026-05-03 | 🟢 unblocked | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index da3b054d..bb3e4082 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-05-06T19:39:11Z", + "generated_at": "2026-05-06T21:46:06Z", "totals": { - "done": 159, + "done": 161, "in_progress": 1, - "partial": 35, + "partial": 33, "stub": 21, "missing": 6, "oos": 28, @@ -1778,9 +1778,9 @@ "id": "p2-44", "title": "AI promotion selection — auto-pick + emit unit_promoted for AI units", "priority": "p2", - "status": "partial", + "status": "done", "scope": "game1", - "updated_at": "2026-05-05", + "updated_at": "2026-05-06", "blocked_by": [], "summary": "`EventBus.unit_promoted(unit, promotion_id)` is wired end-to-end on the\naudio side: the handler in `AudioManager` plays a UI confirmation chime,\nand `audio.json` ships the `unit_promoted` entry. But the signal is only\nemitted from one place:\n`src/game/engine/scenes/combat/promotion_picker.gd:120` — the modal the\n**human** uses to pick a promotion.\n\nAI units never go through that picker. Rust has the eligibility check\n(`mc_combat::check_promotion`) and the validation\n(`mc_combat::validate_promotion_choice`) but **no AI selection logic** —\nzero callers of `unit.promote(id)` for AI-owned units, verified by\n`grep -rn '\\.promote(' src/`.\n\nSo in any AI-vs-AI engagement, level-ups happen silently — the XP bar\nfills but no promotion is ever applied or signalled." }, @@ -1799,7 +1799,7 @@ "id": "p2-44b", "title": "AI promotion dispatch — instrumentation pass to identify the silent gate", "priority": "p2", - "status": "partial", + "status": "done", "scope": "game1", "owner": "unassigned", "updated_at": "2026-05-06", @@ -2886,7 +2886,7 @@ "remaining_by_lead": [ { "owner": "unassigned", - "remaining": 26 + "remaining": 25 }, { "owner": "asset-sprite", diff --git a/.project/objectives/p2-44-ai-promotion-selection.md b/.project/objectives/p2-44-ai-promotion-selection.md index f667a0f0..ab134b45 100644 --- a/.project/objectives/p2-44-ai-promotion-selection.md +++ b/.project/objectives/p2-44-ai-promotion-selection.md @@ -2,9 +2,9 @@ id: p2-44 title: AI promotion selection — auto-pick + emit unit_promoted for AI units priority: p2 -status: partial +status: done scope: game1 -updated_at: 2026-05-05 +updated_at: 2026-05-06 evidence: - src/simulator/crates/mc-ai/src/tactical/promotion.rs - src/game/engine/src/modules/ai/ai_turn_bridge_dispatch.gd @@ -61,15 +61,32 @@ fills but no promotion is ever applied or signalled. direction overrode the older `turn_processor_signals.gd` plan; the same signal still reaches AudioManager, throne-room counter, and the new `auto_play.gd::_on_unit_promoted` events.jsonl writer. -- [-] ◐ GUT end-to-end test deferred: the Rust unit tests cover the picker - personality logic deterministically (5 tests in - `tactical::promotion::tests`); the GDScript path is exercised by the - existing dispatch test harness via `Action::PromotionPicked` JSON - round-trip (covered by `tactical_port_regression.rs::action_serde_*`). - A dedicated GUT scenario `test_p2_44_ai_promotion_end_to_end.gd` is the - one acceptance bullet still open; the apricot batch verification gate - (events.jsonl `unit_promoted` rows for AI units) likewise requires - buildspace sync + 10-seed run that has not yet completed in this cycle. +- [x] ✓ GUT end-to-end test landed at + `src/game/engine/tests/integration/test_p2_44_ai_promotion_end_to_end.gd` + (3/3 green, 0.4s headless). 10-seed apricot batch + `20260506_141905` (build `bb42a1d4d`) recorded **257 unit_promoted + events** across all 10 games (per-game range 9–59), confirming AI units + now promote and emit through the full chain into events.jsonl. + +## Cycle-31 close-out (2026-05-06) + +Three latent bugs blocked end-to-end emission after the cycle-3 picker +landed; all three resolved in cycle-31: +1. `data_loader.gd::_load_raw_category_dir` was clobbering the resources-loaded + `_raw["promotions"]` with the theme `manifest.json` pointer, hiding `trees` + from every accessor (cycle-30 fix in p2-44b). +2. `_eligible_promotion_ids` typed-`String` ctor errored on JSON-`null` + `requires_promotion` / `excludes_flags` / `unit_type` because + `Dictionary.get(key, default)` returns the `null` value when the key + exists, bypassing the default. Fixed by `if dict.has(k) and dict[k] != null` + guards (cycle-31 commit `5217d39e9`). +3. `scenes/tests/auto_play.gd` (the headless events.jsonl writer) was + missing `EventBus.unit_promoted.connect(_on_unit_promoted)` while every + other unit signal was connected. Added connect + handler matching the + `_on_unit_destroyed` shape (cycle-31 commit `bb42a1d4d`). + +Validation: batch `20260506_141905`, 10 seeds × 200 turns, 257 unit_promoted +events total. Status flips done. ## Evidence diff --git a/.project/objectives/p2-44b-promotion-dispatch-instrumentation.md b/.project/objectives/p2-44b-promotion-dispatch-instrumentation.md index f6a63ee8..965ab5f7 100644 --- a/.project/objectives/p2-44b-promotion-dispatch-instrumentation.md +++ b/.project/objectives/p2-44b-promotion-dispatch-instrumentation.md @@ -2,7 +2,7 @@ id: p2-44b title: AI promotion dispatch — instrumentation pass to identify the silent gate priority: p2 -status: partial +status: done scope: game1 category: ai owner: unassigned