- workers/pool.rs: 5 new tests; catch_unwind keeps worker threads alive on handler panic (in-flight request loss unchanged, pinned by test + #843 docs); stubs.rs no longer falsely claims the pool is tested - save/load: execute_save_load pinned .after(Storyteller) so the scheduler cannot legally save pre-Input state; exclusive-system exception recorded in tick_phases.rs rules - surname corpus extracted to bin/shared/surname_corpus.rs (both economy generators import it; byte-identical output verified on 23.6MB+1.45MB TOMLs); all three stamp/watch registries updated - generator_spike gated behind non-default 'generator-spike' feature - economy.rs: 11 new D-181 signal-derivation tests on the new econ_sim Simulation::from_economy in-memory constructor - perception exemption comments now state the consumer sort contract; unused bytemuck removed; rayon comment corrected; the 22 allow(dead_code) documented as serde schema enforcement Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
166 lines
5.9 KiB
Rust
166 lines
5.9 KiB
Rust
//! Stateful simulation runner for server integration (#821).
|
||
//!
|
||
//! [`Simulation`] wraps all simulation state (economy data, node states,
|
||
//! currency, events) and exposes a per-tick `step()` method. This is the
|
||
//! entry point for the game server's economy system, which advances one
|
||
//! economy tick per ECON_TICK_RATE game ticks (D-031).
|
||
//!
|
||
//! The batch `model::run_with_events` is retained for the CLI binary and
|
||
//! stability checks. Both share the same underlying `model::step_inner`.
|
||
|
||
use std::collections::BTreeMap;
|
||
use std::path::Path;
|
||
|
||
use crate::{agents, currency, db, events, model, seed, trade};
|
||
|
||
/// Stateful Settled Reach economics simulation.
|
||
///
|
||
/// Initialize with [`Simulation::load`] once at server startup.
|
||
/// Call [`Simulation::step`] once per economy tick.
|
||
pub struct Simulation {
|
||
pub economy: db::Economy,
|
||
productivity: BTreeMap<(String, String), seed::Productivity>,
|
||
shadow: currency::ShadowEconomy,
|
||
adjacency: BTreeMap<String, Vec<String>>,
|
||
archetypes: BTreeMap<String, agents::Archetype>,
|
||
pub nodes: BTreeMap<String, model::NodeState>,
|
||
currency_state: currency::CurrencyState,
|
||
/// The event input port (D-180). Push events here; they are consumed
|
||
/// on the next `step()` call.
|
||
pub events: events::EventPort,
|
||
/// Number of economy ticks processed so far.
|
||
tick: u64,
|
||
/// Tâtonnement step size (α). Runtime-tunable via SetEconParam (#823).
|
||
/// Default: `model::ALPHA` (0.03).
|
||
pub alpha: f64,
|
||
/// Trade flow damping factor (β). Runtime-tunable via SetEconParam (#823).
|
||
/// Default: `trade::BETA` (0.4).
|
||
pub beta: f64,
|
||
}
|
||
|
||
impl Simulation {
|
||
/// Load economy data from `db_path` and initialize the simulation.
|
||
///
|
||
/// `run_seed` is the per-run PRNG seed for productivity seeding (D-176).
|
||
/// This is typically the game's world seed from `StartupMessage`.
|
||
///
|
||
/// The DB is opened once and the loaded data stored in memory.
|
||
/// Do NOT call this per tick.
|
||
pub fn load(db_path: &Path, run_seed: u64) -> Result<Self, String> {
|
||
let db_pathbuf = db_path.to_path_buf();
|
||
if !db_path.exists() {
|
||
return Err(format!("economy DB not found: {}", db_path.display()));
|
||
}
|
||
|
||
let conn = db::open_db(&db_pathbuf);
|
||
let economy = db::load_economy(&conn);
|
||
Ok(Self::from_economy(economy, run_seed))
|
||
}
|
||
|
||
/// Build a simulation directly from an in-memory [`db::Economy`] — no DB file.
|
||
///
|
||
/// Identical initialization to [`Simulation::load`] after the DB read:
|
||
/// deterministic productivity/shadow seeding from `run_seed`, warm-start
|
||
/// node states. Intended for unit tests and embedded callers that
|
||
/// construct small, fully-controlled economies (T-1064).
|
||
pub fn from_economy(economy: db::Economy, run_seed: u64) -> Self {
|
||
let productivity = seed::seed_all_productivity(&economy, run_seed);
|
||
let shadow = currency::seed_shadow_economy(&economy, run_seed);
|
||
let adjacency = trade::build_adjacency(&economy);
|
||
let archetypes = agents::build_archetype_map(economy.corp_archetype_data.clone());
|
||
let nodes = model::init_nodes(&economy);
|
||
|
||
Simulation {
|
||
economy,
|
||
productivity,
|
||
shadow,
|
||
adjacency,
|
||
archetypes,
|
||
nodes,
|
||
currency_state: currency::CurrencyState::new(),
|
||
events: events::EventPort::new(),
|
||
tick: 0,
|
||
alpha: model::ALPHA,
|
||
beta: trade::BETA,
|
||
}
|
||
}
|
||
|
||
/// Try to load from the auto-detected DB path (same search as the CLI binary).
|
||
///
|
||
/// Searches up from CWD for `server/data/systems.db`.
|
||
pub fn load_auto(run_seed: u64) -> Result<Self, String> {
|
||
let mut dir = std::env::current_dir().map_err(|e| e.to_string())?;
|
||
loop {
|
||
let candidate = dir.join("server").join("data").join("systems.db");
|
||
if candidate.exists() {
|
||
return Self::load(&candidate, run_seed);
|
||
}
|
||
if !dir.pop() {
|
||
break;
|
||
}
|
||
}
|
||
// Also check adjacent `data/` directory (when running from within server/)
|
||
let candidate = std::path::PathBuf::from("data").join("systems.db");
|
||
if candidate.exists() {
|
||
return Self::load(&candidate, run_seed);
|
||
}
|
||
Err("cannot find server/data/systems.db — pass path explicitly or run from project root".to_string())
|
||
}
|
||
|
||
/// Advance the simulation by one economy tick.
|
||
///
|
||
/// Applies active events, runs the Layer 1+2+3 step, and advances the
|
||
/// event port. Call once per economy tick (every ECON_TICK_RATE game ticks).
|
||
pub fn step(&mut self) {
|
||
// Activate any events scheduled for this tick (D-180)
|
||
self.events.activate_scheduled(self.tick);
|
||
|
||
let mods = self.events.compute_modifiers(&self.economy);
|
||
|
||
model::step_inner(
|
||
&self.economy,
|
||
&self.productivity,
|
||
&self.shadow,
|
||
&self.archetypes,
|
||
&mut self.nodes,
|
||
&mods,
|
||
self.alpha,
|
||
);
|
||
|
||
self.currency_state.apply_exchange_shock(mods.exchange_shock);
|
||
trade::trade_step(
|
||
&self.economy,
|
||
&mut self.nodes,
|
||
&self.adjacency,
|
||
&mut self.currency_state,
|
||
self.beta,
|
||
);
|
||
self.currency_state.update_rate();
|
||
|
||
// Expire finished events
|
||
self.events.advance_remaining();
|
||
|
||
self.tick += 1;
|
||
}
|
||
|
||
/// Number of economy ticks processed so far.
|
||
pub fn tick(&self) -> u64 {
|
||
self.tick
|
||
}
|
||
|
||
/// Current Tractus/Mark exchange rate.
|
||
pub fn tractus_mark_rate(&self) -> f64 {
|
||
self.currency_state.tractus_mark_rate
|
||
}
|
||
|
||
/// Read-only access to the loaded economy data.
|
||
pub fn economy(&self) -> &db::Economy {
|
||
&self.economy
|
||
}
|
||
|
||
/// Read-only access to the per-node shadow economy intensities.
|
||
pub fn shadow(&self) -> ¤cy::ShadowEconomy {
|
||
&self.shadow
|
||
}
|
||
}
|