Skip to main content

proof_engine/worldgen/
history.rs

1//! History generation — agent-based civilization simulation.
2//!
3//! Simulates civilizations over thousands of years: founding, expansion,
4//! warfare, trade, cultural development, and decline.
5
6use super::{Rng};
7use super::biomes::BiomeMap;
8use super::settlements::Settlement;
9
10/// A civilization in the world.
11#[derive(Debug, Clone)]
12pub struct Civilization {
13    pub id: u32,
14    pub name: String,
15    pub founding_year: i32,
16    pub collapse_year: Option<i32>,
17    pub capital_settlement: u32,
18    pub settlements: Vec<u32>,
19    pub population: u64,
20    pub technology_level: f32,
21    pub military_strength: f32,
22    pub culture_score: f32,
23    pub trade_score: f32,
24    pub government: GovernmentType,
25    pub religion: ReligionType,
26    pub relations: Vec<(u32, Relation)>,
27    pub historical_events: Vec<HistoricalEvent>,
28    pub traits: Vec<CivTrait>,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum GovernmentType {
33    Tribal, Monarchy, Republic, Theocracy, Empire, Oligarchy, Democracy, Anarchy,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ReligionType {
38    Animism, Polytheism, Monotheism, Philosophy, Ancestor, Nature, Void, Chaos,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Relation {
43    Allied, Friendly, Neutral, Rival, AtWar, Vassal, Overlord,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum CivTrait {
48    Warlike, Peaceful, Mercantile, Scholarly, Nomadic, Seafaring, Religious, Isolationist,
49}
50
51/// A historical event.
52#[derive(Debug, Clone)]
53pub struct HistoricalEvent {
54    pub year: i32,
55    pub event_type: EventType,
56    pub description: String,
57    pub participants: Vec<u32>,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum EventType {
62    Founding, War, Peace, Trade, Discovery, Plague, Famine, GoldenAge,
63    Collapse, Revolution, Migration, Alliance, Betrayal, HeroRise,
64    ArtifactCreation, TempleBuilt, CityFounded, GreatWork,
65}
66
67/// Simulate civilization history.
68pub fn simulate(
69    settlements: &[Settlement],
70    biome_map: &BiomeMap,
71    years: usize,
72    num_civs: usize,
73    rng: &mut Rng,
74) -> Vec<Civilization> {
75    let num_civs = num_civs.min(settlements.len());
76    if num_civs == 0 { return Vec::new(); }
77
78    // Found initial civilizations at best settlements
79    let mut civs: Vec<Civilization> = (0..num_civs)
80        .map(|i| {
81            let settlement = &settlements[i];
82            let traits = vec![random_trait(rng), random_trait(rng)];
83            Civilization {
84                id: i as u32,
85                name: generate_civ_name(rng),
86                founding_year: -(rng.range_u32(500, years as u32) as i32),
87                collapse_year: None,
88                capital_settlement: settlement.id,
89                settlements: vec![settlement.id],
90                population: rng.range_usize(1000, 10000) as u64,
91                technology_level: rng.range_f32(0.1, 0.3),
92                military_strength: rng.range_f32(0.1, 0.5),
93                culture_score: rng.range_f32(0.1, 0.4),
94                trade_score: rng.range_f32(0.1, 0.3),
95                government: random_government(rng),
96                religion: random_religion(rng),
97                relations: Vec::new(),
98                historical_events: Vec::new(),
99                traits,
100            }
101        })
102        .collect();
103
104    // Simulate year by year
105    for year in 0..years as i32 {
106        let adjusted_year = year - (years as i32 / 2);
107
108        for ci in 0..civs.len() {
109            if civs[ci].collapse_year.is_some() { continue; }
110
111            // Population growth
112            let growth = 1.0 + 0.01 * civs[ci].technology_level as f64;
113            civs[ci].population = (civs[ci].population as f64 * growth) as u64;
114
115            // Technology advancement
116            civs[ci].technology_level += rng.range_f32(0.0, 0.005);
117
118            // Random events
119            let event_roll = rng.next_f32();
120            if event_roll < 0.01 {
121                // War
122                if civs.len() > 1 {
123                    let target = rng.range_usize(0, civs.len());
124                    if target != ci && civs[target].collapse_year.is_none() {
125                        let target_name = civs[target].name.clone();
126                        let target_id = civs[target].id;
127                        let ci_id = civs[ci].id;
128                        civs[ci].historical_events.push(HistoricalEvent {
129                            year: adjusted_year,
130                            event_type: EventType::War,
131                            description: format!("War with {}", target_name),
132                            participants: vec![ci_id, target_id],
133                        });
134                    }
135                }
136            } else if event_roll < 0.02 {
137                // Discovery
138                let cid = civs[ci].id;
139                civs[ci].technology_level += 0.05;
140                civs[ci].historical_events.push(HistoricalEvent {
141                    year: adjusted_year,
142                    event_type: EventType::Discovery,
143                    description: "A great discovery was made".to_string(),
144                    participants: vec![cid],
145                });
146            } else if event_roll < 0.025 {
147                // Plague
148                let cid = civs[ci].id;
149                civs[ci].population = (civs[ci].population as f64 * 0.7) as u64;
150                civs[ci].historical_events.push(HistoricalEvent {
151                    year: adjusted_year,
152                    event_type: EventType::Plague,
153                    description: "A terrible plague swept the land".to_string(),
154                    participants: vec![cid],
155                });
156            } else if event_roll < 0.03 {
157                // Golden age
158                let cid = civs[ci].id;
159                civs[ci].culture_score += 0.1;
160                civs[ci].historical_events.push(HistoricalEvent {
161                    year: adjusted_year,
162                    event_type: EventType::GoldenAge,
163                    description: "A golden age of prosperity".to_string(),
164                    participants: vec![cid],
165                });
166            } else if event_roll < 0.032 && civs[ci].population < 100 {
167                // Collapse
168                let cid = civs[ci].id;
169                let cname = civs[ci].name.clone();
170                civs[ci].collapse_year = Some(adjusted_year);
171                civs[ci].historical_events.push(HistoricalEvent {
172                    year: adjusted_year,
173                    event_type: EventType::Collapse,
174                    description: format!("The {} civilization collapsed", cname),
175                    participants: vec![cid],
176                });
177            }
178        }
179    }
180
181    civs
182}
183
184fn random_trait(rng: &mut Rng) -> CivTrait {
185    match rng.range_u32(0, 8) {
186        0 => CivTrait::Warlike,
187        1 => CivTrait::Peaceful,
188        2 => CivTrait::Mercantile,
189        3 => CivTrait::Scholarly,
190        4 => CivTrait::Nomadic,
191        5 => CivTrait::Seafaring,
192        6 => CivTrait::Religious,
193        _ => CivTrait::Isolationist,
194    }
195}
196
197fn random_government(rng: &mut Rng) -> GovernmentType {
198    match rng.range_u32(0, 8) {
199        0 => GovernmentType::Tribal,
200        1 => GovernmentType::Monarchy,
201        2 => GovernmentType::Republic,
202        3 => GovernmentType::Theocracy,
203        4 => GovernmentType::Empire,
204        5 => GovernmentType::Oligarchy,
205        6 => GovernmentType::Democracy,
206        _ => GovernmentType::Anarchy,
207    }
208}
209
210fn random_religion(rng: &mut Rng) -> ReligionType {
211    match rng.range_u32(0, 8) {
212        0 => ReligionType::Animism,
213        1 => ReligionType::Polytheism,
214        2 => ReligionType::Monotheism,
215        3 => ReligionType::Philosophy,
216        4 => ReligionType::Ancestor,
217        5 => ReligionType::Nature,
218        6 => ReligionType::Void,
219        _ => ReligionType::Chaos,
220    }
221}
222
223fn generate_civ_name(rng: &mut Rng) -> String {
224    let roots = ["Ael", "Dor", "Val", "Khor", "Thal", "Zer", "Myr", "Nor",
225        "Sar", "Eld", "Vor", "Ash", "Ith", "Orn", "Bel", "Fen"];
226    let suffixes = ["heim", "gard", "land", "oria", "ium", "eth", "zan",
227        "dor", "keth", "mar", "wen", "ost", "uri", "in"];
228    let root = roots[rng.next_u64() as usize % roots.len()];
229    let suffix = suffixes[rng.next_u64() as usize % suffixes.len()];
230    format!("{}{}", root, suffix)
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_simulate_history() {
239        let settlements: Vec<Settlement> = (0..5).map(|i| Settlement {
240            id: i, name: format!("Town{i}"), grid_x: i as usize * 10, grid_y: 10,
241            size: super::super::settlements::SettlementSize::Village,
242            population: 500, buildings: Vec::new(), roads: Vec::new(),
243            biome: super::super::biomes::Biome::Grassland, near_river: false,
244            near_coast: false, owner_civ: None, founded_year: -1000,
245            resources: 0.5, defense: 0.3,
246        }).collect();
247        let biome_map = super::super::biomes::BiomeMap {
248            width: 64, height: 64,
249            biomes: vec![super::super::biomes::Biome::Grassland; 64 * 64],
250        };
251        let mut rng = Rng::new(42);
252        let civs = simulate(&settlements, &biome_map, 1000, 3, &mut rng);
253        assert_eq!(civs.len(), 3);
254        assert!(civs.iter().all(|c| !c.name.is_empty()));
255        assert!(civs.iter().any(|c| !c.historical_events.is_empty()));
256    }
257}