1use hecs::{World, Bundle};
6use rand::SeedableRng;
7use rand::Rng;
8use rand_chacha::ChaCha8Rng;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11
12use super::Fixed;
13
14pub type SimRng = ChaCha8Rng;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct Position {
24 pub x: i32,
25 pub y: i32,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
30pub struct Citizen {
31 pub age: u32, pub health: Fixed, pub ideology: Fixed, pub welfare: Fixed, pub job: Option<JobType>,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
39pub enum JobType {
40 Farmer,
41 Warrior,
42 Scholar,
43 Trader,
44 Priest,
45 Admin,
46 Unemployed,
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub struct Building {
52 pub building_type: BuildingType,
53 pub hp: Fixed,
54 pub max_hp: Fixed,
55 pub position: Position,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub enum BuildingType {
60 Farm,
61 Mine,
62 Barracks,
63 Temple,
64 Market,
65 House,
66 CityCenter,
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
71pub struct Resources {
72 pub food: Fixed,
73 pub wood: Fixed,
74 pub metal: Fixed,
75 pub energy: Fixed, }
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct Production {
81 pub output_type: ResourceType,
82 pub rate: Fixed, }
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
86pub enum ResourceType {
87 Food,
88 Wood,
89 Metal,
90 Energy,
91}
92
93#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct MilitaryUnit {
96 pub unit_type: UnitType,
97 pub strength: Fixed,
98 pub morale: Fixed,
99 pub position: Position,
100 pub faction_id: u32,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
104pub enum UnitType {
105 Soldier,
106 Archer,
107 Knight,
108 Scout,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
117pub struct WorldState {
118 pub tick: u64,
119 pub population: u64,
120 pub energy_budget_joules: Fixed,
121 pub rng_seed: u64,
122 pub factions: HashMap<u32, String>,
124 pub faction_treasury: HashMap<u32, Fixed>,
126}
127
128impl Default for WorldState {
129 fn default() -> Self {
130 Self {
131 tick: 0,
132 population: 1_000_000,
133 energy_budget_joules: Fixed::from_num(1_000_000_000_000i64),
134 rng_seed: 42,
135 factions: HashMap::from([
136 (0, "Player".to_string()),
137 (1, "AI Faction A".to_string()),
138 (2, "AI Faction B".to_string()),
139 ]),
140 faction_treasury: HashMap::from([
141 (0, Fixed::from_num(10_000)),
142 (1, Fixed::from_num(8_000)),
143 (2, Fixed::from_num(8_000)),
144 ]),
145 }
146 }
147}
148
149pub struct Simulation {
151 pub state: WorldState,
152 pub world: World,
153 rng: SimRng,
154}
155
156impl Simulation {
157 pub fn new() -> Self {
159 let rng = SimRng::seed_from_u64(42);
160 let mut world = World::new();
161
162 Self::spawn_initial_entities(&mut world);
164
165 Self {
166 state: WorldState::default(),
167 world,
168 rng,
169 }
170 }
171
172 pub fn with_seed(seed: u64) -> Self {
174 let rng = SimRng::seed_from_u64(seed);
175 let mut world = World::new();
176 Self::spawn_initial_entities(&mut world);
177
178 Self {
179 state: WorldState {
180 rng_seed: seed,
181 ..Default::default()
182 },
183 world,
184 rng,
185 }
186 }
187
188 fn spawn_initial_entities(world: &mut World) {
190 for i in 0..100 {
192 let citizen = Citizen {
193 age: 20 + (i % 40),
194 health: Fixed::from_num(1),
195 ideology: Fixed::from_num((i as i64 % 20 - 10) as i32) / Fixed::from_num(10),
196 welfare: Fixed::from_num(7) / Fixed::from_num(10),
197 job: Some(JobType::Farmer),
198 };
199 let _ = world.spawn((citizen,));
200 }
201
202 let city = Building {
204 building_type: BuildingType::CityCenter,
205 hp: Fixed::from_num(1000),
206 max_hp: Fixed::from_num(1000),
207 position: Position { x: 0, y: 0 },
208 };
209 let _ = world.spawn((city,));
210
211 for i in 0..5 {
213 let farm = Building {
214 building_type: BuildingType::Farm,
215 hp: Fixed::from_num(200),
216 max_hp: Fixed::from_num(200),
217 position: Position { x: i as i32 - 2, y: 1 },
218 };
219 let _ = world.spawn((farm,));
220 }
221
222 for i in 0..10 {
224 let soldier = MilitaryUnit {
225 unit_type: UnitType::Soldier,
226 strength: Fixed::from_num(10),
227 morale: Fixed::from_num(1),
228 position: Position { x: i as i32, y: 0 },
229 faction_id: 0, };
231 let _ = world.spawn((soldier,));
232 }
233 }
234
235 pub fn rng_mut(&mut self) -> &mut SimRng {
237 &mut self.rng
238 }
239
240 pub fn tick(&mut self) {
242 self.state.tick += 1;
243
244 self.phase_production();
246 self.phase_citizen_lifecycle();
247 self.phase_military();
248 self.phase_economy();
249 }
250
251 fn phase_production(&mut self) {
253 let mut production: HashMap<ResourceType, Fixed> = HashMap::new();
254 production.insert(ResourceType::Food, Fixed::ZERO);
255 production.insert(ResourceType::Wood, Fixed::ZERO);
256 production.insert(ResourceType::Metal, Fixed::ZERO);
257
258 for (_, building) in self.world.query::<&Building>().iter() {
260 match building.building_type {
261 BuildingType::Farm => {
262 *production.get_mut(&ResourceType::Food).unwrap() += Fixed::from_num(10);
263 }
264 BuildingType::Mine => {
265 *production.get_mut(&ResourceType::Metal).unwrap() += Fixed::from_num(5);
266 }
267 _ => {}
268 }
269 }
270
271 tracing::debug!("Tick {} production: food={:?}, metal={:?}",
273 self.state.tick,
274 production.get(&ResourceType::Food),
275 production.get(&ResourceType::Metal));
276 }
277
278 fn phase_citizen_lifecycle(&mut self) {
280 let mut births: u32 = 0;
281
282 for (_, citizen) in self.world.query::<&mut Citizen>().iter() {
283 citizen.age += 1;
285
286 let change = Fixed::from_num(self.rng.gen_range(-5..=5)) / Fixed::from_num(100);
288 citizen.welfare = (citizen.welfare + change).clamp(Fixed::ZERO, Fixed::from_num(1));
289 }
290
291 if self.state.population > 0 && self.rng.gen_bool(0.001) {
293 births = 1;
294 }
295
296 self.state.population += births as u64;
297 }
298
299 fn phase_military(&mut self) {
301 for (_, unit) in self.world.query::<&mut MilitaryUnit>().iter() {
302 if unit.morale < Fixed::from_num(1) {
304 unit.morale = (unit.morale + Fixed::from_num(1) / Fixed::from_num(100))
305 .min(Fixed::from_num(1));
306 }
307 }
308 }
309
310 fn phase_economy(&mut self) {
312 let consumption = Fixed::from_num(self.state.population) / Fixed::from_num(1000);
314 self.state.energy_budget_joules =
315 (self.state.energy_budget_joules - consumption).max(Fixed::ZERO);
316 }
317
318 pub fn snapshot(&self) -> SimulationSnapshot {
320 let citizen_count = self.world.query::<&Citizen>().iter().count();
321 let building_count = self.world.query::<&Building>().iter().count();
322 let military_count = self.world.query::<&MilitaryUnit>().iter().count();
323
324 SimulationSnapshot {
325 tick: self.state.tick,
326 population: self.state.population,
327 citizen_count,
328 building_count,
329 military_count,
330 energy_budget: self.state.energy_budget_joules,
331 }
332 }
333}
334
335impl Default for Simulation {
336 fn default() -> Self {
337 Self::new()
338 }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
343pub struct SimulationSnapshot {
344 pub tick: u64,
345 pub population: u64,
346 pub citizen_count: usize,
347 pub building_count: usize,
348 pub military_count: usize,
349 pub energy_budget: Fixed,
350}
351
352#[cfg(test)]
357mod tests {
358 use super::*;
359
360 #[test]
361 fn test_simulation_creation() {
362 let sim = Simulation::new();
363 assert_eq!(sim.state.tick, 0);
364 }
365
366 #[test]
367 fn test_tick_advances() {
368 let mut sim = Simulation::new();
369 sim.tick();
370 assert_eq!(sim.state.tick, 1);
371 }
372
373 #[test]
374 fn test_initial_entities() {
375 let sim = Simulation::new();
376 let snapshot = sim.snapshot();
377 assert!(snapshot.citizen_count > 0);
378 assert!(snapshot.building_count > 0);
379 assert!(snapshot.military_count > 0);
380 }
381
382 #[test]
383 fn test_determinism() {
384 let mut sim1 = Simulation::with_seed(12345);
385 let mut sim2 = Simulation::with_seed(12345);
386
387 for _ in 0..100 {
388 sim1.tick();
389 sim2.tick();
390 }
391
392 assert_eq!(sim1.state.tick, sim2.state.tick);
393 assert_eq!(sim1.state.population, sim2.state.population);
394 }
395}