Implements the full D-180/D-181 economics pipeline: **#810 — Event input port (D-180)** - Add EconEvent struct with Target/Effect/Duration/Visibility variants - Implement EventPort as typed input queue for external disruptions - Apply events in simulation step; D-179 Test 3 now uses real shock injection **#821 — Integrate econ-sim into server tick loop** - Extract econ-sim as library crate (lib.rs + sim.rs, Cargo.toml [lib] section) - Add Simulation stateful runner; step() advances one economy tick - Add EconSimResource, EconStateResource (7 D-181 signals), tick_economy_simulation - Economy loads once at startup; graceful no-op when systems.db absent - Server advances economy 1 tick per 10 game ticks (D-031) **#822 — Expose economy state over IPC bridge** - Protocol version 20 → 21 - Add EconomySnapshot, EconNodeSnapshot wire types - Add EconStateQuery PlayerAction variant; response in economy_snapshot field - Add EconQueryBuffer resource + serve_econ_state_query system **#823 — Economics debug commands** - Add InjectEconEvent, SetEconParam, GetEconState to DebugCommandKind - Add EconDebugEffect, EconParamKind enums - SetEconParam mutates α/β at runtime (α/β promoted to pub const + Simulation fields) - ALPHA and BETA constants threaded through step_inner/trade_step signatures All 1147 unit tests pass; zero warnings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
//! 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)
|
||||
let tick32 = self.tick as u32;
|
||||
self.events.activate_scheduled(tick32);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user