- Widen EventPort tick methods from u32 to u64 (prevents overflow) - Add is_identity() guard on hot-path String allocation in modifiers - Replace Vec::remove(0) with VecDeque::pop_front() in price history - Add .after(tick_economy_simulation) ordering for debug commands - Fix stale PROTOCOL_VERSION assertion (20 → 21) in serialization test - Add D-181 Phase 2 visibility scope comment on serve_econ_state_query - Eliminate double lookup in rebuild_signals via single-pass extraction - Track economy seed TODO with backlog ticket reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
156 lines
5.4 KiB
Rust
156 lines
5.4 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);
|
||
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);
|
||
|
||
Ok(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
|
||
}
|
||
}
|