Files
settled-reach/tooling/econ-sim/src/sim.rs
T
jpmschweitzerandClaude Fable 5 0bd895fcac chore(engine): server hygiene batch — workers tests, surname dedup, save pin (T-1063, T-1064)
- 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>
2026-06-12 16:22:28 +02:00

166 lines
5.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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) -> &currency::ShadowEconomy {
&self.shadow
}
}