1pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
35pub struct Fixed {
36 pub raw: i64,
38}
39
40pub const SCALE: i64 = 1_000_000; impl 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 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
130pub type SimRng = ChaCha8Rng;
132
133pub fn create_rng(seed: u64) -> SimRng {
135 SimRng::seed_from_u64(seed)
136}
137
138pub 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 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}