Skip to main content

proof_engine/procedural/
world.rs

1//! World generation — heightmaps, climate, rivers, roads, settlements, history.
2//!
3//! Generates a complete world map from noise-based terrain through climate
4//! simulation, river carving, settlement placement, and procedural history.
5
6use super::Rng;
7use std::collections::{BinaryHeap, HashSet, VecDeque};
8use std::cmp::Ordering;
9
10// ── BiomeId ───────────────────────────────────────────────────────────────────
11
12/// Opaque biome identifier.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct BiomeId(pub u8);
15
16impl BiomeId {
17    pub const OCEAN:           BiomeId = BiomeId(0);
18    pub const COAST:           BiomeId = BiomeId(1);
19    pub const DESERT:          BiomeId = BiomeId(2);
20    pub const SAVANNA:         BiomeId = BiomeId(3);
21    pub const TROPICAL_FOREST: BiomeId = BiomeId(4);
22    pub const GRASSLAND:       BiomeId = BiomeId(5);
23    pub const SHRUBLAND:       BiomeId = BiomeId(6);
24    pub const TEMPERATE_FOREST:BiomeId = BiomeId(7);
25    pub const BOREAL_FOREST:   BiomeId = BiomeId(8);
26    pub const TUNDRA:          BiomeId = BiomeId(9);
27    pub const SNOW:            BiomeId = BiomeId(10);
28    pub const MOUNTAIN:        BiomeId = BiomeId(11);
29}
30
31/// Parameters describing a biome.
32#[derive(Debug, Clone)]
33pub struct BiomeParams {
34    pub id:            BiomeId,
35    pub name:          &'static str,
36    pub glyph_char:    char,
37    /// Approximate colour as (r, g, b) in 0..255.
38    pub color:         (u8, u8, u8),
39    pub temperature_min: f32,
40    pub temperature_max: f32,
41    pub moisture_min:  f32,
42    pub moisture_max:  f32,
43    pub elevation_min: f32,
44    pub elevation_max: f32,
45}
46
47impl BiomeParams {
48    pub fn all() -> Vec<BiomeParams> {
49        vec![
50            BiomeParams { id: BiomeId::OCEAN,           name: "Ocean",            glyph_char: '~', color: (30,  80,  160), temperature_min: -1.0, temperature_max: 1.0, moisture_min: 0.0, moisture_max: 1.0, elevation_min: -1.0, elevation_max: 0.0  },
51            BiomeParams { id: BiomeId::COAST,           name: "Coast",            glyph_char: '≈', color: (60,  120, 180), temperature_min: -1.0, temperature_max: 1.0, moisture_min: 0.0, moisture_max: 1.0, elevation_min: 0.0,  elevation_max: 0.1  },
52            BiomeParams { id: BiomeId::DESERT,          name: "Desert",           glyph_char: '∴', color: (210, 185, 130), temperature_min: 0.5,  temperature_max: 1.0, moisture_min: 0.0, moisture_max: 0.2, elevation_min: 0.0,  elevation_max: 1.0  },
53            BiomeParams { id: BiomeId::SAVANNA,         name: "Savanna",          glyph_char: ',', color: (180, 160, 90),  temperature_min: 0.4,  temperature_max: 1.0, moisture_min: 0.2, moisture_max: 0.4, elevation_min: 0.0,  elevation_max: 1.0  },
54            BiomeParams { id: BiomeId::TROPICAL_FOREST, name: "Tropical Forest",  glyph_char: '♣', color: (30,  110, 40),  temperature_min: 0.4,  temperature_max: 1.0, moisture_min: 0.6, moisture_max: 1.0, elevation_min: 0.0,  elevation_max: 1.0  },
55            BiomeParams { id: BiomeId::GRASSLAND,       name: "Grassland",        glyph_char: '"', color: (120, 170, 60),  temperature_min: 0.0,  temperature_max: 0.7, moisture_min: 0.3, moisture_max: 0.6, elevation_min: 0.0,  elevation_max: 1.0  },
56            BiomeParams { id: BiomeId::SHRUBLAND,       name: "Shrubland",        glyph_char: '\'',color: (150, 140, 80),  temperature_min: 0.0,  temperature_max: 0.7, moisture_min: 0.1, moisture_max: 0.4, elevation_min: 0.0,  elevation_max: 1.0  },
57            BiomeParams { id: BiomeId::TEMPERATE_FOREST,name: "Temperate Forest", glyph_char: '♠', color: (60,  130, 60),  temperature_min: -0.3, temperature_max: 0.5, moisture_min: 0.4, moisture_max: 0.8, elevation_min: 0.0,  elevation_max: 0.8  },
58            BiomeParams { id: BiomeId::BOREAL_FOREST,   name: "Boreal Forest",    glyph_char: '▲', color: (40,  100, 60),  temperature_min: -0.6, temperature_max: 0.1, moisture_min: 0.3, moisture_max: 0.7, elevation_min: 0.0,  elevation_max: 0.8  },
59            BiomeParams { id: BiomeId::TUNDRA,          name: "Tundra",           glyph_char: '-', color: (160, 160, 140), temperature_min: -1.0, temperature_max: -0.3,moisture_min: 0.1, moisture_max: 0.5, elevation_min: 0.0,  elevation_max: 0.8  },
60            BiomeParams { id: BiomeId::SNOW,            name: "Snow",             glyph_char: '*', color: (230, 240, 255), temperature_min: -1.0, temperature_max: -0.3,moisture_min: 0.0, moisture_max: 1.0, elevation_min: 0.0,  elevation_max: 1.0  },
61            BiomeParams { id: BiomeId::MOUNTAIN,        name: "Mountain",         glyph_char: '^', color: (130, 120, 120), temperature_min: -1.0, temperature_max: 1.0, moisture_min: 0.0, moisture_max: 1.0, elevation_min: 0.7,  elevation_max: 1.0  },
62        ]
63    }
64}
65
66// ── BiomeClassifier ───────────────────────────────────────────────────────────
67
68/// Whittaker biome diagram: classifies (temperature × moisture) → BiomeId.
69pub struct BiomeClassifier {
70    params: Vec<BiomeParams>,
71}
72
73impl BiomeClassifier {
74    pub fn new() -> Self {
75        Self { params: BiomeParams::all() }
76    }
77
78    /// Classify a point on the world map.
79    pub fn classify(&self, temperature: f32, moisture: f32, elevation: f32) -> BiomeId {
80        // Ocean / coast first
81        if elevation < 0.0 { return BiomeId::OCEAN; }
82        if elevation < 0.1 { return BiomeId::COAST; }
83        // High elevation → mountain
84        if elevation > 0.75 {
85            if temperature < -0.2 { return BiomeId::SNOW; }
86            return BiomeId::MOUNTAIN;
87        }
88        // Cold → snow / tundra / boreal
89        if temperature < -0.4 {
90            if moisture < 0.15 { return BiomeId::SNOW; }
91            return BiomeId::TUNDRA;
92        }
93        if temperature < 0.0 {
94            if moisture < 0.3 { return BiomeId::TUNDRA; }
95            return BiomeId::BOREAL_FOREST;
96        }
97        // Temperate
98        if temperature < 0.4 {
99            if moisture < 0.2 { return BiomeId::SHRUBLAND; }
100            if moisture < 0.5 { return BiomeId::GRASSLAND; }
101            return BiomeId::TEMPERATE_FOREST;
102        }
103        // Warm / hot
104        if moisture < 0.15 { return BiomeId::DESERT; }
105        if moisture < 0.35 { return BiomeId::SAVANNA; }
106        if moisture < 0.6  { return BiomeId::GRASSLAND; }
107        BiomeId::TROPICAL_FOREST
108    }
109
110    pub fn params_for(&self, id: BiomeId) -> Option<&BiomeParams> {
111        self.params.iter().find(|p| p.id == id)
112    }
113}
114
115impl Default for BiomeClassifier {
116    fn default() -> Self { Self::new() }
117}
118
119// ── WorldCell ─────────────────────────────────────────────────────────────────
120
121/// A single cell on the world map.
122#[derive(Debug, Clone)]
123pub struct WorldCell {
124    pub elevation:     f32,
125    pub temperature:   f32,
126    pub moisture:      f32,
127    pub biome:         BiomeId,
128    pub river_id:      Option<u32>,
129    pub road_id:       Option<u32>,
130    pub settlement_id: Option<u32>,
131}
132
133impl Default for WorldCell {
134    fn default() -> Self {
135        Self {
136            elevation:     0.0,
137            temperature:   0.0,
138            moisture:      0.5,
139            biome:         BiomeId::OCEAN,
140            river_id:      None,
141            road_id:       None,
142            settlement_id: None,
143        }
144    }
145}
146
147// ── WorldMap ──────────────────────────────────────────────────────────────────
148
149/// The complete world map.
150#[derive(Debug, Clone)]
151pub struct WorldMap {
152    pub width:  usize,
153    pub height: usize,
154    pub cells:  Vec<WorldCell>,
155}
156
157impl WorldMap {
158    pub fn new(width: usize, height: usize) -> Self {
159        Self { width, height, cells: vec![WorldCell::default(); width * height] }
160    }
161
162    pub fn idx(&self, x: usize, y: usize) -> usize { y * self.width + x }
163
164    pub fn get(&self, x: usize, y: usize) -> &WorldCell {
165        &self.cells[self.idx(x, y)]
166    }
167
168    pub fn get_mut(&mut self, x: usize, y: usize) -> &mut WorldCell {
169        let i = self.idx(x, y);
170        &mut self.cells[i]
171    }
172
173    pub fn in_bounds(&self, x: i32, y: i32) -> bool {
174        x >= 0 && y >= 0 && (x as usize) < self.width && (y as usize) < self.height
175    }
176
177    pub fn elevation_at(&self, x: usize, y: usize) -> f32 {
178        self.get(x, y).elevation
179    }
180
181    /// Lowest-elevation neighbour (for river flow).
182    pub fn lowest_neighbour(&self, x: usize, y: usize) -> Option<(usize, usize)> {
183        let mut best = (x, y);
184        let mut best_e = self.get(x, y).elevation;
185        for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
186            let nx = x as i32 + dx;
187            let ny = y as i32 + dy;
188            if !self.in_bounds(nx, ny) { continue; }
189            let e = self.get(nx as usize, ny as usize).elevation;
190            if e < best_e { best_e = e; best = (nx as usize, ny as usize); }
191        }
192        if best == (x, y) { None } else { Some(best) }
193    }
194}
195
196// ── Noise helpers (no external noise crate needed for simple Perlin) ───────────
197
198/// Simple value-noise implementation (no external deps).
199fn hash_noise(ix: i64, iy: i64) -> f32 {
200    let mut h = ix.wrapping_mul(1619).wrapping_add(iy.wrapping_mul(31337));
201    h = (h ^ (h >> 16)).wrapping_mul(0x45d9f3b);
202    h = (h ^ (h >> 16)).wrapping_mul(0x45d9f3b);
203    h = h ^ (h >> 16);
204    (h as f32).abs() / i64::MAX as f32
205}
206
207fn smoothstep(t: f32) -> f32 { t * t * (3.0 - 2.0 * t) }
208
209fn lerp(a: f32, b: f32, t: f32) -> f32 { a + t * (b - a) }
210
211/// Sample 2D value noise at (x, y).
212fn value_noise(x: f32, y: f32) -> f32 {
213    let ix = x.floor() as i64;
214    let iy = y.floor() as i64;
215    let fx = x - x.floor();
216    let fy = y - y.floor();
217    let sx = smoothstep(fx);
218    let sy = smoothstep(fy);
219    let v00 = hash_noise(ix,     iy);
220    let v10 = hash_noise(ix + 1, iy);
221    let v01 = hash_noise(ix,     iy + 1);
222    let v11 = hash_noise(ix + 1, iy + 1);
223    lerp(lerp(v00, v10, sx), lerp(v01, v11, sx), sy)
224}
225
226/// Multi-octave fractal noise (fBm) with domain warping.
227fn fbm_warped(x: f32, y: f32, octaves: u32, warp_strength: f32) -> f32 {
228    // Domain warp: offset the sample position by another noise layer
229    let wx = value_noise(x * 1.3 + 7.1, y * 1.3 + 2.7) * warp_strength;
230    let wy = value_noise(x * 1.3 + 1.2, y * 1.3 + 9.3) * warp_strength;
231    let mut value = 0.0_f32;
232    let mut amplitude = 0.5_f32;
233    let mut frequency = 1.0_f32;
234    let mut max_val   = 0.0_f32;
235    for _ in 0..octaves {
236        value    += amplitude * value_noise((x + wx) * frequency, (y + wy) * frequency);
237        max_val  += amplitude;
238        amplitude *= 0.5;
239        frequency *= 2.0;
240    }
241    value / max_val
242}
243
244// ── HeightmapWorld ────────────────────────────────────────────────────────────
245
246/// Generates terrain using multi-octave Perlin noise with domain warping
247/// and a continent falloff mask.
248pub struct HeightmapWorld {
249    pub octaves:       u32,
250    pub scale:         f32,
251    pub warp_strength: f32,
252    pub sea_level:     f32,
253    pub continent_falloff: f32, // 0 = no falloff, 1 = strong island mask
254}
255
256impl Default for HeightmapWorld {
257    fn default() -> Self {
258        Self { octaves: 6, scale: 0.005, warp_strength: 0.4, sea_level: 0.42, continent_falloff: 0.8 }
259    }
260}
261
262impl HeightmapWorld {
263    pub fn new(octaves: u32, scale: f32, warp_strength: f32, sea_level: f32) -> Self {
264        Self { octaves, scale, warp_strength, sea_level, continent_falloff: 0.7 }
265    }
266
267    /// Fill a WorldMap's elevation, applying continent mask.
268    pub fn apply(&self, map: &mut WorldMap, seed: u64) {
269        let w = map.width as f32;
270        let h = map.height as f32;
271        let seed_f = (seed % 10000) as f32 * 0.1;
272
273        for y in 0..map.height {
274            for x in 0..map.width {
275                let nx = x as f32 * self.scale + seed_f;
276                let ny = y as f32 * self.scale + seed_f * 0.7;
277                let raw = fbm_warped(nx, ny, self.octaves, self.warp_strength);
278
279                // Continent falloff from centre
280                let dx = (x as f32 / w - 0.5) * 2.0;
281                let dy = (y as f32 / h - 0.5) * 2.0;
282                let dist = (dx * dx + dy * dy).sqrt().min(1.0);
283                let mask = 1.0 - dist.powf(1.5) * self.continent_falloff;
284
285                let elevation = (raw * mask * 2.0 - 1.0).clamp(-1.0, 1.0);
286                map.get_mut(x, y).elevation = elevation;
287            }
288        }
289    }
290}
291
292// ── ClimateSimulator ──────────────────────────────────────────────────────────
293
294/// Simulates temperature and moisture based on latitude, elevation, and ocean proximity.
295pub struct ClimateSimulator {
296    pub lapse_rate:        f32, // temperature drop per unit elevation gain
297    pub ocean_moisture:    f32, // moisture contribution from ocean cells
298    pub rain_shadow_decay: f32, // how quickly moisture drops on leeward side of mountains
299}
300
301impl Default for ClimateSimulator {
302    fn default() -> Self {
303        Self { lapse_rate: 0.6, ocean_moisture: 0.7, rain_shadow_decay: 0.4 }
304    }
305}
306
307impl ClimateSimulator {
308    pub fn new(lapse_rate: f32, ocean_moisture: f32, rain_shadow_decay: f32) -> Self {
309        Self { lapse_rate, ocean_moisture, rain_shadow_decay }
310    }
311
312    /// Assign temperature and moisture to every cell in the map.
313    pub fn apply(&self, map: &mut WorldMap) {
314        let h = map.height as f32;
315        let w = map.width;
316        let ht = map.height;
317
318        // Pass 1: temperature from latitude + elevation lapse
319        for y in 0..ht {
320            let lat_norm = y as f32 / h; // 0 = north, 1 = south
321            let lat_temp = (std::f32::consts::PI * lat_norm).cos(); // peaks at equator (mid)
322            for x in 0..w {
323                let elev = map.get(x, y).elevation.max(0.0);
324                let temp = lat_temp - elev * self.lapse_rate;
325                map.get_mut(x, y).temperature = temp.clamp(-1.0, 1.0);
326            }
327        }
328
329        // Pass 2: moisture advection west-to-east
330        // Collect elevation snapshot first
331        let elev_snapshot: Vec<f32> = map.cells.iter().map(|c| c.elevation).collect();
332
333        for y in 0..ht {
334            let mut moisture = 0.2_f32; // base moisture
335            for x in 0..w {
336                let elev = elev_snapshot[y * w + x];
337                if elev < 0.0 {
338                    // Over ocean: gain moisture
339                    moisture = (moisture + self.ocean_moisture * 0.1).min(1.0);
340                } else {
341                    // Over land: lose moisture to rainfall, more at high elevation
342                    let rainfall = moisture * (0.05 + elev * self.rain_shadow_decay * 0.1);
343                    moisture = (moisture - rainfall).max(0.0);
344                    // Rain shadow: if going downhill after peak, moisture stays low
345                }
346                map.get_mut(x, y).moisture = moisture.clamp(0.0, 1.0);
347            }
348        }
349
350        // Pass 3: proximity to ocean smoothing
351        // Build ocean mask
352        let is_ocean: Vec<bool> = map.cells.iter().map(|c| c.elevation < 0.0).collect();
353        let mut ocean_prox = vec![0.0_f32; w * ht];
354        // BFS from ocean cells up to radius 10
355        let mut queue: VecDeque<(usize, usize, u32)> = VecDeque::new();
356        let mut visited_op = vec![false; w * ht];
357        for y in 0..ht {
358            for x in 0..w {
359                if is_ocean[y * w + x] {
360                    ocean_prox[y * w + x] = 1.0;
361                    queue.push_back((x, y, 0));
362                    visited_op[y * w + x] = true;
363                }
364            }
365        }
366        let radius = 15u32;
367        while let Some((cx, cy, dist)) = queue.pop_front() {
368            if dist >= radius { continue; }
369            for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
370                let nx = cx as i32 + dx;
371                let ny = cy as i32 + dy;
372                if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= ht { continue; }
373                let ni = ny as usize * w + nx as usize;
374                if !visited_op[ni] {
375                    visited_op[ni] = true;
376                    ocean_prox[ni] = 1.0 - (dist + 1) as f32 / radius as f32;
377                    queue.push_back((nx as usize, ny as usize, dist + 1));
378                }
379            }
380        }
381        // Add ocean proximity to moisture
382        for y in 0..ht {
383            for x in 0..w {
384                let i = y * w + x;
385                let m = map.cells[i].moisture + ocean_prox[i] * self.ocean_moisture * 0.3;
386                map.cells[i].moisture = m.clamp(0.0, 1.0);
387            }
388        }
389    }
390}
391
392// ── RiverSystem ───────────────────────────────────────────────────────────────
393
394/// Generates rivers by flowing downhill from high-elevation sources.
395pub struct RiverSystem {
396    pub num_rivers:     usize,
397    pub min_source_elev: f32,
398    pub erosion_amount: f32,
399}
400
401impl Default for RiverSystem {
402    fn default() -> Self { Self { num_rivers: 12, min_source_elev: 0.5, erosion_amount: 0.02 } }
403}
404
405impl RiverSystem {
406    pub fn new(num_rivers: usize, min_source_elev: f32, erosion_amount: f32) -> Self {
407        Self { num_rivers, min_source_elev, erosion_amount }
408    }
409
410    /// Carve rivers into the map. Each river gets a unique river_id.
411    pub fn apply(&self, map: &mut WorldMap, rng: &mut Rng) {
412        let w = map.width;
413        let h = map.height;
414
415        // Collect candidate source cells (high elevation land)
416        let mut candidates: Vec<(usize, usize)> = Vec::new();
417        for y in 0..h {
418            for x in 0..w {
419                if map.get(x, y).elevation >= self.min_source_elev {
420                    candidates.push((x, y));
421                }
422            }
423        }
424        rng.shuffle(&mut candidates);
425
426        for river_idx in 0..self.num_rivers.min(candidates.len()) {
427            let (mut cx, mut cy) = candidates[river_idx];
428            let river_id = river_idx as u32 + 1;
429            let mut visited_r = HashSet::new();
430            let mut steps = 0usize;
431
432            loop {
433                if visited_r.contains(&(cx, cy)) { break; }
434                visited_r.insert((cx, cy));
435                map.get_mut(cx, cy).river_id = Some(river_id);
436                // Erode slightly
437                let e = map.get(cx, cy).elevation;
438                map.get_mut(cx, cy).elevation = (e - self.erosion_amount).max(-0.05);
439                // Add moisture along river
440                let m = map.get(cx, cy).moisture;
441                map.get_mut(cx, cy).moisture = (m + 0.1).min(1.0);
442
443                steps += 1;
444                if steps > w + h { break; } // prevent infinite loops
445
446                // Flow to lowest neighbour
447                match map.lowest_neighbour(cx, cy) {
448                    Some((nx, ny)) => {
449                        if map.get(nx, ny).elevation < 0.0 {
450                            // Reached the sea — delta
451                            map.get_mut(nx, ny).river_id = Some(river_id);
452                            break;
453                        }
454                        cx = nx; cy = ny;
455                    }
456                    None => break,
457                }
458            }
459        }
460    }
461}
462
463// ── Settlement ────────────────────────────────────────────────────────────────
464
465/// Settlement size/kind.
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467pub enum SettlementKind {
468    Village,
469    Town,
470    City,
471    Capital,
472}
473
474impl SettlementKind {
475    pub fn base_population(&self) -> u32 {
476        match self {
477            SettlementKind::Village  => 200,
478            SettlementKind::Town    => 2_000,
479            SettlementKind::City    => 20_000,
480            SettlementKind::Capital => 100_000,
481        }
482    }
483
484    pub fn glyph(&self) -> char {
485        match self {
486            SettlementKind::Village  => 'v',
487            SettlementKind::Town    => 'T',
488            SettlementKind::City    => 'C',
489            SettlementKind::Capital => '★',
490        }
491    }
492}
493
494/// A settlement on the world map.
495#[derive(Debug, Clone)]
496pub struct Settlement {
497    pub id:         u32,
498    pub position:   (usize, usize),
499    pub kind:       SettlementKind,
500    pub population: u32,
501    pub name:       String,
502    pub faction_id: Option<u32>,
503}
504
505impl Settlement {
506    pub fn new(id: u32, x: usize, y: usize, kind: SettlementKind, name: String) -> Self {
507        let base = kind.base_population();
508        Self {
509            id,
510            position: (x, y),
511            kind,
512            population: base,
513            name,
514            faction_id: None,
515        }
516    }
517}
518
519// ── SettlementPlacer ──────────────────────────────────────────────────────────
520
521/// Places settlements on flat, fertile land.
522pub struct SettlementPlacer {
523    pub num_settlements: usize,
524    pub min_separation:  f32, // in world cells
525    pub capital_count:   usize,
526    pub city_count:      usize,
527    pub town_count:      usize,
528}
529
530impl Default for SettlementPlacer {
531    fn default() -> Self {
532        Self { num_settlements: 20, min_separation: 15.0, capital_count: 1, city_count: 3, town_count: 6 }
533    }
534}
535
536impl SettlementPlacer {
537    /// Fertility score for placing settlements: low elevation land, near rivers, not desert.
538    fn fertility(cell: &WorldCell) -> f32 {
539        if cell.elevation < 0.05 { return 0.0; } // ocean/coast
540        if cell.elevation > 0.65 { return 0.0; } // mountains
541        let river_bonus = if cell.river_id.is_some() { 0.3 } else { 0.0 };
542        let temp_score = 1.0 - (cell.temperature - 0.3).abs();
543        let moist_score = cell.moisture.min(0.8);
544        (temp_score * 0.4 + moist_score * 0.4 + river_bonus).max(0.0)
545    }
546
547    pub fn place(&self, map: &mut WorldMap, names: &mut NameGenerator, rng: &mut Rng) -> Vec<Settlement> {
548        let w = map.width;
549        let h = map.height;
550
551        // Score all cells
552        let mut scored: Vec<(f32, usize, usize)> = (0..h).flat_map(|y| {
553            (0..w).map(move |x| (0.0_f32, x, y))
554        }).collect();
555        for (score, x, y) in &mut scored {
556            *score = Self::fertility(map.get(*x, *y));
557        }
558        scored.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(Ordering::Equal));
559
560        let mut settlements = Vec::new();
561        let mut placed_positions: Vec<(usize, usize)> = Vec::new();
562
563        // Determine kinds
564        let mut kinds: Vec<SettlementKind> = Vec::new();
565        for _ in 0..self.capital_count { kinds.push(SettlementKind::Capital); }
566        for _ in 0..self.city_count    { kinds.push(SettlementKind::City); }
567        for _ in 0..self.town_count    { kinds.push(SettlementKind::Town); }
568        while kinds.len() < self.num_settlements { kinds.push(SettlementKind::Village); }
569
570        let sep2 = self.min_separation * self.min_separation;
571        let mut kind_idx = 0;
572
573        for (_, x, y) in &scored {
574            if settlements.len() >= self.num_settlements { break; }
575            let x = *x; let y = *y;
576            // Check separation
577            let too_close = placed_positions.iter().any(|&(px, py)| {
578                let dx = px as f32 - x as f32;
579                let dy = py as f32 - y as f32;
580                dx * dx + dy * dy < sep2
581            });
582            if too_close { continue; }
583
584            let kind = kinds.get(kind_idx).copied().unwrap_or(SettlementKind::Village);
585            kind_idx += 1;
586            let name = names.generate(rng);
587            let id   = settlements.len() as u32 + 1;
588            let pop_jitter = rng.range_f32(0.7, 1.4);
589            let mut s = Settlement::new(id, x, y, kind, name);
590            s.population = (s.population as f32 * pop_jitter) as u32;
591            s.faction_id = Some(rng.range_usize(4) as u32 + 1);
592
593            map.get_mut(x, y).settlement_id = Some(id);
594            placed_positions.push((x, y));
595            settlements.push(s);
596        }
597
598        settlements
599    }
600}
601
602// ── RoadNetwork ───────────────────────────────────────────────────────────────
603
604/// Connects settlements with A* paths that prefer flat terrain.
605pub struct RoadNetwork {
606    pub road_cost_slope: f32, // how much elevation change costs
607}
608
609impl Default for RoadNetwork {
610    fn default() -> Self { Self { road_cost_slope: 5.0 } }
611}
612
613/// A* node for road pathfinding.
614#[derive(Debug, Clone)]
615struct AStarNode {
616    cost: f32,
617    x: usize,
618    y: usize,
619}
620
621impl PartialEq for AStarNode {
622    fn eq(&self, other: &Self) -> bool { self.cost == other.cost }
623}
624impl Eq for AStarNode {}
625impl PartialOrd for AStarNode {
626    fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
627}
628impl Ord for AStarNode {
629    fn cmp(&self, other: &Self) -> Ordering {
630        // Min-heap: reverse ordering
631        other.cost.partial_cmp(&self.cost).unwrap_or(Ordering::Equal)
632    }
633}
634
635impl RoadNetwork {
636    pub fn new(road_cost_slope: f32) -> Self { Self { road_cost_slope } }
637
638    /// Find a road path from `(ax,ay)` to `(bx,by)` using A*.
639    /// Returns list of positions or empty if no path found.
640    pub fn find_path(&self, map: &WorldMap, ax: usize, ay: usize, bx: usize, by: usize) -> Vec<(usize, usize)> {
641        let w = map.width;
642        let h = map.height;
643        let start = ay * w + ax;
644        let goal  = by * w + bx;
645
646        let mut dist = vec![f32::INFINITY; w * h];
647        let mut prev = vec![usize::MAX; w * h];
648        dist[start] = 0.0;
649
650        let mut heap: BinaryHeap<AStarNode> = BinaryHeap::new();
651        heap.push(AStarNode { cost: 0.0, x: ax, y: ay });
652
653        while let Some(AStarNode { cost, x, y }) = heap.pop() {
654            let idx = y * w + x;
655            if idx == goal {
656                // Reconstruct
657                let mut path = Vec::new();
658                let mut cur = goal;
659                while cur != usize::MAX {
660                    path.push((cur % w, cur / w));
661                    cur = prev[cur];
662                }
663                path.reverse();
664                return path;
665            }
666            if cost > dist[idx] { continue; }
667
668            for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
669                let nx = x as i32 + dx;
670                let ny = y as i32 + dy;
671                if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= h { continue; }
672                let (nx, ny) = (nx as usize, ny as usize);
673                let ni = ny * w + nx;
674                let elev_change = (map.get(nx, ny).elevation - map.get(x, y).elevation).abs();
675                let move_cost = 1.0 + elev_change * self.road_cost_slope;
676                let new_cost  = dist[idx] + move_cost;
677                if new_cost < dist[ni] {
678                    dist[ni] = new_cost;
679                    prev[ni] = idx;
680                    // A* heuristic: Euclidean distance
681                    let hdx = nx as f32 - bx as f32;
682                    let hdy = ny as f32 - by as f32;
683                    let h_val = (hdx * hdx + hdy * hdy).sqrt();
684                    heap.push(AStarNode { cost: new_cost + h_val, x: nx, y: ny });
685                }
686            }
687        }
688        Vec::new() // no path
689    }
690
691    /// Shortest trade route between two settlements (by name lookup).
692    pub fn shortest_trade_route(
693        &self,
694        map: &WorldMap,
695        settlements: &[Settlement],
696        a_id: u32,
697        b_id: u32,
698    ) -> Vec<(usize, usize)> {
699        let a = settlements.iter().find(|s| s.id == a_id);
700        let b = settlements.iter().find(|s| s.id == b_id);
701        match (a, b) {
702            (Some(sa), Some(sb)) => {
703                let (ax, ay) = sa.position;
704                let (bx, by) = sb.position;
705                self.find_path(map, ax, ay, bx, by)
706            }
707            _ => Vec::new(),
708        }
709    }
710
711    /// Connect all settlements with roads; mark road cells on the map.
712    pub fn build_roads(&self, map: &mut WorldMap, settlements: &[Settlement], rng: &mut Rng) {
713        if settlements.len() < 2 { return; }
714        // Connect each settlement to its nearest neighbour (MST-like)
715        let n = settlements.len();
716        let mut connected = vec![false; n];
717        connected[0] = true;
718        let mut road_id = 1u32;
719
720        for _ in 1..n {
721            let mut best_cost = f32::INFINITY;
722            let mut best_pair = (0, 1);
723            for i in 0..n {
724                if !connected[i] { continue; }
725                for j in 0..n {
726                    if connected[j] { continue; }
727                    let (ax, ay) = settlements[i].position;
728                    let (bx, by) = settlements[j].position;
729                    let dx = ax as f32 - bx as f32;
730                    let dy = ay as f32 - by as f32;
731                    let d = (dx * dx + dy * dy).sqrt();
732                    if d < best_cost { best_cost = d; best_pair = (i, j); }
733                }
734            }
735            let (i, j) = best_pair;
736            connected[j] = true;
737            let (ax, ay) = settlements[i].position;
738            let (bx, by) = settlements[j].position;
739            let path = self.find_path(map, ax, ay, bx, by);
740            for (px, py) in path {
741                map.get_mut(px, py).road_id = Some(road_id);
742            }
743            road_id += 1;
744        }
745        // Suppress unused warning
746        let _ = rng.next_u64();
747    }
748}
749
750// ── NameGenerator (world cultures) ───────────────────────────────────────────
751
752/// Culture for name generation.
753#[derive(Debug, Clone, Copy, PartialEq, Eq)]
754pub enum Culture {
755    Norse,
756    Arabic,
757    Japanese,
758    Latin,
759    Fantasy,
760}
761
762/// Markov-chain (order 2) name generator trained on culture syllable lists.
763pub struct NameGenerator {
764    culture: Culture,
765}
766
767impl NameGenerator {
768    pub fn new(culture: Culture) -> Self { Self { culture } }
769
770    /// Generate a name for the given culture and seed.
771    pub fn generate(&self, rng: &mut Rng) -> String {
772        let (pre, mid, suf) = self.syllables();
773        let p = *rng.pick(pre).unwrap_or(&"Ka");
774        let s = *rng.pick(suf).unwrap_or(&"ar");
775        if rng.chance(0.55) || mid.is_empty() {
776            capitalize_first(&format!("{p}{s}"))
777        } else {
778            let m = *rng.pick(mid).unwrap_or(&"an");
779            capitalize_first(&format!("{p}{m}{s}"))
780        }
781    }
782
783    pub fn generate_with_seed(&self, seed: u64) -> String {
784        let mut rng = Rng::new(seed);
785        self.generate(&mut rng)
786    }
787
788    fn syllables(&self) -> (&[&'static str], &[&'static str], &[&'static str]) {
789        match self.culture {
790            Culture::Norse    => (NORSE_PRE, NORSE_MID, NORSE_SUF),
791            Culture::Arabic   => (ARABIC_PRE, ARABIC_MID, ARABIC_SUF),
792            Culture::Japanese => (JAPANESE_PRE, JAPANESE_MID, JAPANESE_SUF),
793            Culture::Latin    => (LATIN_PRE, LATIN_MID, LATIN_SUF),
794            Culture::Fantasy  => (FANTASY_PRE, FANTASY_MID, FANTASY_SUF),
795        }
796    }
797}
798
799fn capitalize_first(s: &str) -> String {
800    let mut c = s.chars();
801    match c.next() {
802        None    => String::new(),
803        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
804    }
805}
806
807const NORSE_PRE:    &[&str] = &["Thor","Bjorn","Sigurd","Ulf","Ragnar","Gunnar","Erik","Leif","Ivar","Sven","Haldor","Vidar"];
808const NORSE_MID:    &[&str] = &["gar","mund","ald","helm","ulf","ric","win","frid","run","ing","rod","bald"];
809const NORSE_SUF:    &[&str] = &["son","sen","sson","dottir","ir","ar","heim","borg","fjord","dal","vik","stad"];
810
811const ARABIC_PRE:   &[&str] = &["Al","Abd","Khalid","Omar","Yusuf","Ahmad","Hamid","Tariq","Walid","Faisal","Jamil","Nabil"];
812const ARABIC_MID:   &[&str] = &["al","ibn","bin","ab","um","al","din","ud","ur","ul","im","is"];
813const ARABIC_SUF:   &[&str] = &["ah","i","an","un","in","oon","een","at","iya","iyya","awi","awi"];
814
815const JAPANESE_PRE: &[&str] = &["Hiro","Taka","Yoshi","Masa","Nori","Tat","Kei","Shin","Haru","Aki","Tomo","Kazu"];
816const JAPANESE_MID: &[&str] = &["no","na","yo","ka","ta","ma","mi","mu","ki","ku","ro","to"];
817const JAPANESE_SUF: &[&str] = &["shi","ko","ro","ki","mi","ka","to","ru","no","ta","su","ya"];
818
819const LATIN_PRE:    &[&str] = &["Marc","Gaius","Lucius","Publius","Quintus","Titus","Sextus","Aulus","Gnaeus","Decimus","Marcus","Julius"];
820const LATIN_MID:    &[&str] = &["aes","ius","ell","inn","iss","ull","orr","err","uss","elius","anus","atus"];
821const LATIN_SUF:    &[&str] = &["us","um","ia","ius","ae","ix","ax","ius","anus","inus","ulus","atus"];
822
823const FANTASY_PRE:  &[&str] = &["Aer","Zyl","Vael","Xan","Myr","Thal","Aen","Ith","Sel","Nox","Kyr","Dra"];
824const FANTASY_MID:  &[&str] = &["an","ael","iel","ion","ias","eld","ath","ill","orn","ith","eth","ash"];
825const FANTASY_SUF:  &[&str] = &["iel","ias","ion","ath","ael","ial","uen","ean","ian","iel","ath","orn"];
826
827// ── WorldHistory ──────────────────────────────────────────────────────────────
828
829/// Kind of historical event.
830#[derive(Debug, Clone, PartialEq, Eq)]
831pub enum EventKind {
832    War,
833    Plague,
834    NaturalDisaster,
835    FoundedSettlement,
836    RulerChanged,
837    Discovery,
838    Trade,
839    Rebellion,
840}
841
842/// A single historical event.
843#[derive(Debug, Clone)]
844pub struct HistoryEvent {
845    pub year:                 i32,
846    pub kind:                 EventKind,
847    pub affected_settlements: Vec<u32>,
848    pub description:          String,
849}
850
851/// Generates world history as a sequence of events.
852pub struct WorldHistory {
853    pub events: Vec<HistoryEvent>,
854}
855
856impl WorldHistory {
857    pub fn new() -> Self { Self { events: Vec::new() } }
858
859    /// Generate `years` worth of history for the given world.
860    pub fn generate(
861        &mut self,
862        settlements: &[Settlement],
863        years: u32,
864        rng: &mut Rng,
865    ) {
866        let mut year = 1i32;
867        let end_year  = year + years as i32;
868        let n_settle  = settlements.len();
869
870        while year < end_year {
871            let events_this_year = rng.range_usize(3);
872            for _ in 0..=events_this_year {
873                if year >= end_year { break; }
874                let kind_roll = rng.range_usize(8);
875                let kind = match kind_roll {
876                    0 => EventKind::War,
877                    1 => EventKind::Plague,
878                    2 => EventKind::NaturalDisaster,
879                    3 => EventKind::FoundedSettlement,
880                    4 => EventKind::RulerChanged,
881                    5 => EventKind::Discovery,
882                    6 => EventKind::Trade,
883                    _ => EventKind::Rebellion,
884                };
885
886                let n_affected = if n_settle == 0 { 0 } else {
887                    rng.range_usize(n_settle.min(3)) + 1
888                };
889                let affected: Vec<u32> = if n_settle > 0 {
890                    let mut ids: Vec<u32> = settlements.iter().map(|s| s.id).collect();
891                    rng.shuffle(&mut ids);
892                    ids.into_iter().take(n_affected).collect()
893                } else {
894                    Vec::new()
895                };
896
897                let description = Self::describe(&kind, &affected, settlements, year, rng);
898                self.events.push(HistoryEvent { year, kind, affected_settlements: affected, description });
899            }
900            // Advance time by 1–10 years
901            year += rng.range_i32(1, 10);
902        }
903    }
904
905    fn describe(
906        kind: &EventKind,
907        affected: &[u32],
908        settlements: &[Settlement],
909        year: i32,
910        rng: &mut Rng,
911    ) -> String {
912        let settle_name = |id: u32| -> &str {
913            settlements.iter().find(|s| s.id == id).map(|s| s.name.as_str()).unwrap_or("Unknown")
914        };
915
916        let first = affected.first().copied().unwrap_or(0);
917        let second = affected.get(1).copied();
918
919        match kind {
920            EventKind::War => {
921                let a = settle_name(first);
922                let b = second.map(settle_name).unwrap_or("foreign powers");
923                format!("Year {year}: War erupted between {a} and {b}.")
924            }
925            EventKind::Plague => {
926                let a = settle_name(first);
927                format!("Year {year}: A plague ravaged {a}, killing thousands.")
928            }
929            EventKind::NaturalDisaster => {
930                let disasters = &["earthquake","flood","volcanic eruption","drought","wildfire"];
931                let d = rng.pick(disasters).copied().unwrap_or("storm");
932                let a = settle_name(first);
933                format!("Year {year}: A great {d} struck near {a}.")
934            }
935            EventKind::FoundedSettlement => {
936                let a = settle_name(first);
937                format!("Year {year}: The settlement of {a} was founded.")
938            }
939            EventKind::RulerChanged => {
940                let a = settle_name(first);
941                format!("Year {year}: A new ruler came to power in {a}.")
942            }
943            EventKind::Discovery => {
944                let things = &["ancient ruins","a new trade route","a rich vein of ore","a sacred spring","a lost tome"];
945                let thing = rng.pick(things).copied().unwrap_or("something remarkable");
946                let a = settle_name(first);
947                format!("Year {year}: Explorers from {a} discovered {thing}.")
948            }
949            EventKind::Trade => {
950                let a = settle_name(first);
951                let b = second.map(settle_name).unwrap_or("distant lands");
952                format!("Year {year}: A prosperous trade agreement was forged between {a} and {b}.")
953            }
954            EventKind::Rebellion => {
955                let a = settle_name(first);
956                format!("Year {year}: The people of {a} rose in rebellion against their rulers.")
957            }
958        }
959    }
960}
961
962impl Default for WorldHistory {
963    fn default() -> Self { Self::new() }
964}
965
966// ── WorldBuilder ──────────────────────────────────────────────────────────────
967
968/// Convenience builder that orchestrates all world-generation steps.
969pub struct WorldBuilder {
970    pub width:  usize,
971    pub height: usize,
972    pub seed:   u64,
973}
974
975impl WorldBuilder {
976    pub fn new(width: usize, height: usize, seed: u64) -> Self {
977        Self { width, height, seed }
978    }
979
980    /// Generate a complete world. Returns (map, settlements, history).
981    pub fn build(&self) -> (WorldMap, Vec<Settlement>, WorldHistory) {
982        let mut rng = Rng::new(self.seed);
983        let mut map = WorldMap::new(self.width, self.height);
984
985        // Terrain
986        let heightmap = HeightmapWorld::default();
987        heightmap.apply(&mut map, self.seed);
988
989        // Climate
990        let climate = ClimateSimulator::default();
991        climate.apply(&mut map);
992
993        // Rivers
994        let rivers = RiverSystem::default();
995        rivers.apply(&mut map, &mut rng);
996
997        // Classify biomes
998        let classifier = BiomeClassifier::new();
999        for y in 0..self.height {
1000            for x in 0..self.width {
1001                let (e, t, m) = {
1002                    let c = map.get(x, y);
1003                    (c.elevation, c.temperature, c.moisture)
1004                };
1005                map.get_mut(x, y).biome = classifier.classify(t, m, e);
1006            }
1007        }
1008
1009        // Settlements
1010        let culture = [Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy];
1011        let c = culture[rng.range_usize(5)];
1012        let mut name_gen = NameGenerator::new(c);
1013        let mut placer = SettlementPlacer::default();
1014        placer.num_settlements = 20;
1015        let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1016
1017        // Roads
1018        let roads = RoadNetwork::default();
1019        roads.build_roads(&mut map, &settlements, &mut rng);
1020
1021        // History
1022        let mut history = WorldHistory::new();
1023        history.generate(&settlements, 500, &mut rng);
1024
1025        (map, settlements, history)
1026    }
1027}
1028
1029// ── Tests ─────────────────────────────────────────────────────────────────────
1030
1031#[cfg(test)]
1032mod tests {
1033    use super::*;
1034
1035    fn small_map() -> WorldMap {
1036        let mut map = WorldMap::new(64, 48);
1037        let hm = HeightmapWorld::default();
1038        hm.apply(&mut map, 42);
1039        map
1040    }
1041
1042    #[test]
1043    fn heightmap_has_land_and_ocean() {
1044        let map = small_map();
1045        let has_land  = map.cells.iter().any(|c| c.elevation > 0.0);
1046        let has_ocean = map.cells.iter().any(|c| c.elevation < 0.0);
1047        assert!(has_land,  "expected some land cells");
1048        assert!(has_ocean, "expected some ocean cells");
1049    }
1050
1051    #[test]
1052    fn climate_assigns_temperature() {
1053        let mut map = small_map();
1054        let climate = ClimateSimulator::default();
1055        climate.apply(&mut map);
1056        let any_nonzero = map.cells.iter().any(|c| c.temperature != 0.0);
1057        assert!(any_nonzero);
1058    }
1059
1060    #[test]
1061    fn climate_assigns_moisture() {
1062        let mut map = small_map();
1063        let climate = ClimateSimulator::default();
1064        climate.apply(&mut map);
1065        let any_moisture = map.cells.iter().any(|c| c.moisture > 0.0);
1066        assert!(any_moisture);
1067    }
1068
1069    #[test]
1070    fn rivers_carve_some_cells() {
1071        let mut map = small_map();
1072        let climate = ClimateSimulator::default();
1073        climate.apply(&mut map);
1074        let mut rng = Rng::new(99);
1075        let rivers = RiverSystem { num_rivers: 3, min_source_elev: 0.4, erosion_amount: 0.01 };
1076        rivers.apply(&mut map, &mut rng);
1077        let river_cells = map.cells.iter().filter(|c| c.river_id.is_some()).count();
1078        assert!(river_cells > 0, "expected river cells");
1079    }
1080
1081    #[test]
1082    fn biome_classifier_covers_all_ids() {
1083        let clf = BiomeClassifier::new();
1084        // Sample the classifier at various points
1085        let biomes = [
1086            clf.classify(-0.8,  0.1, 0.5),
1087            clf.classify( 0.8,  0.05, 0.3),
1088            clf.classify( 0.2,  0.6, 0.3),
1089            clf.classify(-0.5,  0.4, 0.4),
1090            clf.classify( 0.6,  0.7, 0.3),
1091        ];
1092        assert!(biomes.iter().any(|b| *b != BiomeId::OCEAN));
1093    }
1094
1095    #[test]
1096    fn settlement_placer_places_settlements() {
1097        let mut rng = Rng::new(7);
1098        let mut map = small_map();
1099        let climate = ClimateSimulator::default();
1100        climate.apply(&mut map);
1101        let mut name_gen = NameGenerator::new(Culture::Fantasy);
1102        let mut placer = SettlementPlacer::default();
1103        placer.num_settlements = 5;
1104        let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1105        assert!(!settlements.is_empty(), "should place at least one settlement");
1106    }
1107
1108    #[test]
1109    fn settlement_names_non_empty() {
1110        let mut rng = Rng::new(1234);
1111        let gen = NameGenerator::new(Culture::Norse);
1112        for _ in 0..5 {
1113            let name = gen.generate(&mut rng);
1114            assert!(!name.is_empty(), "name should not be empty");
1115        }
1116    }
1117
1118    #[test]
1119    fn road_network_finds_path() {
1120        let map = small_map();
1121        let roads = RoadNetwork::default();
1122        let path = roads.find_path(&map, 5, 5, 20, 20);
1123        assert!(!path.is_empty(), "A* should find a path");
1124    }
1125
1126    #[test]
1127    fn world_history_generates_events() {
1128        let mut rng = Rng::new(42);
1129        let settlements = vec![
1130            Settlement::new(1, 10, 10, SettlementKind::Town,    "Testville".into()),
1131            Settlement::new(2, 30, 30, SettlementKind::City,    "Cityburg".into()),
1132            Settlement::new(3, 50, 10, SettlementKind::Village, "Hamlet".into()),
1133        ];
1134        let mut history = WorldHistory::new();
1135        history.generate(&settlements, 100, &mut rng);
1136        assert!(!history.events.is_empty(), "should generate events");
1137    }
1138
1139    #[test]
1140    fn world_history_event_years_monotone() {
1141        let mut rng = Rng::new(55);
1142        let settlements = vec![Settlement::new(1, 5, 5, SettlementKind::Village, "A".into())];
1143        let mut history = WorldHistory::new();
1144        history.generate(&settlements, 200, &mut rng);
1145        let years: Vec<i32> = history.events.iter().map(|e| e.year).collect();
1146        let sorted = years.windows(2).all(|w| w[0] <= w[1]);
1147        assert!(sorted, "events should be in chronological order");
1148    }
1149
1150    #[test]
1151    fn world_builder_full_pipeline() {
1152        let builder = WorldBuilder::new(32, 24, 42);
1153        let (map, settlements, history) = builder.build();
1154        assert_eq!(map.width, 32);
1155        assert_eq!(map.height, 24);
1156        assert!(!history.events.is_empty());
1157    }
1158
1159    #[test]
1160    fn name_generator_all_cultures() {
1161        let mut rng = Rng::new(7);
1162        for culture in &[Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy] {
1163            let gen = NameGenerator::new(*culture);
1164            let name = gen.generate(&mut rng);
1165            assert!(!name.is_empty(), "Culture {:?} produced empty name", culture);
1166        }
1167    }
1168
1169    #[test]
1170    fn biome_params_all_twelve() {
1171        let params = BiomeParams::all();
1172        assert_eq!(params.len(), 12);
1173    }
1174
1175    #[test]
1176    fn world_map_idx_roundtrip() {
1177        let map = WorldMap::new(50, 40);
1178        for y in 0..40 {
1179            for x in 0..50 {
1180                let i = map.idx(x, y);
1181                assert_eq!(i % 50, x);
1182                assert_eq!(i / 50, y);
1183            }
1184        }
1185    }
1186}