feat(@projects/@magic-civilization): 🌡️ p3-26 gap 1 (start) — climate physics ticks in the headless turn
The headless mc-turn ran no climate (live-game GDScript only). Now it does:
- mc-turn deps mc-climate (no cycle; mc-climate is lower-level).
- TurnProcessor::process_climate_phase ticks mc_climate::physics::ClimatePhysics once per
round on state.grid (process_step(grid, turn, map_seed, dt=1.0)). Default config
("{}"/"[]"/"{}") matches the ecology bench; the grid carries climate state across turns;
fresh-processor-per-turn is safe (physics is the operator, grid is the state). No-op
without a grid.
- Called in step() right after fauna (world-level end-of-round phase).
First slice of gap 1 — temperature/aerosol/precipitation now evolve on the live grid in
self-play. Still to come: the weather + climate_effects (unit HP) + marine_harvest chain.
Verified: climate_phase_ticks_grid_deterministically (determinism + no-grid no-op);
mc-turn 336/0 (no regression — climate phase runs in every step() with a grid).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f9593c4d29
commit
e9409f22cd
2 changed files with 59 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ mc-combat = { path = "../mc-combat" }
|
|||
mc-economy = { path = "../mc-economy" }
|
||||
mc-civics = { path = "../mc-civics" }
|
||||
mc-trade = { path = "../mc-trade" }
|
||||
mc-climate = { path = "../mc-climate" }
|
||||
mc-tech = { path = "../mc-tech" }
|
||||
mc-replay = { path = "../mc-replay" }
|
||||
mc-comms = { path = "../mc-comms" }
|
||||
|
|
|
|||
|
|
@ -491,6 +491,13 @@ impl TurnProcessor {
|
|||
// when a grid with lairs exists.
|
||||
self.process_fauna_encounters_inner(state, &mut result, true);
|
||||
|
||||
// Phase 5b: climate runtime (p3-26 gap 1). Tick the climate physics once per
|
||||
// round so temperature / aerosol / precipitation evolve on the live grid in the
|
||||
// headless sim — previously climate ran only in the GDScript live game. The grid
|
||||
// carries the climate state across turns; default config matches the ecology
|
||||
// bench (`ClimatePhysics::new("{}","[]","{}")`).
|
||||
self.process_climate_phase(state);
|
||||
|
||||
// Phase 5a-sentry: wake sentrying units that have enemies in vision range (2 hex).
|
||||
// Runs after movement so positions are current; runs before PvP so the
|
||||
// now-awoken unit's state is consistent when combat checks fire.
|
||||
|
|
@ -1007,6 +1014,21 @@ impl TurnProcessor {
|
|||
/// claim tiles — returning `Vec<usize>` from the pool is dropped here.
|
||||
/// The live game calls `mc_culture::CulturePool` via `GdCulture` and reacts
|
||||
/// to the ready list on the GDScript side.
|
||||
/// p3-26 gap 1: tick the climate physics once per round on the live grid so
|
||||
/// temperature / aerosol / precipitation evolve in the headless sim. The
|
||||
/// physics operates on (and persists state in) `GridState`; a fresh
|
||||
/// `ClimatePhysics` per round is fine — it is the operator, the grid is the
|
||||
/// state. Default config (`"{}"`/`"[]"`/`"{}"`) matches the ecology bench;
|
||||
/// `dt = 1.0` = one turn. No-op when there is no grid (pure unit-test states).
|
||||
fn process_climate_phase(&self, state: &mut GameState) {
|
||||
let turn = state.turn;
|
||||
let seed = state.map_seed;
|
||||
if let Some(grid) = state.grid.as_mut() {
|
||||
let mut climate = mc_climate::physics::ClimatePhysics::new("{}", "[]", "{}");
|
||||
climate.process_step(grid, turn, seed, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn process_culture(&self, state: &mut GameState, pi: usize) {
|
||||
// Grid dims read before the &mut player borrow (needed for in-bounds
|
||||
// candidate filtering during border expansion).
|
||||
|
|
@ -5796,6 +5818,42 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn climate_phase_ticks_grid_deterministically() {
|
||||
// p3-26 gap 1: the climate physics ticks the live grid each round, and is
|
||||
// deterministic for a given (start, turn, seed). No-op without a grid.
|
||||
use mc_core::grid::GridState;
|
||||
let build = || {
|
||||
let mut state = GameState::default();
|
||||
state.turn = 3;
|
||||
state.map_seed = 99;
|
||||
let mut grid = GridState::new(8, 8);
|
||||
for t in &mut grid.tiles {
|
||||
t.biome_label_id = "temperate_grassland".into();
|
||||
t.temperature = 0.5;
|
||||
t.quality = 3;
|
||||
}
|
||||
state.grid = Some(grid);
|
||||
state
|
||||
};
|
||||
let processor = TurnProcessor::new(100);
|
||||
|
||||
let mut a = build();
|
||||
processor.process_climate_phase(&mut a);
|
||||
let after_a: Vec<f32> = a.grid.as_ref().unwrap().tiles.iter().map(|t| t.temperature).collect();
|
||||
|
||||
let mut b = build();
|
||||
processor.process_climate_phase(&mut b);
|
||||
let after_b: Vec<f32> = b.grid.as_ref().unwrap().tiles.iter().map(|t| t.temperature).collect();
|
||||
|
||||
assert_eq!(after_a, after_b, "climate tick must be deterministic for same seed+turn");
|
||||
assert_eq!(after_a.len(), 64, "all tiles processed");
|
||||
|
||||
// No grid → no panic.
|
||||
let mut empty = GameState::default();
|
||||
processor.process_climate_phase(&mut empty);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn processor_is_deterministic() {
|
||||
use mc_core::grid::GridState;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue