//! Currency zones, exchange rates, and shadow economy seeding (D-171, D-172, D-174). //! //! Three currencies (D-171): //! Tractus — Reach-wide standard, numeraire for all simulation pricing. //! Mark — Compact of Westphalia, ~3% conversion friction on cross-zone trade. //! Sol — Earth legacy, modeled as shadow commodity (not a numeraire). //! //! Exchange rate: floating Tractus/Mark rate driven by net cross-zone trade balance. //! Initialized at 1.0 (parity). Adjusted each tick by net flow signal × α_fx. //! //! Shadow economy (D-174): per-node intensity (0.0–1.0) seeded from political //! zone, hop distance, gate topology, and currency zone. use std::collections::BTreeMap; use rand::SeedableRng; use rand_chacha::ChaCha8Rng; use crate::db::Economy; use crate::prng::{derive_seed, standard_normal}; // --------------------------------------------------------------------------- // Constants // --------------------------------------------------------------------------- /// Cross-zone conversion friction (D-172): applied when trade crosses /// TRACTUS_PRIMARY ↔ MARK_PRIMARY boundaries. pub const ZONE_FRICTION: f64 = 0.03; /// Exchange rate adjustment rate per tick: how strongly net cross-zone /// flow imbalance moves the Tractus/Mark rate. const ALPHA_FX: f64 = 0.002; /// Exchange rate bounds (D-171): hard clamp to prevent runaway divergence. const FX_RATE_MIN: f64 = 0.5; const FX_RATE_MAX: f64 = 2.0; /// Maximum shadow economy intensity for dead-end topology bonus. const DEAD_END_SHADOW_BONUS: f64 = 0.10; /// Shadow economy noise standard deviation (log-normal jitter per node). const SHADOW_NOISE_SIGMA: f64 = 0.08; // --------------------------------------------------------------------------- // Shadow economy // --------------------------------------------------------------------------- /// Per-node shadow economy intensity (0.0–1.0). /// /// Seeds from: political zone, hop distance, gate topology, currency zone. /// Reference bands (D-174): core ~0.0–0.2, mid-reach ~0.3–0.6, frontier ~0.6–0.9. pub struct ShadowEconomy { /// system_id → shadow intensity [0.0, 1.0] pub intensity: BTreeMap, } pub fn seed_shadow_economy(economy: &Economy, run_seed: u64) -> ShadowEconomy { let mut intensity = BTreeMap::new(); for (system_id, sys) in &economy.systems { // Base from hop distance: clamp to [0.0, 0.6] range let hop_base = (sys.hop_distance as f64 / 15.0).clamp(0.0, 0.6); // Political zone modifier let zone_mod = match sys.political_zone.as_deref() { Some("institutional_core") => -0.25, Some("earth_sphere") | Some("diplomatic_periphery") => -0.15, Some("commercial_mid_reach") | Some("commercial_periphery") => 0.0, Some("research_periphery") => 0.05, Some("contested_frontier") | Some("deep_reach_isolate") => 0.15, _ => 0.0, }; // Gate topology: dead-end systems are harder to police let topology_mod = if sys.gate_topology.as_deref() == Some("dead_end") { DEAD_END_SHADOW_BONUS } else { 0.0 }; // Currency zone: Compact friction drives principled shadow economy let currency_mod = if sys.currency_zone == "MARK_PRIMARY" { 0.20 } else { 0.0 }; let base = (hop_base + zone_mod + topology_mod + currency_mod).clamp(0.0, 0.95); // Per-node PRNG jitter (Box-Muller) let node_seed = derive_seed(run_seed, system_id); let mut rng = ChaCha8Rng::seed_from_u64(node_seed); let noise = standard_normal(&mut rng) * SHADOW_NOISE_SIGMA; let final_intensity = (base + noise).clamp(0.0, 1.0); intensity.insert(system_id.clone(), final_intensity); } ShadowEconomy { intensity } } // --------------------------------------------------------------------------- // Exchange rate // --------------------------------------------------------------------------- /// Mutable exchange rate state updated each tick. #[derive(Debug, Clone)] pub struct CurrencyState { /// Tractus/Mark rate: how many Marks 1 Tractus buys. /// 1.0 = parity. >1.0 = Tractus stronger (Mark depreciated). pub tractus_mark_rate: f64, /// Net cross-zone Tractus→Mark commodity flow accumulated this tick. /// Positive = Tractus zone exporting to Mark zone (Mark zone demand >). pub net_cross_zone_flow: f64, } impl CurrencyState { pub fn new() -> Self { CurrencyState { tractus_mark_rate: 1.0, net_cross_zone_flow: 0.0, } } /// Adjust exchange rate from net cross-zone trade imbalance. /// /// If Tractus zone exports more than it imports from the Mark zone, /// demand for Tractus rises → Tractus appreciates (rate increases). pub fn update_rate(&mut self) { // Positive net flow (Tractus→Mark) → Tractus stronger → rate rises let adjustment = ALPHA_FX * self.net_cross_zone_flow; self.tractus_mark_rate = (self.tractus_mark_rate + adjustment).clamp(FX_RATE_MIN, FX_RATE_MAX); self.net_cross_zone_flow = 0.0; // reset accumulator for next tick } /// Apply an additive exchange rate delta from an `ExchangeShock` event (D-180). /// /// The result is clamped to the hard bounds `[FX_RATE_MIN, FX_RATE_MAX]`. pub fn apply_exchange_shock(&mut self, delta: f64) { if delta != 0.0 { self.tractus_mark_rate = (self.tractus_mark_rate + delta).clamp(FX_RATE_MIN, FX_RATE_MAX); } } /// Transport cost factor from `from_zone` to `to_zone`. /// /// Cross-zone (TRACTUS ↔ MARK) incurs an additional 3% friction. /// Sol (GJ 0, MIXED) neither adds nor removes friction. pub fn zone_friction_factor(&self, from_zone: &str, to_zone: &str) -> f64 { let cross_zone = (from_zone == "TRACTUS_PRIMARY" && to_zone == "MARK_PRIMARY") || (from_zone == "MARK_PRIMARY" && to_zone == "TRACTUS_PRIMARY"); if cross_zone { ZONE_FRICTION } else { 0.0 } } }