//! 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>, archetypes: BTreeMap, pub nodes: BTreeMap, 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 { 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 { 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 } }