magicciv/.project/objectives/mc-replay-followup-unit-spawn-events.md
Natalie b5ad7c7f44 feat(@projects/@magic-civilization): mark replay unit spawn event coverage as complete
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-05-11 20:38:42 -07:00

7.8 KiB
Raw Blame History

id title priority status scope category owner created updated_at completed_at blocked_by follow_ups related
mc-replay-followup-unit-spawn-events mc-turn unit-spawn event coverage — every PlayerState.units.push emits UnitCreated / CityUnitCompleted p2 done game1 tooling simulator-infra 2026-05-11 2026-05-11 2026-05-11
p2-67

Context

Event::UnitCreated and Event::CityUnitCompleted are the canonical wire markers for "a unit just entered the world." Replay and transcript reconstructors (Claude Player API, headless test drivers, future debug tooling) consume those events to rebuild the unit ledger without re-running the simulator.

Today coverage is lossy. The 25-turn full_game_transcript.rs mock run observed (recap.md):

  • Slot 1 grew from 4 → 27 units across turns 0-16.
  • Slot 2 grew from 4 → 5 units in the same window.
  • Total Event::CityUnitCompleted / Event::UnitCreated lines in the transcript: a small fraction of the actual spawn count.

The transcript's hard-constraint check (constraint 3 — "AI builds ≥ 1 unit by turn 10") had to fall back to an observational check (PlayerState.units.len() growth across score snapshots) because the event-based check missed multiple known spawns. The check passes today only because the fallback exists; remove it and constraint 3 fails on a healthy run.

Replay reconstructors cannot rely on observational fallbacks — they have no PlayerState to compare against, only the event stream. Missing events → ghost units in the rebuilt ledger.

Source-of-truth rails

  • Rust crate: mc-turn — audit every state.players[pi].units .push(...) site (and every helper that does so transitively). Each must emit the appropriate event into TurnResult.events.
  • JSON path: no content changes.
  • GDScript: no changes — GDScript already consumes Event::UnitCreated / Event::CityUnitCompleted via the wire decoder.

Acceptance bullets

  • Audit grep — every units.push site in mc-turn/src/** categorised (see ## Audit results below).
  • Every (a) / (c) / (d) site emits the matching chronicle entry (TurnEvent::UnitCreated for net-new spawns, TurnEvent::UnitCaptured for capture / ransom-rollover ownership transfers) with the monotonic MapUnit.id, owning clan as ClanId(pi as u32), and hex (col, row) packed into TileCoord. The dispatch layer translates each entry into Event::UnitCreated and (when city: Some(...)) also Event::CityUnitCompleted.
  • Regression test mc-turn/tests/unit_spawn_event_coverage.rs runs a 12-turn bench fixture and asserts the per-slot invariant growth ≡ created + captured killed after every turn. Equality of cumulative counts is the contract. 3 tests, 3 pass.
  • full_game_transcript.rs constraint 3 drops the HARNESS_INITIAL_UNITS observational fallback (and the pre-recap score_snapshot fallback in the recap-summary path) and relies solely on the event stream. The constraint still passes (claude_vs_ai_full_game_transcript ... ok).
  • Determinism — unit_created_event_counts_are_deterministic runs the fixture twice and asserts byte-identical event-count vectors. Test passes.
  • No regression in mc-turn / mc-player-api existing tests (cargo test -p mc-turn --lib: 208 pass; cargo test -p mc-player-api: 87 lib + 1 integration pass).

Audit results

Every units.push site found across mc-turn, mc-combat, mc-city, mc-player-api:

File:line Category Action
mc-turn/src/processor.rs:1266 (try_spawn_unit) (a) city production Added TurnEvent::UnitCreated { city: Some(...) }
mc-turn/src/processor.rs:1327 (spawn_unit_typed) (a) city production Added TurnEvent::UnitCreated { city: Some(...) }
mc-turn/src/processor.rs:2162 (transfer_captured_unit) (d) PvP capture Already emits via pending_capture_events.drain_into
mc-turn/src/processor.rs:2238 (process_ransom_expiry) (d) ransom rollover Added TurnEvent::UnitCaptured emit (bypassed drain path)
mc-turn/src/processor.rs:3715,4355,5312,5402,5497 (e) test fixtures No event (cfg(test) / bench harness)
mc-turn/src/building_action_handlers.rs:507 (e) test fixture No event (cfg(test))
mc-player-api/src/dispatch.rs:1262 (e) test fixture No event (cfg(test))
mc-player-api/src/projection.rs:785,796,1085,1091 (e) test fixtures No event (cfg(test))
mc-player-api/tests/common/mod.rs:82 (e) test fixture No event (test helper)
mc-vision/src/lib.rs:583,598,619,640,666,685,719,752,773,776 (e) test fixtures No event (cfg(test))
mc-turn/tests/ambient_encounter_integration.rs:62 (e) test fixture No event (test setup)
mc-turn/tests/event_collector_wiring.rs:102,116,128 (e) test fixture No event (test setup)
mc-turn/tests/abstract_projection.rs:152 (e) test fixture No event (test setup)
mc-city/src/production.rs:165 n/a (catalog insert) def: UnitDef, not a MapUnit push — unrelated
mc-turn/src/gpu/unit_movement.rs:426,427 n/a (scratch buffers) gpu_units / cpu_units are local scratch, not state

Production audit: 2 sites added UnitCreated emit; 1 site added UnitCaptured emit; 1 site already correctly emitting; 0 missing.

Changes shipped

  • src/simulator/crates/mc-replay/src/event.rs — new TurnEvent::UnitCreated { turn, clan, unit_id, unit_kind, hex, city: Option<CityName> } variant; turn() accessor updated.
  • src/simulator/crates/mc-turn/src/processor.rs — emit at try_spawn_unit (1266) and spawn_unit_typed (1327); emit TurnEvent::UnitCaptured at process_ransom_expiry (2238).
  • src/simulator/crates/mc-turn/tests/unit_spawn_event_coverage.rs — new 3-test regression suite (growth-vs-events invariant, determinism, city-attribution).
  • src/simulator/crates/mc-turn/tests/event_collector_wiring.rs — extend summary match with UnitCreated arm (compile-time exhaustiveness preserved).
  • src/simulator/crates/mc-player-api/src/dispatch.rstranslate_processor_events adds branches for TurnEvent ::UnitCreated (→ Event::UnitCreated + optional Event::CityUnitCompleted) and TurnEvent::UnitCaptured (→ Event::UnitCreated { owner: captor }).
  • src/simulator/crates/mc-player-api/tests/full_game_transcript.rs — drop HARNESS_INITIAL_UNITS fallback; recap and constraint-3 detection both read the event stream directly.
  • src/simulator/api-gdext/src/replay.rs — exhaustive-match arm for TurnEvent::UnitCreated (GDExt replay viewer dictionary).

Effort

~half a dev-day (audit + per-site event emission patches + one coverage test + transcript-driver fallback removal).

Out of scope

  • Event::UnitDestroyed coverage — the kill paths in mc-turn are separately tracked; the swap_remove sites have their own audit.
  • Wire-event serialisation changes — the event shapes are stable per p2-67; this objective only ensures the emit sites are complete.