diff --git a/.project/objectives/README.md b/.project/objectives/README.md
index 33c2e39b..b5381ba4 100644
--- a/.project/objectives/README.md
+++ b/.project/objectives/README.md
@@ -17,8 +17,8 @@
| **P0** | 44 | 0 | 0 | 0 | 0 | 0 | 44 |
| **P1** | 88 | 0 | 0 | 0 | 0 | 1 | 89 |
| **P2** | 130 | 0 | 0 | 0 | 0 | 1 | 131 |
-| **P3 (oos)** | 34 | 0 | 3 | 0 | 0 | 29 | 66 |
-| **total** | **296** | **0** | **3** | **0** | **0** | **31** | **330** |
+| **P3 (oos)** | 34 | 0 | 6 | 1 | 0 | 29 | 70 |
+| **total** | **296** | **0** | **6** | **1** | **0** | **31** | **334** |
@@ -26,7 +26,7 @@
| Team Lead | Remaining |
|---|---|
-| [warcouncil](../team-leads/warcouncil.md) | 3 |
+| [warcouncil](../team-leads/warcouncil.md) | 7 |
|
diff --git a/.project/objectives/p3-29-rail1-turn-unification.md b/.project/objectives/p3-29-rail1-turn-unification.md
index 6885a87e..7831ffea 100644
--- a/.project/objectives/p3-29-rail1-turn-unification.md
+++ b/.project/objectives/p3-29-rail1-turn-unification.md
@@ -4,7 +4,7 @@ title: Rail-1 turn unification — live game calls the Rust turn, delete GDScrip
priority: p3
scope: game1
owner: warcouncil
-status: open
+status: partial
updated_at: 2026-06-27
---
@@ -26,6 +26,13 @@ The bridge already exists: `GdTurnProcessor::step(GdGameState)` (api-gdext/src/l
## Acceptance
+**Steps 1-2 COMPLETE (headless-safe, 2026-06-27):** the Rust turn surfaces all granular UI
+events (CityGrew 06c6e2547, CityBordersExpanded db808e477, FloraSuccession 7b6d24bde) AND
+`GdTurnProcessor.step`'s result dict carries them as `result["events"]` (e165b4e6c, via
+`replay::event_to_dict`). Also fixed a latent bug surfaced by the consolidation: B2 healing
+healed besieged cities → siege-suppress (de68c9c10). All headless prep for the swap is done.
+
+**Remaining = the live swap (steps 3-5, render-gated):**
- [ ] `turn_manager.gd` runs the turn via `GdTurnProcessor.step(GameState)` instead of the
per-player `proc._process_*` loop.
- [ ] The returned `TurnResult` is rendered to UI (EventBus signals: chronicle, combat log,
diff --git a/public/games/age-of-dwarves/data/objectives.json b/public/games/age-of-dwarves/data/objectives.json
index 91227a6a..62ef1542 100644
--- a/public/games/age-of-dwarves/data/objectives.json
+++ b/public/games/age-of-dwarves/data/objectives.json
@@ -1,13 +1,13 @@
{
- "generated_at": "2026-06-26T18:57:51Z",
+ "generated_at": "2026-06-27T07:58:32Z",
"totals": {
- "partial": 3,
- "missing": 0,
- "done": 296,
"in_progress": 0,
- "stub": 0,
+ "partial": 6,
+ "stub": 1,
+ "missing": 0,
"oos": 31,
- "total": 330
+ "done": 296,
+ "total": 334
},
"objectives": [
{
@@ -3309,6 +3309,46 @@
"owner": "warcouncil",
"updated_at": "2026-06-26",
"summary": "> **Owner directive (2026-06-26):** the /loop \"continue until Game-1 done\" is **not\n> finished until the SIMULATOR is complete** — the headless Rust sim\n> (`mc-turn::TurnProcessor`, driven via `GdPlayerApi`/`magic_civ_*`) must play a full\n> self-play game with ALL systems, not the reduced subset shipped so far. See\n> [[project_loop_done_means_simulator_complete]].\n\nThe headless `step()` ([processor.rs:392+](../../src/simulator/crates/mc-turn/src/processor.rs))\ncurrently runs: trade (p3-25), economy, city_production (single queue), culture+border\nexpansion, tech/science, fauna encounters, combat/siege, diplomacy. Verified live via\n`magic_civ_view` (e.g. border expansion fired turn 0→1: `owned_tiles [[1,6]]→[[1,6],[0,6]]`).\n\n**Gaps (each verified absent from the headless turn):**"
+ },
+ {
+ "id": "p3-27",
+ "title": "Biosphere in the headless sim — ecology population + flora succession + marine ecology",
+ "priority": "p3",
+ "status": "partial",
+ "scope": "game1",
+ "owner": "warcouncil",
+ "updated_at": "2026-06-26",
+ "summary": "Split from [[p3-26-complete-headless-simulator]]. The biological simulators exist as full\nRust crates — `mc-ecology` (`EcologyEngine`: per-tile fauna populations, predator/prey,\nevolution) and `mc-flora` (`FloraEngine`: per-tile vegetation + succession) — and the LIVE\ngame ticks them every turn (`EcologyState.tick` + `take_flora_transitions`,\nturn_manager.gd:315). But the **headless `mc-turn` step does NOT tick them** (verified: `0\nhits` for EcologyEngine/FloraEngine/flora in `processor.rs`). Only fauna *encounters*\n(combat) run headless. So the headless sim has no living biosphere for events/economy to\ninteract with."
+ },
+ {
+ "id": "p3-28",
+ "title": "Modular turn architecture — break dep cycle, phase registry, boot-config DRY",
+ "priority": "p3",
+ "status": "partial",
+ "scope": "game1",
+ "owner": "warcouncil",
+ "updated_at": "2026-06-26",
+ "summary": "The per-subsystem sprawl noticed while porting climate/events/happiness/healing/ecology\nrevealed three SOLID/DRY/DIP debts. \"Foundation first\" tackled the layering + phase pieces."
+ },
+ {
+ "id": "p3-29",
+ "title": "Rail-1 turn unification — live game calls the Rust turn, delete GDScript orchestration",
+ "priority": "p3",
+ "status": "partial",
+ "scope": "game1",
+ "owner": "warcouncil",
+ "updated_at": "2026-06-27",
+ "summary": "**The DRY / Rail-1 violation (verified 2026-06-27).** There are TWO turn orchestrations:\n- LIVE: `turn_manager.gd` → `turn_processor.gd::_process_*` (GDScript) + `EcologyState.tick` +\n `WorldsimState` — GDScript orchestrating the turn.\n- HEADLESS: `GdPlayerApi` → `mc_turn::TurnProcessor::step` (Rust).\n\nThe system *math* lives once in the Rust crates (DRY). The turn *orchestration* is duplicated —\nand the p3-26/p3-27 work this session added happiness/healing/improvements/recipes/equipment/\necology to `mc-turn` while the live game still runs its GDScript copies (e.g. `EcologyState.tick`\nduplicates the new Rust `ecology_phase`). This session BUILT `mc-turn::step` into the complete\nsingle source of truth; this objective is the capstone that makes it actually single.\n\nThe bridge already exists: `GdTurnProcessor::step(GdGameState)` (api-gdext/src/lib.rs:6354) runs\n`mc_turn::TurnProcessor::step` on the LIVE game's state. The live turn just doesn't call it."
+ },
+ {
+ "id": "p3-30",
+ "title": "Port wild-creature AI from GDScript to Rust (Rail-1 compliance)",
+ "priority": "p3",
+ "status": "stub",
+ "scope": "game1",
+ "owner": "warcouncil",
+ "updated_at": "2026-06-27",
+ "summary": "**Rail-1 gap surfaced during the p3-29 logic sweep (2026-06-27).** The live turn runs\nwild-creature AI **decision logic in GDScript**: `turn_processor.gd::_process_wild_creatures`\n(line 459) calls `wild_ai.process_wild_turn(game_map)` →\n`src/game/engine/src/modules/ai/wild_creature_ai.gd` (302 LOC — a guard / attack / roam state\nmachine over `owner == -1` units).\n\nThis is sim logic, not presentation, so it violates Rail-1 (\"GDScript is presentation only\";\n\"AI decision-making lives in Rust\"). It is **distinct from [[p0-26-ai-tactical-rust-port]]**,\nwhich ported *player* tactical AI (`simple_heuristic_ai.gd` / `ai_tactical.gd` / `ai_military.gd`)\nand explicitly did not touch wild-creature behaviour. It is also distinct from the fauna\n*population/rendering/stats* objectives (`g2-08`, `p3-12`, `p1-49`, `p2-58a`) — those model\necology; this is the per-creature **combat behaviour AI**.\n\nThe combat *resolution* for wilds already lives in Rust (`mc-combat::wilds`); only the\n*decision* layer (who to attack, when to roam, leash enforcement) is still GDScript."
}
]
}