refactor(@projects/@magic-civilization): 🧩 unify turn phases — ecology into mc-turn + end-of-turn phase registry (OCP)
With the dependency cycle broken (5ee312e45), mc-turn can finally depend on mc-ecology, so
the ecology tick moves out of its mc-player-api::apply_end_turn special-case and into the turn
itself — all phases now live in one crate.
Introduces `mc-turn::sim_phases` — the end-of-turn world-sim phases declared as an ordered DATA
registry (`END_OF_TURN_PHASES = [ecology, healing]`, uniform `fn(&mut GameState)`), run after
climate inside step(). Extending the living world (marine, fauna disease, …) is now one line in
the registry + a phase module — no edit to step()'s body, sequence visible in one place.
- Moved ecology_phase.rs → mc-turn; added mc-turn → mc-ecology dep.
- step(): replaced the direct healing call with `run_end_of_turn_phases` (ecology → healing).
- Removed from mc-player-api: the apply_end_turn ecology call, module, file, and mc-ecology dep.
mc-turn 276/0 (incl. ecology + sim_phases tests), mc-player-api 135/0. Behavior identical
(ecology still ticks once per turn, now inside step). Dylib rebuild pending (functionally
equivalent; FFI surface unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5ee312e452
commit
af41ea10a9
9 changed files with 69 additions and 25 deletions
2
src/simulator/Cargo.lock
generated
2
src/simulator/Cargo.lock
generated
|
|
@ -1780,7 +1780,6 @@ dependencies = [
|
|||
"mc-compute",
|
||||
"mc-core",
|
||||
"mc-flora",
|
||||
"mc-mapgen",
|
||||
"mc-profiling",
|
||||
"rayon",
|
||||
"serde",
|
||||
|
|
@ -2049,6 +2048,7 @@ dependencies = [
|
|||
"mc-comms",
|
||||
"mc-core",
|
||||
"mc-culture",
|
||||
"mc-ecology",
|
||||
"mc-economy",
|
||||
"mc-happiness",
|
||||
"mc-observation",
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ mc-tech = { path = "../mc-tech" }
|
|||
mc-trade = { path = "../mc-trade" }
|
||||
mc-state = { path = "../mc-state" }
|
||||
mc-turn = { path = "../mc-turn" }
|
||||
mc-ecology = { path = "../mc-ecology" }
|
||||
mc-combat = { path = "../mc-combat" }
|
||||
mc-items = { path = "../mc-items" }
|
||||
mc-ai = { path = "../mc-ai" }
|
||||
|
|
|
|||
|
|
@ -423,12 +423,6 @@ fn apply_end_turn(state: &mut GameState, player: PlayerId) -> Result<Vec<Event>,
|
|||
}
|
||||
let mut result = processor.step(state);
|
||||
|
||||
// p3-27 biosphere: tick the living ecology (fauna populations + flora
|
||||
// succession) right after the turn step. Lives here, not in
|
||||
// `TurnProcessor::step`, to avoid the `mc-turn → mc-ecology → mc-mapgen →
|
||||
// mc-turn` dependency cycle. No-op until the species library is boot-loaded.
|
||||
crate::ecology_phase::process_ecology_phase(state);
|
||||
|
||||
// Communications Phase 6 — end-of-turn comms passes.
|
||||
// Runs after the processor step (so `state.turn` is the new turn
|
||||
// and `step_comms` evaluates deliveries against it). Order:
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ pub mod action;
|
|||
pub mod comms_dispatch;
|
||||
pub mod controllers;
|
||||
pub mod dispatch;
|
||||
pub mod ecology_phase;
|
||||
pub mod error;
|
||||
pub mod learned;
|
||||
pub mod projection;
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ mc-economy = { path = "../mc-economy" }
|
|||
mc-civics = { path = "../mc-civics" }
|
||||
mc-trade = { path = "../mc-trade" }
|
||||
mc-climate = { path = "../mc-climate" }
|
||||
mc-ecology = { path = "../mc-ecology" }
|
||||
mc-tech = { path = "../mc-tech" }
|
||||
mc-replay = { path = "../mc-replay" }
|
||||
mc-comms = { path = "../mc-comms" }
|
||||
|
|
|
|||
|
|
@ -1,15 +1,8 @@
|
|||
//! p3-27 biosphere — per-turn ecology tick for the headless turn.
|
||||
//! p3-27 biosphere — per-turn ecology tick.
|
||||
//!
|
||||
//! The live game ticks `mc_ecology::EcologyEngine` every turn through the
|
||||
//! `GdFaunaEcology` GDExtension node (`process_step` + continuation-JSON
|
||||
//! save/restore). The headless turn never did — so the simulated world had no
|
||||
//! living fauna for the economy or natural events to interact with.
|
||||
//!
|
||||
//! This lives in `mc-player-api` (the turn-orchestration layer), NOT in
|
||||
//! `mc-turn::step`, because `mc-turn → mc-ecology` would be a cyclic dependency
|
||||
//! (`mc-ecology → mc-mapgen → mc-turn`). The dispatcher calls it immediately
|
||||
//! after `TurnProcessor::step`, so the canonical headless play path
|
||||
//! (`apply_end_turn`) ticks the biosphere every turn.
|
||||
//! save/restore). This brings the same living biosphere to the headless turn.
|
||||
//!
|
||||
//! It reproduces the live construction exactly: a fresh `EcologyEngine` (default
|
||||
//! config), its species library loaded from the boot-supplied fauna JSONs,
|
||||
|
|
@ -19,8 +12,9 @@
|
|||
//! (emergence, population dynamics, flora succession) so the returned
|
||||
//! `FloraTransition`s are a report, not work the caller must apply.
|
||||
//!
|
||||
//! Determinism: `process_step` and `seed_initial` are seeded from
|
||||
//! `GameState::map_seed`, so the whole phase is reproducible.
|
||||
//! Registered in [`crate::sim_phases::END_OF_TURN_PHASES`] and run after climate
|
||||
//! so the biosphere reacts to the freshly-updated temperature/moisture.
|
||||
//! Determinism: seeded from `GameState::map_seed`.
|
||||
|
||||
use mc_ecology::engine::EcologyEngine;
|
||||
use mc_ecology::species::load_species_library;
|
||||
|
|
@ -134,7 +128,6 @@ mod tests {
|
|||
let mut state = build_state(&species);
|
||||
assert!(state.worldsim_state_json.is_empty(), "starts barren");
|
||||
|
||||
// Genesis tick: seeds base trophic populations, writes continuation JSON.
|
||||
process_ecology_phase(&mut state);
|
||||
assert!(
|
||||
!state.worldsim_state_json.is_empty(),
|
||||
|
|
@ -142,7 +135,6 @@ mod tests {
|
|||
);
|
||||
let after_first = state.worldsim_state_json.clone();
|
||||
|
||||
// Second tick restores + evolves; continuation JSON advances (tick_count).
|
||||
process_ecology_phase(&mut state);
|
||||
assert!(!state.worldsim_state_json.is_empty());
|
||||
assert_ne!(
|
||||
|
|
@ -47,6 +47,10 @@ pub mod courier_resolver;
|
|||
pub mod happiness_phase;
|
||||
/// p3-26 B2 — Unit + city healing tick.
|
||||
pub mod healing;
|
||||
/// p3-27 — Per-turn ecology (fauna populations + flora succession) tick.
|
||||
pub mod ecology_phase;
|
||||
/// End-of-turn world-simulation phase registry (ecology, healing, …).
|
||||
pub mod sim_phases;
|
||||
#[cfg(feature = "gpu")]
|
||||
pub mod gpu;
|
||||
|
||||
|
|
|
|||
|
|
@ -503,9 +503,11 @@ impl TurnProcessor {
|
|||
// bench (`ClimatePhysics::new("{}","[]","{}")`).
|
||||
self.process_climate_phase(state);
|
||||
|
||||
// p3-26 B2: end-of-turn HP regen for units (territory/fortified rates) and cities
|
||||
// (heal toward max_hp), mirroring the live `_process_healing`/`_process_city_healing`.
|
||||
crate::healing::process_healing_phase(state);
|
||||
// End-of-turn world-simulation phases (ecology → healing), declared as an
|
||||
// ordered registry in `sim_phases` rather than hardcoded here. Extend the
|
||||
// living world by adding a phase there, not by editing step(). Ecology
|
||||
// reacts to the climate just ticked; healing settles afterward.
|
||||
crate::sim_phases::run_end_of_turn_phases(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
|
||||
|
|
|
|||
53
src/simulator/crates/mc-turn/src/sim_phases.rs
Normal file
53
src/simulator/crates/mc-turn/src/sim_phases.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
//! End-of-turn world-simulation phase registry.
|
||||
//!
|
||||
//! The turn's end-of-turn phases — the living-world updates that run after the
|
||||
//! economic + combat machinery — are declared here as DATA rather than hardcoded
|
||||
//! calls threaded through `TurnProcessor::step`. Each is a uniform
|
||||
//! `fn(&mut GameState)` that loops the map/players internally and is a safe no-op
|
||||
//! when its inputs aren't booted (e.g. ecology with no species library).
|
||||
//!
|
||||
//! Extending the simulator with a new end-of-turn subsystem (marine ecology,
|
||||
//! fauna disease, …) is one line here plus the phase module — no edit to the body
|
||||
//! of `step()`, and the ordered sequence stays visible in one place (OCP).
|
||||
//!
|
||||
//! Climate (`process_climate_phase`) runs immediately *before* this block so the
|
||||
//! biosphere reacts to the freshly-updated weather; it stays a `TurnProcessor`
|
||||
//! method for now because it shares the processor's climate helpers.
|
||||
|
||||
use mc_state::game_state::GameState;
|
||||
|
||||
/// A turn phase: mutates the whole `GameState` for one turn, looping internally.
|
||||
pub type SimPhaseFn = fn(&mut GameState);
|
||||
|
||||
/// Ordered end-of-turn world-sim phases, run after climate. Order matters:
|
||||
/// ecology evolves the biosphere first, then unit/city recovery settles.
|
||||
pub const END_OF_TURN_PHASES: &[(&str, SimPhaseFn)] = &[
|
||||
("ecology", crate::ecology_phase::process_ecology_phase),
|
||||
("healing", crate::healing::process_healing_phase),
|
||||
];
|
||||
|
||||
/// Run every registered end-of-turn phase in order.
|
||||
pub fn run_end_of_turn_phases(state: &mut GameState) {
|
||||
for (_name, phase) in END_OF_TURN_PHASES {
|
||||
phase(state);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn registry_lists_phases_in_documented_order() {
|
||||
let names: Vec<&str> = END_OF_TURN_PHASES.iter().map(|(n, _)| *n).collect();
|
||||
assert_eq!(names, vec!["ecology", "healing"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_end_of_turn_phases_is_a_safe_noop_on_empty_state() {
|
||||
// No grid / no species / no units → every phase no-ops without panicking.
|
||||
let mut state = GameState::default();
|
||||
run_end_of_turn_phases(&mut state);
|
||||
assert!(state.worldsim_state_json.is_empty());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue