magicciv/.project/objectives/p3-26-complete-headless-simulator.md
Natalie b4c402e766 docs(@projects/@magic-civilization): p3-26 Gap 3 DONE (equipment/crafting verified headless) + Gap 4 scope assessment
Gap 3 — Equipment/crafting: verified the full craft→equip→combat path runs
headless and Rust-authoritative (orig bullet was stale at [ ]):
  - PlayerAction::CraftEquipment → craft_equipment dispatch (materials gate +
    consume strategic_ledger + equip), 2 tests
  - recipe_phase ("recipe_refine") in END_OF_TURN_PHASES — passive crafting
    economy refines raw→quality-tiered product every self-play turn, 1 test
  - equip_combat_bonus reads boot-loaded item_combat at every combat site, 2 tests
  - boot path: set_item_combat_json FFI ← headless harness _apply_item_combat
  - MCTS AI not electing to craft = deliberate 9-kind GPU-rollout constraint,
    not a missing system
  Verified green: mc-turn + mc-player-api 557/0.

Gap 4 — Per-building queues: recorded verified assessment. Bench single-slot +
per-turn AI reselection is functionally equivalent to a FIFO build queue for the
self-play SIMULATION outcome; the multi-item queue is a live-game UI affordance
belonging to the p3-25/p3-29 projection arc. Owner scope call pending: does p3-26
require simulating a multi-item queue, or reclassify Gap 4 out of the headless bar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:44:11 -04:00

15 KiB

id title priority scope owner status updated_at
p3-26 Complete the headless simulator — close the live-vs-headless system gaps (loop done-criterion) p3 game1 warcouncil partial 2026-06-26

Summary

Owner directive (2026-06-26): the /loop "continue until Game-1 done" is not finished until the SIMULATOR is complete — the headless Rust sim (mc-turn::TurnProcessor, driven via GdPlayerApi/magic_civ_*) must play a full self-play game with ALL systems, not the reduced subset shipped so far. See project_loop_done_means_simulator_complete.

The headless step() (processor.rs:392+) currently runs: trade (p3-25), economy, city_production (single queue), culture+border expansion, tech/science, fauna encounters, combat/siege, diplomacy. Verified live via magic_civ_view (e.g. border expansion fired turn 0→1: owned_tiles [[1,6]]→[[1,6],[0,6]]).

Gaps (each verified absent from the headless turn):

Acceptance (sequenced; each gap closed in bounded, cargo+e2e-verified increments)

  • Gap 1 — Climate / environment runtime — DONE (verified 2026-06-27). mc-turn::process_climate_phase ticks ClimatePhysics::process_step + weather::derive_events + climate_effects::apply (tile effects + unit HP loss, mirroring climate_effects.gd: hp=max(0,hp-loss)) each round on state.grid. Tests: climate_phase_ticks_grid_deterministically, apply_climate_effects_fans_hp_loss_onto_units. The former "marine_harvest remaining" item is CLOSED: ocean_dead_fraction is both computed (compute_global_stats, physics.rs:800, reef-based) and consumed (step_evaporation, physics.rs:460) inside the climate phase's process_step — the marine→climate feed runs headless and is Rust-authoritative. The live fish-stock _compute_ocean_dead_fraction is a divergence, not a missing port (Rail-1: Rust drives). The trophic-cascade tick_ocean_state is gold-plating (not in the live game) — see p3-27-biosphere-headless. ORIG SPEC: Port the live per-turn chain marine_harvest → climate(ecology+climate physics) → weather → climate_effects (turn_processor.gd::_process_climate) into a mc-turn climate phase. The PHYSICS is already Rust (mc-climate: physics.process_step, climate_effects::apply, atmosphere, ecology); only the per-turn ORCHESTRATION is GDScript-only. Wire it onto state.grid in step(). Effects: weather events + unit HP damage + tile climate shifts in the headless sim.
  • [~] Gap 2 — Natural / "apocalyptic" events (M3 milestone). STARTED 2026-06-26: ported the deterministic core to mc-climate::events (hash_noise/roll_severity/category_fires), verified to match the live GDScript game bit-for-bit (hash_noise(10,0,1000)=0.67791910066535; the TS web guide's Math.sin diverges on large args — separate concern). NB: .messy is gone; the source is the live ecological_events.gd + ecological_event_handlers_a/b.gd + 12-category JSON in public/resources/events/. Progress 2026-06-26: mc-climate::events = deterministic core (GDScript-verified) + config model/loader + dispatch (process_events) + WILDFIRE & DROUGHT categories (effect + dispatch) + wired into mc-turn climate phase + GdPlayerApi.set_events_config_json FFI + headless-harness loading (DataLoader.get_ecological_events). ~10 events tests; mc-turn 338/0; dylib rebuilt (FFI present) + boot GUT 750/0. Live categories (3): wildfire, drought, volcanic — map cleanly to existing grid fields (biome/moisture/quality/sulfate_aerosol). Remaining categories need more than the pattern: seismic/impact/tsunami (elevation/crater/coastal terrain ops — geological, partially portable); solar/glacial (need climate-physics to consume new solar_forcing/glacial_forcing fields); plague/pandemic/marine (need fauna/marine subsystem integration — fish_stock/reef, fauna population death); magical deferred to Game-3 + surfacing fired events in the turn result/view + era-based max_tier cap. ORIG: Port the .messy ecological_events.gd (992 lines) → Rust, split per the milestone plan (.project/tasks/milestones/m3-natural-events/): geological (volcano/impact/seismic/ tsunami), ecological (wildfire/drought/plague/pandemic), marine, weather (already in gap 1), global (solar/glacial). Deterministic from seed per EVENT_FREQUENCY_SPEC.md; triggers from climate (gap 1), damage targets in biology (fauna/ecology). Wire into the turn. (Magic category excluded — M4.)
    • VERIFIED 2026-06-27 — two listed "remaining" sub-items reassessed: (a) era-based max_tier cap = NON-PARITY (gold-plating, dropped). The live GDScript events modules have NO max_tier/era-tier cap (0 hits in src/game/engine/src/modules/events/). Headless uses max_tier=10 (processor.rs:1117); adding an era cap would invent a rule the live game lacks. Leave flat unless a design adds the cap to the game first. (b) surfacing fired events = minor OBSERVABILITY gap only. Events already fire AND apply terrain effects headless; process_events returns the fired list but step() discards it (let _fired =, processor.rs:1117). The SYSTEM runs in self-play; only replay/observation visibility is missing. Low priority (parallels the p3-29 §A event surface, render-gated payoff).
  • Gap 3 — Equipment / crafting — DONE (verified 2026-06-27; orig bullet stale). The full craft→equip→combat path runs headless and Rust-authoritative:
    • Action + dispatch: PlayerAction::CraftEquipment { unit_id, item_id } (action.rs:195) → craft_equipment (dispatch.rs:1521) checks all materials against strategic_ledger, consumes them (saturating), pushes mc_items::EquippedItem onto the unit. 2 tests: craft_equipment_consumes_materials_and_equips, craft_equipment_rejects_insufficient_materials (dispatch.rs:2446/2462).
    • Recipe resolution in the turn: recipe_phase::process_recipe_phase ("recipe_refine") sits in sim_phases::END_OF_TURN_PHASES (sim_phases.rs:31) — every self-play turn refines raw→product (quality-tiered) via mc_city::recipes::tick_recipes against each player's stockpile. Test recipe_phase_refines_resources (recipe_phase.rs:70).
    • Equipment affects combat: equip_combat_bonus sums equipped-item atk/def from the boot-loaded item_combat table at every combat site (processor.rs:56/2934/3771); 2 tests (processor.rs:5496/5506).
    • Boot path: item_combat loaded into headless via set_item_combat_json FFI (player_api.rs:208) from the headless harness _apply_item_combat (player_api_main.gd:255).
    • NON-gap (deliberate): the MCTS rollout policy exposes 9 GPU-rollout-legal ActionKinds (policy.rs:79-87) — CraftEquipment is a deterministic player action, not a strategic rollout choice, so the self-play AI not electing to craft is the same intentional constraint as the GPU policy surface, not a missing system. Recipe refinement (the passive crafting economy) DOES run autonomously every self-play turn. Verified green: mc-turn + mc-player-api 557/0.
  • Gap 4 — Per-building build queues. Bench CityState has a single queue; per-building queues live in the full mc_city::City (live game). This is the dual city-model split (p3-25 ... step 6 / city_slot.rs). Either give CityState per-building queues or unify the models so the headless turn simulates them.
    • Assessment (2026-06-27) — likely PARITY-not-gap for the self-play criterion; owner scope call pending. Verified: bench CityState.queue is Option<Queueable> (single in-progress slot, mc-city/src/lib.rs:138); process_city_production completes it then clears to None (processor.rs:1604/1615); the bench AI sets exactly one item via apply_queue_production. The live construction_queue is a FIFO list (city.gd:74) — but a FIFO of buildings is functionally select-the-next-item-when-the-current-completes, which single-slot + per-turn AI reselection already achieves; the simulation outcome (what gets built, in what order) is identical. The multi-item queue is a player-UX affordance (batch decisions ahead of time), not a self-play simulation behavior. So for p3-26's bar ("a full self-play game with ALL systems") this is arguably already met. The genuine multi-queue/per-building-item surface belongs to the live-game projection arc (p3-25-unify-dual-city-model / p3-29-rail1-turn-unification), where the UI reads getState(). Owner decision needed: does p3-26 require the bench to simulate a multi-item queue, or is single-slot+reselection the accepted headless model (Gap 4 reclassified out of p3-26 into the p3-25/29 projection arc)?

Notes

Created 2026-06-26. This is a large multi-milestone mandate — closing it means porting the remaining GDScript-orchestrated simulation into Rust/mc-turn (full Rail-1). Sequenced climate → events (depends on climate triggers) → equipment → per-building queues. Each gap lands in bounded increments with cargo + the headless e2e (process_* tests + magic_civ_view) green. Reference impls in @magic-civilization.messy/ per atomic-porting rules. Related: p3-25 ... (the dual city model underlies gaps 1+4).

Full migration backlog (verified 2026-06-26 — live turn pipeline vs headless mc-turn step)

Diffed turn_processor.gd/turn_manager.gd (live per-turn calls) against mc-turn::step (grep confirmed 0 hits for each "missing" subsystem). Done in headless: trade, economy, city-production+growth, culture+border, tech/science, fauna encounters, PvP/siege/lair combat, climate physics+weather+effects, 3 event categories.

Backlog (live-only → migrate into mc-turn):

  • B1 Happiness + Golden Age (7993ba7ca — happiness_phase.rs, wired post-economy-loop) — mc-happiness exists; tick per-turn (golden-age meter, anarchy).
  • B2 Unit + city healing (7993ba7ca — healing.rs, wired post-climate) — HP regen (in-territory / fortified / city heal).
  • B3 Improvements subsystem — DONE 2026-06-26 (cf18e5768 state · ffe5fa0fb logic · 63362f9c4 FFI/harness). Full chain live: BuildImprovement → pending(build_turns) → build-tick → city_improvements → process_improvement_yields. Was fully absent; now state+logic+boot all wired+tested. Original finding below:
    • TurnProcessor::improvement_yield_table is on the fresh-per-turn processor and is NEVER populated by apply_end_turn (only set manually in unit tests) → process_improvement_yields no-ops in real play.
    • handle_build_improvement (action_handlers/mod.rs) is a stub: validates the unit, returns Ok(()), places nothing. The action's improvement_id is dropped by the dispatcher.
    • GameState holds no improvement data; improvement build_turns ARE in the JSON (farm.json etc.).
    • Plan (ecology-phase shape): GameState += improvement yield+build_turns tables (#[serde skip] boot) + pending_improvements: Vec<{col,row,improvement_id,city_idx,turns_remaining}>; apply_end_turn loads the tables (or process_improvement_yields reads GameState); dispatch handles BuildImprovement directly (unit tile → owning city via CityState.owned_tiles → push pending w/ build_turns); a build-tick phase decrements + completes into city_improvements[ci]; FFI + harness boot. Full subsystem.
  • B4 Government/civics per-turnmc-civics; (disabled in live too — stub).
  • [~] B5 Loot decay — PARITY (disabled in the live game too; not a gap). mc-items::process_loot_decay exists; revive only if live re-enables loot.
  • [~] B6 Equipment/crafting — B6a DONE, B6b remaining
    • B6a resource-refinement (178a3d5b8 + 7001d68f7): mc_city::recipes wired into the turn — recipe_phase loads strategic_ledger → ResourceStockpile, tick_recipes (consume raw → produce refined) over the player's buildings, writes back; recipes_json boot + FFI + harness. Real economic transform.
    • B6b equipment combat (core) DONE (b22e98275 equipped state · 564a7ed4a combat-read · 10923f468 item-table boot · 70415b922 CraftEquipment): MapUnit.equipped + item_combat table; equip_combat_bonus injected into BOTH combat paths (attack/defense, safe — 0 for unequipped); CraftEquipment action consumes materials (B6a's refined outputs) → equips. Full chain: refine → craft → equip → fights harder. mc-player-api 138/0, mc-turn 284/0.
    • [~] Loot drop-on-death + decay = B5 — disabled in the live game too, so headless-absent is PARITY not a gap (building it would gold-plate behavior the live game doesn't run). drop_all_loot/process_loot_decay exist in mc-items if ever revived; unit-death hook is scattered across combat sites. — mc-items HAS item logic (EquippedItem, charges, drop_all_loot, process_loot_decay) but UNWIRED; mc-city/recipes.rs HAS resource refinement (Recipe consumes/produces, tick_recipe(&mut ResourceStockpile), QualityTier) but UNWIRED. Bench has NO ResourceStockpile (not on City/Player) and MapUnit carries NO equipped items; no Craft/Equip action; combat doesn't read gear. 5-step interlocking plan: (1) CityState += ResourceStockpile fed by deposit/economy; (2) RecipeRegistry boot + recipe-tick phase → QualityTier products; (3) MapUnit += equipped + Craft/Equip actions; (4) mc-combat reads equipped (charges/bonuses); (5) wire process_loot_decay (closes B5). No clean sub-slice — pieces interlock across mc-state/city/items/combat/player-api.
  • B7 Per-building queues — dual-city-model unification (was gap 4).
  • [~] B8 event categories — 9/12 live wildfire/drought/volcanic/seismic/impact/tsunami/plague (terrain) + solar (global warming) + glacial (cold) via magic_heat_delta forcing (fc3db77c7). pandemic/ecological FAUNA handled by mc-ecology disease applier; marine ticks via process_step; magical→G3. Effectively complete.
  • Biosphere cluster → see p3-27 ... (ecology population + flora succession + marine).

Sequencing: B1/B2/B3 (self-contained turn-glue) parallelizable as separate modules; B6/B7 ride the dual-city-model; B8 ecological/marine events depend on p3-27. Each lands cargo+test-green, wired into step() serially.