Adds tooling/econ-sim — a standalone Rust binary for the Phase 2 economics simulation: Layer 1 (Leontief production, #806): - Deterministic per-run PRNG seeding of corp×site productivity (D-176) - Fixed-coefficient production chains; scarcity cascades downstream (D-178) - Per-capita population demand for finals and services - Gate-energy demand reduction for fusion_fuel at connected nodes (D-186) - Price adjustment via local tâtonnement Layer 2 (spatial price equilibrium, #807): - Damped tâtonnement trade flows along gate links (α=0.03, β=0.4, D-178) - 8% transport cost per hop damps long-distance arbitrage - Flows computed from pre-step snapshot; applied atomically - --stability-check implements D-179 Tests 1 and 2: · Test 1: cold-start convergence ±5% at tick 100 → PASS (max 1.05%) · Test 2: long-run stability ±2% over ticks 900–999 → PASS (max 0.00%) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
//! Database loading — reads economy data from systems.db.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::process;
|
||||
|
||||
use rusqlite::Connection;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Data types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Commodity {
|
||||
pub id: String,
|
||||
// Display name — used in reporting (#807+):
|
||||
#[allow(dead_code)]
|
||||
pub name: String,
|
||||
pub tier: String,
|
||||
pub base_price: f64,
|
||||
// Used by Layer 2+ pricing (#807, #808):
|
||||
#[allow(dead_code)]
|
||||
pub elasticity: String,
|
||||
#[allow(dead_code)]
|
||||
pub production_ubiquity: Option<String>,
|
||||
#[allow(dead_code)]
|
||||
pub demand_model: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ChainInput {
|
||||
pub commodity_id: String,
|
||||
pub quantity: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ProductionChain {
|
||||
pub chain_id: String,
|
||||
pub output_commodity_id: String,
|
||||
pub output_quantity: f64,
|
||||
// Used by Layer 2+ for location-constrained production (#807):
|
||||
#[allow(dead_code)]
|
||||
pub location_bound: bool,
|
||||
pub inputs: Vec<ChainInput>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CorpPresence {
|
||||
pub corp_id: String,
|
||||
pub system_id: String,
|
||||
pub primary_operation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SystemInfo {
|
||||
pub system_id: String,
|
||||
// Used for display/reporting in #807+:
|
||||
#[allow(dead_code)]
|
||||
pub proper_name: Option<String>,
|
||||
pub population: i64,
|
||||
pub cultural_corridor: Option<String>,
|
||||
pub gate_energy_connected: bool,
|
||||
}
|
||||
|
||||
/// A directed gate link between two systems.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GateLink {
|
||||
pub from_system_id: String,
|
||||
pub to_system_id: String,
|
||||
}
|
||||
|
||||
/// The complete economics dataset loaded from systems.db.
|
||||
pub struct Economy {
|
||||
pub commodities: Vec<Commodity>,
|
||||
pub commodity_map: HashMap<String, Commodity>,
|
||||
pub chains: Vec<ProductionChain>,
|
||||
/// Map: output_commodity_id → list of chains that produce it
|
||||
pub chains_by_output: HashMap<String, Vec<ProductionChain>>,
|
||||
/// Map: system_id → SystemInfo
|
||||
pub systems: HashMap<String, SystemInfo>,
|
||||
pub corp_presences: Vec<CorpPresence>,
|
||||
/// Map: system_id → list of corp presences
|
||||
pub presences_by_system: HashMap<String, Vec<CorpPresence>>,
|
||||
/// Bidirectional gate links (transport graph)
|
||||
pub gate_links: Vec<GateLink>,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DB helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn resolve_db_path(explicit: Option<PathBuf>) -> PathBuf {
|
||||
if let Some(p) = explicit {
|
||||
return p;
|
||||
}
|
||||
let mut dir = std::env::current_dir().expect("Cannot determine CWD");
|
||||
loop {
|
||||
let candidate = dir.join("server").join("data").join("systems.db");
|
||||
if candidate.exists() {
|
||||
return candidate;
|
||||
}
|
||||
if !dir.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
eprintln!("error: cannot find server/data/systems.db — pass --db explicitly");
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
pub fn open_db(path: &PathBuf) -> Connection {
|
||||
let conn = Connection::open(path).unwrap_or_else(|e| {
|
||||
eprintln!("error: cannot open {}: {}", path.display(), e);
|
||||
process::exit(1);
|
||||
});
|
||||
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;")
|
||||
.expect("PRAGMA setup failed");
|
||||
conn
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Loaders
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn load_commodities(conn: &Connection) -> Vec<Commodity> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT commodity_id, name, tier, base_price, elasticity,
|
||||
production_ubiquity, demand_model
|
||||
FROM commodities ORDER BY commodity_id",
|
||||
)
|
||||
.expect("prepare commodities");
|
||||
|
||||
stmt.query_map([], |row| {
|
||||
Ok(Commodity {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
tier: row.get(2)?,
|
||||
base_price: row.get(3)?,
|
||||
elasticity: row.get(4)?,
|
||||
production_ubiquity: row.get(5)?,
|
||||
demand_model: row.get::<_, Option<String>>(6)?.unwrap_or_default(),
|
||||
})
|
||||
})
|
||||
.expect("query commodities")
|
||||
.filter_map(|r| r.ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_chains(conn: &Connection) -> Vec<ProductionChain> {
|
||||
let mut chain_stmt = conn
|
||||
.prepare(
|
||||
"SELECT chain_id, output_commodity_id, output_quantity, location_bound
|
||||
FROM production_chains ORDER BY chain_id",
|
||||
)
|
||||
.expect("prepare chains");
|
||||
|
||||
let mut chains: Vec<ProductionChain> = chain_stmt
|
||||
.query_map([], |row| {
|
||||
Ok(ProductionChain {
|
||||
chain_id: row.get(0)?,
|
||||
output_commodity_id: row.get(1)?,
|
||||
output_quantity: row.get(2)?,
|
||||
location_bound: row.get::<_, i32>(3)? != 0,
|
||||
inputs: Vec::new(),
|
||||
})
|
||||
})
|
||||
.expect("query chains")
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
// Load inputs for each chain
|
||||
let mut input_stmt = conn
|
||||
.prepare(
|
||||
"SELECT chain_id, input_commodity_id, quantity
|
||||
FROM chain_inputs ORDER BY chain_id, input_commodity_id",
|
||||
)
|
||||
.expect("prepare chain_inputs");
|
||||
|
||||
let all_inputs: Vec<(String, String, f64)> = input_stmt
|
||||
.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
||||
.expect("query chain_inputs")
|
||||
.filter_map(|r| r.ok())
|
||||
.collect();
|
||||
|
||||
// Build index of chain_id → inputs
|
||||
let mut input_map: HashMap<String, Vec<ChainInput>> = HashMap::new();
|
||||
for (chain_id, commodity_id, quantity) in all_inputs {
|
||||
input_map
|
||||
.entry(chain_id)
|
||||
.or_default()
|
||||
.push(ChainInput { commodity_id, quantity });
|
||||
}
|
||||
|
||||
for chain in &mut chains {
|
||||
if let Some(inputs) = input_map.remove(&chain.chain_id) {
|
||||
chain.inputs = inputs;
|
||||
}
|
||||
}
|
||||
|
||||
chains
|
||||
}
|
||||
|
||||
fn load_systems(conn: &Connection) -> HashMap<String, SystemInfo> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT ss.system_id, ss.proper_name, ss.cultural_corridor,
|
||||
ss.gate_energy_connected,
|
||||
COALESCE(se.population, 0) as population
|
||||
FROM star_systems ss
|
||||
LEFT JOIN system_economy se ON ss.system_id = se.system_id
|
||||
ORDER BY ss.system_id",
|
||||
)
|
||||
.expect("prepare systems");
|
||||
|
||||
stmt.query_map([], |row| {
|
||||
Ok(SystemInfo {
|
||||
system_id: row.get(0)?,
|
||||
proper_name: row.get(1)?,
|
||||
population: row.get(4)?,
|
||||
cultural_corridor: row.get(2)?,
|
||||
gate_energy_connected: row.get::<_, Option<i32>>(3)?.unwrap_or(1) != 0,
|
||||
})
|
||||
})
|
||||
.expect("query systems")
|
||||
.filter_map(|r| r.ok())
|
||||
.map(|s| (s.system_id.clone(), s))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_gate_links(conn: &Connection) -> Vec<GateLink> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT from_system_id, to_system_id FROM gate_links
|
||||
ORDER BY from_system_id, to_system_id",
|
||||
)
|
||||
.expect("prepare gate_links");
|
||||
|
||||
stmt.query_map([], |row| {
|
||||
Ok(GateLink {
|
||||
from_system_id: row.get(0)?,
|
||||
to_system_id: row.get(1)?,
|
||||
})
|
||||
})
|
||||
.expect("query gate_links")
|
||||
.filter_map(|r| r.ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_corp_presences(conn: &Connection) -> Vec<CorpPresence> {
|
||||
let mut stmt = conn
|
||||
.prepare(
|
||||
"SELECT corp_id, location_id, primary_operation
|
||||
FROM corp_presence
|
||||
WHERE location_type = 'system'
|
||||
ORDER BY location_id, corp_id",
|
||||
)
|
||||
.expect("prepare corp_presence");
|
||||
|
||||
stmt.query_map([], |row| {
|
||||
Ok(CorpPresence {
|
||||
corp_id: row.get(0)?,
|
||||
system_id: row.get(1)?,
|
||||
primary_operation: row.get(2)?,
|
||||
})
|
||||
})
|
||||
.expect("query corp_presence")
|
||||
.filter_map(|r| r.ok())
|
||||
.collect()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main loader
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
pub fn load_economy(conn: &Connection) -> Economy {
|
||||
let commodities = load_commodities(conn);
|
||||
let commodity_map: HashMap<String, Commodity> =
|
||||
commodities.iter().map(|c| (c.id.clone(), c.clone())).collect();
|
||||
|
||||
let chains = load_chains(conn);
|
||||
let mut chains_by_output: HashMap<String, Vec<ProductionChain>> = HashMap::new();
|
||||
for chain in &chains {
|
||||
chains_by_output
|
||||
.entry(chain.output_commodity_id.clone())
|
||||
.or_default()
|
||||
.push(chain.clone());
|
||||
}
|
||||
|
||||
let systems = load_systems(conn);
|
||||
let corp_presences = load_corp_presences(conn);
|
||||
|
||||
let mut presences_by_system: HashMap<String, Vec<CorpPresence>> = HashMap::new();
|
||||
for cp in &corp_presences {
|
||||
presences_by_system
|
||||
.entry(cp.system_id.clone())
|
||||
.or_default()
|
||||
.push(cp.clone());
|
||||
}
|
||||
|
||||
let gate_links = load_gate_links(conn);
|
||||
|
||||
Economy {
|
||||
commodities,
|
||||
commodity_map,
|
||||
chains,
|
||||
chains_by_output,
|
||||
systems,
|
||||
corp_presences,
|
||||
presences_by_system,
|
||||
gate_links,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user