feat(@projects/magic-civilization): ✨ update worldsim accumulator save/load logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
b5286c48f8
commit
5074152af6
1 changed files with 11 additions and 1 deletions
|
|
@ -87,7 +87,7 @@ hydrology re-solve) hook the terraforming cascade into the same step.
|
|||
- ✓ **Grid-only Rust worldsim accumulators persist losslessly — DONE (2026-06-09).** The history-dependent per-tile counters the live worldsim step *writes* on `climate._grid` — `surface_water` + `original_biome_id` (climate `physics.rs`) and `bloom_streak` (ecology `biological.rs::advance_bloom_streak`) — now serialize to/from the save envelope. New `GdGridState.worldsim_accumulators_to_json` / `restore_worldsim_accumulators_from_json` (`api-gdext/src/lib.rs`) emit a sparse, deterministically-ordered `[col,row,surface_water_bits,original_biome_id,bloom_streak]` array (`surface_water` as raw `f32` bits → bit-exact; sorted `(col,row)`, no HashMap → byte-stable); `climate.gd` stashes the payload on load and injects it into the rebuilt `_grid` on the first post-load `process_turn` (after `_sync_tiles_to_grid`, before physics); `save_manager.gd` wires it as the `climate_accumulators` envelope key (missing/empty in old saves → fresh defaults). **Three-field set verified COMPLETE for the live per-turn path by source audit:** the live ecology step is `mc-climate::EcologyPhysics::process_step` (canopy/undergrowth/fungi/succession/habitat — all synced back + persisted by `tile_serializer`, or recomputed-each-tick), NOT `mc-flora::FloraEngine` (which writes the `soil_depth`/`deadwood`/`maturity` accumulators only in worldgen/benches — no api-gdext bridge, never live); the ecology feedback fields (`total_grazing_pressure` etc.) are reset-and-recomputed each tick (`engine.rs:895`); the only per-turn-mutated, grid-only, unsynced, non-derived fields are exactly the three. **Verified on apricot (2026-06-09, seed 0xC0FFEE):** GUT `test_grid_accumulators_round_trip_byte_identical` (lossless serialize→restore-onto-fresh-grid→re-serialize, bit-exact), `test_grid_accumulators_old_save_reads_as_noop` (backward-compat no-op), and `test_grid_accumulators_continue_trajectory_when_inputs_preserved` (continued trajectory byte-identical when climate inputs are held constant; the accumulator restore is the *sole* source of the three fields — `surface_water`/`original_biome_id` are stripped from the input carry, `bloom_streak` is never in `tile_to_dict`, so the test is non-tautological + load-bearing) — all green in the `test_worldsim_playable_path.gd` 10/10 scoped run (101 asserts). The load→save-WITHOUT-playing-a-turn path is also covered: `climate.gd::worldsim_accumulators_to_save_json` prefers the not-yet-injected `_pending_accumulators` so a re-save before the first post-load turn is byte-stable (never `""`/stale), proven by `test_climate_accumulators_resave_without_play_is_lossless` (`test_climate_tile_sync.gd`, 6/6 green on apricot). Rust `mc-save` round-trip + `mc-worldsim` determinism remain green.
|
||||
- **Correction:** `marine_bloom_turns` (named in the earlier residual list) is NOT a Rust grid accumulator — it is a GDScript `Tile` property (`tile.gd:109`) driven by the Marine Engineer Iron-Seeding *player action* (`marine_harvest.gd`), with zero presence in any `.rs` file. Its persistence is a `tile_serializer.gd` concern (GDScript event counter), entirely outside worldsim orchestration; it does not belong in this fix and does not gate p2-80.
|
||||
- ✓ **Climate-INPUT save-fidelity (`wind_direction`) — CLOSED via `p2-82` (2026-06-09).** Discovered during this verification: a *fully* byte-identical continued **`surface_water`** trajectory was initially NOT achieved — not because of the accumulators (persisted above), but because `mc-climate/physics.rs:336/399` reads `tile.wind_direction` (worldgen-set transport driver → moisture → `surface_water`), and it was NOT carried by `tile_serializer.gd` nor re-derived on load, so it reset to `0` and the climate transport diverged. **Fixed** (`p2-82`): `wind_direction` now round-trips through `tile_serializer.gd::to_dict`/`from_dict` (sparse — default `0` omitted; old saves with no key → `0`, backward-compatible). The flagged second candidate `aerosol_mitigation` was resolved as a NON-issue (exhaustive grep: no runtime writer, always its `0.0` default → losing it on load is a no-op). **Verified on apricot (2026-06-09):** `test_wind_direction_survives_tile_serializer_round_trip` drives wind through the REAL save hop (`to_dict` → `JSON.stringify` → `JSON.parse_string` → `from_dict`) and restores `5`; `test_wind_direction_default_omitted_and_old_save_reads_zero` locks sparseness + old-save compat (`test_climate_tile_sync.gd` 8/8); combined with the control (carrying wind forward → byte-identical `surface_water` trajectory, locked by `test_grid_accumulators_continue_trajectory_when_inputs_preserved`), the production continued climate trajectory is now byte-identical. Full-suite regression-clean (failing count held at 43 after the save-format addition). **Closed objective: `.project/objectives/p2-82-climate-input-save-fidelity.md` (status done).**
|
||||
- ◻ **api-gdext per-turn call — BLOCKED WITH CAUSE (whole-game Rust port), not a p2-80-sized change.** The bullet's premise (that the playable game already runs `mc_turn::TurnProcessor::step`, so swapping to `WorldSim::step` is a call-site change) is **false** — see the Premise Correction in the Summary. The interactive discrete turn is GDScript; `WorldSim::step` = Rust `processor.step(state)` (the unified discrete turn) **+** climate + ecology + events. Driving the playable game through `WorldSim::step` therefore requires first porting the *entire discrete turn* (economy / production / research / cities / combat) into `mc_turn::TurnProcessor` and making the live `GdGameState` the authoritative state a Rust turn advances — i.e. the whole-game Rail-1 port tracked separately (`p0-26`-class, `p1-29j` autoplay action-application still a *stub*). That is explicitly **out of p2-80's scope** (Non-goals: "this is integration, persistence, determinism, and presentation only — not authoring/porting engines"). **Deliberately not built:** a narrow "unify the three worldsim orchestration calls into one Rust call" increment — it would not satisfy the bullet's literal text (the game still wouldn't advance through `WorldSim::step`), the Rail-1 gain is marginal (the GDScript "orchestration" is three ordered bridge calls + a seed mix, not simulation logic), and it would risk the render+save paths just stabilized. **Functional note:** p2-80's *functional* intent — "drive the existing worldsim engines in the playable turn" — is already MET: climate + ecology + world-events run every playable turn via the bridge (`turn_manager.gd`), proven visible (Render hook ✓) and persistent (Save persistence). This bullet is the residual *architectural-purity* question of **where the orchestration lives** (Rust vs GDScript), unblockable only by the whole-game port. **Full feasibility + sizing + phased bridgehead plan: `.project/designs/p2-80-bullet2-port-sizing.md`** (verified data-ownership inversion; recommended increment 1 = `p1-29j` autoplay bridgehead).
|
||||
- → **api-gdext per-turn call — MOVED TO NON-GOALS (2026-06-09).** Formally removed from this objective's acceptance set: driving the playable game through a single Rust `WorldSim::step` requires the whole-game Rail-1 discrete-turn port (the interactive discrete turn is GDScript, not `mc_turn::TurnProcessor::step` — see the Premise Correction in the Summary), which is a `p0-26`/`p1-29j`-class effort explicitly outside p2-80's "integration / persistence / determinism / presentation only" scope. p2-80's *functional* intent — "drive the existing worldsim engines in the playable turn" — is already MET via the bridge (climate + ecology + world-events run every playable turn through `turn_manager.gd`, proven by Render hook ✓ + Save persistence ✓). See **Non-goals** below and the full feasibility/sizing/phased-bridgehead plan `.project/designs/p2-80-bullet2-port-sizing.md`.
|
||||
- ✓ **api-wasm parity — documented separate cut, no silent divergence.** `api-wasm` (the guide-web design lab) depends on `mc-climate` + `mc-ecology` + `mc-compute` but **not** `mc-turn` / `mc-worldsim` (`api-wasm/Cargo.toml`). It exposes `WasmGrid` — a worldgen snapshot plus per-tile climate/tectonics stepping for interactive parameter exploration (`generate_for_lab`, `tileClimateJson`, `computeStatsJson`) — **not** a playable game: no `GameState`, no `TurnProcessor`, no cities/units/events. It deliberately does **not** reuse `WorldSim::step`, which requires `mc-turn::TurnProcessor` + a full `GameState` and would pull the entire gameplay stack into the WASM bundle for a tool that only visualizes worldgen + climate. **No physics divergence:** the game and the lab call the *same* `mc-climate` / `mc-ecology` `process_step` fns — only the orchestration differs (lab: worldgen snapshot + climate stepping for viz; game: `WorldSim::step` per turn).
|
||||
- ✓ **Full continuous-tick set wired** — verified by grep against the step. `EcologyEngine::process_step` (`engine.rs:276`) runs emergence, LV `tick_populations`, dispersal, tier succession, fish stocks, feedback, lair lifecycle. `generation::apply_migrations` runs in that step too (carrying-capacity migration, `engine.rs:343` — the g2-10 path). `biological::advance_bloom_streak` runs **once per turn** inside `dispatch_world_events` (`mc-worldsim/src/event_dispatch.rs:91`, unconditional when a grid is present), which `WorldSim::step` calls (`lib.rs:189`). `evolution::run_evolution` is **deliberately out** of the per-turn set: it is the pre-game from-epoch worldgen evolution (`evolution.rs:116` "Run pre-game evolution on a grid" — runs `world_age.evolution_ticks` of geological→biological deep time; every caller is a worldgen binary/bench, never a per-turn path). Running it per turn would re-evolve millennia each turn. *(Note: `advance_bloom_streak` mutates the grid-only `bloom_streak`, which now survives save/load via the `climate_accumulators` envelope key — see the accumulator-persistence bullet, closed 2026-06-09.)*
|
||||
- ✓ **Determinism gate — DONE (2026-06-08).** Pinned golden vector through the *real* api-gdext bridge path (not the crate test). GUT `test_worldsim_trajectory_golden_vector` (in `test_worldsim_playable_path.gd`) drives the worldsim per-turn pair through the live `#[func]` bridge methods in `turn_manager.gd`'s exact order — `GdFaunaEcology.tick_populations(grid, map_seed+turn)` then `GdWorldSim.dispatch_on_grid(grid, turn, map_seed)` (the `SeedDomain::WorldsimDynamics` / `dispatch_world_events` RNG path) — for 12 turns, hashes `(fauna continuation JSON \| eco_map JSON)` to SHA-256, and asserts BOTH (a) run_a == run_b (nondeterminism guard) AND (b) the hash equals the frozen golden `78d25ae1…d580d` (catches a deterministic regression that shifts both runs equally — which (a) alone cannot). **Verified on apricot:** the hash is byte-stable across two *separate process invocations* (strongest determinism evidence); the file is now 10/10 green on apricot (2026-06-09). Distinct from the crate test (which drives `WorldSim::step` as one composed call); this exercises the three separately-seed-threaded bridge calls the live game actually makes. The `(seed, save)` save→restore→continue half is locked by `test_continued_trajectory_byte_identical_after_save_load` (also through the bridge).
|
||||
|
|
@ -102,6 +102,16 @@ hydrology re-solve) hook the terraforming cascade into the same step.
|
|||
is the substrate they hook into.
|
||||
- Soil derivation (`g2-06`) — that is a genuinely-missing engine, the one
|
||||
worldsim piece that is NOT already built.
|
||||
- **Routing the playable discrete turn through a single Rust `WorldSim::step`**
|
||||
(the former "api-gdext per-turn call" acceptance bullet). This requires porting
|
||||
the entire discrete turn (economy / production / research / cities / combat)
|
||||
into `mc_turn::TurnProcessor` and making the live `GdGameState` the authoritative
|
||||
state a Rust turn advances — the whole-game Rail-1 port (`p0-26`/`p1-29j`-class),
|
||||
not a p2-80-sized integration change. p2-80's functional intent is already met
|
||||
via the GDScript bridge (the worldsim engines run every playable turn); this is
|
||||
the orthogonal architectural-purity question of *where the orchestration lives*.
|
||||
Full sizing + phased bridgehead plan: `.project/designs/p2-80-bullet2-port-sizing.md`
|
||||
(recommended increment 1 = `p1-29j` autoplay bridgehead).
|
||||
|
||||
## Risks
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue