Skip to main content

civ_engine/
engine.rs

1//! CivLab Simulation Engine - Core Tick Loop with ECS
2//!
3//! This module provides the deterministic simulation loop with entity component system.
4
5use 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
14/// Seeded RNG for reproducible simulation
15pub type SimRng = ChaCha8Rng;
16
17// ============================================================================
18// COMPONENTS - Data attached to entities
19// ============================================================================
20
21/// Position on the hex grid
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct Position {
24    pub x: i32,
25    pub y: i32,
26}
27
28/// Citizen entity component
29#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
30pub struct Citizen {
31    pub age: u32,              // Age in years
32    pub health: Fixed,          // Health 0.0 - 1.0
33    pub ideology: Fixed,        // -1.0 (libertarian) to 1.0 (authoritarian)
34    pub welfare: Fixed,        // 0.0 - 1.0
35    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/// Building entity component
50#[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/// Resource storage component
70#[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,  // Joules
76}
77
78/// Production capability
79#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct Production {
81    pub output_type: ResourceType,
82    pub rate: Fixed,  // Per tick
83}
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/// Military unit component
94#[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// ============================================================================
112// WORLD STATE
113// ============================================================================
114
115/// Global world state
116#[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    /// Faction ID -> faction name
123    pub factions: HashMap<u32, String>,
124    /// Faction ID -> treasury balance
125    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
149/// Simulation engine combining state + ECS world
150pub struct Simulation {
151    pub state: WorldState,
152    pub world: World,
153    rng: SimRng,
154}
155
156impl Simulation {
157    /// Create new simulation with default state
158    pub fn new() -> Self {
159        let rng = SimRng::seed_from_u64(42);
160        let mut world = World::new();
161        
162        // Spawn initial entities
163        Self::spawn_initial_entities(&mut world);
164        
165        Self {
166            state: WorldState::default(),
167            world,
168            rng,
169        }
170    }
171    
172    /// Create simulation with custom seed
173    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    /// Spawn initial world entities
189    fn spawn_initial_entities(world: &mut World) {
190        // Create initial citizens
191        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        // Create city center
203        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        // Create farms
212        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        // Create initial military
223        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,  // Player faction
230            };
231            let _ = world.spawn((soldier,));
232        }
233    }
234    
235    /// Get mutable reference to RNG
236    pub fn rng_mut(&mut self) -> &mut SimRng {
237        &mut self.rng
238    }
239    
240    /// Advance simulation by one tick
241    pub fn tick(&mut self) {
242        self.state.tick += 1;
243        
244        // Run simulation phases
245        self.phase_production();
246        self.phase_citizen_lifecycle();
247        self.phase_military();
248        self.phase_economy();
249    }
250    
251    /// Production phase - buildings produce resources
252    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        // Collect production from buildings
259        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        // Apply production to state (simplified - would go to resources in full impl)
272        tracing::debug!("Tick {} production: food={:?}, metal={:?}", 
273            self.state.tick,
274            production.get(&ResourceType::Food),
275            production.get(&ResourceType::Metal));
276    }
277    
278    /// Citizen lifecycle phase
279    fn phase_citizen_lifecycle(&mut self) {
280        let mut births: u32 = 0;
281        
282        for (_, citizen) in self.world.query::<&mut Citizen>().iter() {
283            // Age citizens
284            citizen.age += 1;
285            
286            // Simple welfare decay/growth based on random
287            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        // Births based on welfare
292        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    /// Military phase
300    fn phase_military(&mut self) {
301        for (_, unit) in self.world.query::<&mut MilitaryUnit>().iter() {
302            // Morale recovery
303            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    /// Economy phase - energy consumption
311    fn phase_economy(&mut self) {
312        // Base energy consumption per citizen
313        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    /// Get snapshot of current state
319    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/// Snapshot of simulation state
342#[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// ============================================================================
353// TESTS
354// ============================================================================
355
356#[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}