Skip to main content

civ_engine/
lib.rs

1//! CivLab Deterministic Simulation Engine
2//! 
3//! Uses fixed-point arithmetic for deterministic simulation results.
4//! Uses i64 with scaling for deterministic calculations.
5//!
6//! ## Modules
7//!
8//! - `engine` - Full ECS-based simulation with tick loop
9//! - `step` - Simple step function for basic simulation
10//! - `policy` - Policy/consumption calculations
11//! - `metrics` - Tyranny/legitimacy metrics
12//! - `io` - File I/O utilities
13
14pub mod engine;
15pub mod policy;
16pub mod metrics;
17pub mod io;
18
19pub use engine::{
20    Simulation, SimulationSnapshot, WorldState,
21    Position, Citizen, JobType, Building, BuildingType,
22    Resources, Production, ResourceType, MilitaryUnit, UnitType,
23};
24
25pub use policy::{effective_consumption, PolicyInput};
26pub use metrics::{compute, Metrics};
27
28use rand::SeedableRng;
29use rand_chacha::ChaCha8Rng;
30
31/// Fixed-point type: i64 with 18 decimal places of precision
32/// Stored as raw i64, divided by 10^18 for actual value
33/// This ensures deterministic simulation across platforms
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
35pub struct Fixed {
36    /// Raw value scaled by 10^18
37    pub raw: i64,
38}
39
40pub const SCALE: i64 = 1_000_000; // 10^6 (easier to work with)
41
42impl Fixed {
43    pub const ZERO: Fixed = Fixed { raw: 0 };
44    pub const ONE: Fixed = Fixed { raw: SCALE };
45    
46    pub fn from_num<T: TryInto<i128>>(n: T) -> Self {
47        let scaled = n.try_into().unwrap_or(0) * SCALE as i128;
48        Fixed { raw: scaled as i64 }
49    }
50    
51    pub fn from_raw(raw: i64) -> Self {
52        Fixed { raw }
53    }
54    
55    pub fn to_f64(self) -> f64 {
56        self.raw as f64 / SCALE as f64
57    }
58    
59    pub fn saturating_add(self, other: Fixed) -> Fixed {
60        Fixed { raw: self.raw.saturating_add(other.raw) }
61    }
62    
63    pub fn saturating_sub(self, other: Fixed) -> Fixed {
64        Fixed { raw: self.raw.saturating_sub(other.raw) }
65    }
66    
67    pub fn clamp(self, min: Fixed, max: Fixed) -> Fixed {
68        Fixed { raw: self.raw.clamp(min.raw, max.raw) }
69    }
70}
71
72impl std::ops::Add for Fixed {
73    type Output = Fixed;
74    fn add(self, other: Fixed) -> Fixed {
75        Fixed { raw: self.raw + other.raw }
76    }
77}
78
79impl std::ops::Sub for Fixed {
80    type Output = Fixed;
81    fn sub(self, other: Fixed) -> Fixed {
82        Fixed { raw: self.raw - other.raw }
83    }
84}
85
86impl std::ops::Mul for Fixed {
87    type Output = Fixed;
88    fn mul(self, other: Fixed) -> Fixed {
89        // Multiply and divide by scale to maintain precision
90        let result = (self.raw as i128) * (other.raw as i128) / SCALE as i128;
91        Fixed { raw: result as i64 }
92    }
93}
94
95impl std::ops::Div for Fixed {
96    type Output = Fixed;
97    fn div(self, other: Fixed) -> Fixed {
98        let result = (self.raw as i128 * SCALE as i128) / (other.raw.max(1) as i128);
99        Fixed { raw: result as i64 }
100    }
101}
102
103impl std::ops::AddAssign for Fixed {
104    fn add_assign(&mut self, other: Fixed) {
105        self.raw += other.raw;
106    }
107}
108
109impl std::ops::SubAssign for Fixed {
110    fn sub_assign(&mut self, other: Fixed) {
111        self.raw -= other.raw;
112    }
113}
114
115impl serde::Serialize for Fixed {
116    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
117    where S: serde::Serializer {
118        serializer.serialize_f64(self.to_f64())
119    }
120}
121
122impl<'de> serde::Deserialize<'de> for Fixed {
123    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124    where D: serde::Deserializer<'de> {
125        let f = f64::deserialize(deserializer)?;
126        Ok(Fixed::from_num((f * SCALE as f64) as i64))
127    }
128}
129
130/// Seeded RNG for deterministic simulation
131pub type SimRng = ChaCha8Rng;
132
133/// Create a seeded RNG from world state
134pub fn create_rng(seed: u64) -> SimRng {
135    SimRng::seed_from_u64(seed)
136}
137
138/// Advance simulation by one tick (simple API)
139pub fn step(mut state: WorldState, consumption_joules: Fixed) -> WorldState {
140    state.tick += 1;
141    let result = state.energy_budget_joules.saturating_sub(consumption_joules);
142    state.energy_budget_joules = if result.raw < 0 { Fixed::ZERO } else { result };
143    state
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use std::collections::HashMap;
150
151    #[test]
152    fn step_advances_tick() {
153        let s = WorldState::default();
154        let n = step(s, Fixed::from_num(100));
155        assert_eq!(n.tick, 1);
156    }
157
158    #[test]
159    fn step_decreases_energy() {
160        let s = WorldState::default();
161        // Initial energy is 1_000_000_000_000, subtract 1000 = 999_999_999_000
162        let expected = Fixed::from_num(1_000_000_000_000i64) - Fixed::from_num(1000i64);
163        let n = step(s, Fixed::from_num(1000));
164        assert_eq!(n.energy_budget_joules, expected);
165    }
166
167    #[test]
168    fn step_energy_floor_at_zero() {
169        let mut s = WorldState::default();
170        s.energy_budget_joules = Fixed::from_num(50);
171        let n = step(s, Fixed::from_num(100));
172        assert_eq!(n.energy_budget_joules, Fixed::ZERO);
173    }
174
175    #[test]
176    fn determinism_same_seed_same_output() {
177        let s1 = WorldState { 
178            tick: 0, 
179            population: 100, 
180            energy_budget_joules: Fixed::from_num(1000), 
181            rng_seed: 12345,
182            factions: HashMap::new(),
183            faction_treasury: HashMap::new(),
184        };
185        let s2 = WorldState { 
186            tick: 0, 
187            population: 100, 
188            energy_budget_joules: Fixed::from_num(1000), 
189            rng_seed: 12345,
190            factions: HashMap::new(),
191            faction_treasury: HashMap::new(),
192        };
193        
194        let r1 = step(s1, Fixed::from_num(10));
195        let r2 = step(s2, Fixed::from_num(10));
196        
197        assert_eq!(r1.tick, r2.tick);
198        assert_eq!(r1.energy_budget_joules, r2.energy_budget_joules);
199    }
200}