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            // `cost` is f = g + h; compare its g part with the best known g.
667            // Comparing f with g (as this did) skipped every node but the
668            // start, so no path was ever found.
669            let h_here = ((x as f32 - bx as f32).powi(2) + (y as f32 - by as f32).powi(2)).sqrt();
670            if cost - h_here > dist[idx] + 1e-4 { continue; }
671
672            for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
673                let nx = x as i32 + dx;
674                let ny = y as i32 + dy;
675                if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= h { continue; }
676                let (nx, ny) = (nx as usize, ny as usize);
677                let ni = ny * w + nx;
678                let elev_change = (map.get(nx, ny).elevation - map.get(x, y).elevation).abs();
679                let move_cost = 1.0 + elev_change * self.road_cost_slope;
680                let new_cost  = dist[idx] + move_cost;
681                if new_cost < dist[ni] {
682                    dist[ni] = new_cost;
683                    prev[ni] = idx;
684                    // A* heuristic: Euclidean distance
685                    let hdx = nx as f32 - bx as f32;
686                    let hdy = ny as f32 - by as f32;
687                    let h_val = (hdx * hdx + hdy * hdy).sqrt();
688                    heap.push(AStarNode { cost: new_cost + h_val, x: nx, y: ny });
689                }
690            }
691        }
692        Vec::new() // no path
693    }
694
695    /// Shortest trade route between two settlements (by name lookup).
696    pub fn shortest_trade_route(
697        &self,
698        map: &WorldMap,
699        settlements: &[Settlement],
700        a_id: u32,
701        b_id: u32,
702    ) -> Vec<(usize, usize)> {
703        let a = settlements.iter().find(|s| s.id == a_id);
704        let b = settlements.iter().find(|s| s.id == b_id);
705        match (a, b) {
706            (Some(sa), Some(sb)) => {
707                let (ax, ay) = sa.position;
708                let (bx, by) = sb.position;
709                self.find_path(map, ax, ay, bx, by)
710            }
711            _ => Vec::new(),
712        }
713    }
714
715    /// Connect all settlements with roads; mark road cells on the map.
716    pub fn build_roads(&self, map: &mut WorldMap, settlements: &[Settlement], rng: &mut Rng) {
717        if settlements.len() < 2 { return; }
718        // Connect each settlement to its nearest neighbour (MST-like)
719        let n = settlements.len();
720        let mut connected = vec![false; n];
721        connected[0] = true;
722        let mut road_id = 1u32;
723
724        for _ in 1..n {
725            let mut best_cost = f32::INFINITY;
726            let mut best_pair = (0, 1);
727            for i in 0..n {
728                if !connected[i] { continue; }
729                for j in 0..n {
730                    if connected[j] { continue; }
731                    let (ax, ay) = settlements[i].position;
732                    let (bx, by) = settlements[j].position;
733                    let dx = ax as f32 - bx as f32;
734                    let dy = ay as f32 - by as f32;
735                    let d = (dx * dx + dy * dy).sqrt();
736                    if d < best_cost { best_cost = d; best_pair = (i, j); }
737                }
738            }
739            let (i, j) = best_pair;
740            connected[j] = true;
741            let (ax, ay) = settlements[i].position;
742            let (bx, by) = settlements[j].position;
743            let path = self.find_path(map, ax, ay, bx, by);
744            for (px, py) in path {
745                map.get_mut(px, py).road_id = Some(road_id);
746            }
747            road_id += 1;
748        }
749        // Suppress unused warning
750        let _ = rng.next_u64();
751    }
752}
753
754// ── NameGenerator (world cultures) ───────────────────────────────────────────
755
756/// Culture for name generation.
757#[derive(Debug, Clone, Copy, PartialEq, Eq)]
758pub enum Culture {
759    Norse,
760    Arabic,
761    Japanese,
762    Latin,
763    Fantasy,
764}
765
766/// Markov-chain (order 2) name generator trained on culture syllable lists.
767pub struct NameGenerator {
768    culture: Culture,
769}
770
771impl NameGenerator {
772    pub fn new(culture: Culture) -> Self { Self { culture } }
773
774    /// Generate a name for the given culture and seed.
775    pub fn generate(&self, rng: &mut Rng) -> String {
776        let (pre, mid, suf) = self.syllables();
777        let p = *rng.pick(pre).unwrap_or(&"Ka");
778        let s = *rng.pick(suf).unwrap_or(&"ar");
779        if rng.chance(0.55) || mid.is_empty() {
780            capitalize_first(&format!("{p}{s}"))
781        } else {
782            let m = *rng.pick(mid).unwrap_or(&"an");
783            capitalize_first(&format!("{p}{m}{s}"))
784        }
785    }
786
787    pub fn generate_with_seed(&self, seed: u64) -> String {
788        let mut rng = Rng::new(seed);
789        self.generate(&mut rng)
790    }
791
792    fn syllables(&self) -> (&[&'static str], &[&'static str], &[&'static str]) {
793        match self.culture {
794            Culture::Norse    => (NORSE_PRE, NORSE_MID, NORSE_SUF),
795            Culture::Arabic   => (ARABIC_PRE, ARABIC_MID, ARABIC_SUF),
796            Culture::Japanese => (JAPANESE_PRE, JAPANESE_MID, JAPANESE_SUF),
797            Culture::Latin    => (LATIN_PRE, LATIN_MID, LATIN_SUF),
798            Culture::Fantasy  => (FANTASY_PRE, FANTASY_MID, FANTASY_SUF),
799        }
800    }
801}
802
803fn capitalize_first(s: &str) -> String {
804    let mut c = s.chars();
805    match c.next() {
806        None    => String::new(),
807        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
808    }
809}
810
811const NORSE_PRE:    &[&str] = &["Thor","Bjorn","Sigurd","Ulf","Ragnar","Gunnar","Erik","Leif","Ivar","Sven","Haldor","Vidar"];
812const NORSE_MID:    &[&str] = &["gar","mund","ald","helm","ulf","ric","win","frid","run","ing","rod","bald"];
813const NORSE_SUF:    &[&str] = &["son","sen","sson","dottir","ir","ar","heim","borg","fjord","dal","vik","stad"];
814
815const ARABIC_PRE:   &[&str] = &["Al","Abd","Khalid","Omar","Yusuf","Ahmad","Hamid","Tariq","Walid","Faisal","Jamil","Nabil"];
816const ARABIC_MID:   &[&str] = &["al","ibn","bin","ab","um","al","din","ud","ur","ul","im","is"];
817const ARABIC_SUF:   &[&str] = &["ah","i","an","un","in","oon","een","at","iya","iyya","awi","awi"];
818
819const JAPANESE_PRE: &[&str] = &["Hiro","Taka","Yoshi","Masa","Nori","Tat","Kei","Shin","Haru","Aki","Tomo","Kazu"];
820const JAPANESE_MID: &[&str] = &["no","na","yo","ka","ta","ma","mi","mu","ki","ku","ro","to"];
821const JAPANESE_SUF: &[&str] = &["shi","ko","ro","ki","mi","ka","to","ru","no","ta","su","ya"];
822
823const LATIN_PRE:    &[&str] = &["Marc","Gaius","Lucius","Publius","Quintus","Titus","Sextus","Aulus","Gnaeus","Decimus","Marcus","Julius"];
824const LATIN_MID:    &[&str] = &["aes","ius","ell","inn","iss","ull","orr","err","uss","elius","anus","atus"];
825const LATIN_SUF:    &[&str] = &["us","um","ia","ius","ae","ix","ax","ius","anus","inus","ulus","atus"];
826
827const FANTASY_PRE:  &[&str] = &["Aer","Zyl","Vael","Xan","Myr","Thal","Aen","Ith","Sel","Nox","Kyr","Dra"];
828const FANTASY_MID:  &[&str] = &["an","ael","iel","ion","ias","eld","ath","ill","orn","ith","eth","ash"];
829const FANTASY_SUF:  &[&str] = &["iel","ias","ion","ath","ael","ial","uen","ean","ian","iel","ath","orn"];
830
831// ── WorldHistory ──────────────────────────────────────────────────────────────
832
833/// Kind of historical event.
834#[derive(Debug, Clone, PartialEq, Eq)]
835pub enum EventKind {
836    War,
837    Plague,
838    NaturalDisaster,
839    FoundedSettlement,
840    RulerChanged,
841    Discovery,
842    Trade,
843    Rebellion,
844}
845
846/// A single historical event.
847#[derive(Debug, Clone)]
848pub struct HistoryEvent {
849    pub year:                 i32,
850    pub kind:                 EventKind,
851    pub affected_settlements: Vec<u32>,
852    pub description:          String,
853}
854
855/// Generates world history as a sequence of events.
856pub struct WorldHistory {
857    pub events: Vec<HistoryEvent>,
858}
859
860impl WorldHistory {
861    pub fn new() -> Self { Self { events: Vec::new() } }
862
863    /// Generate `years` worth of history for the given world.
864    pub fn generate(
865        &mut self,
866        settlements: &[Settlement],
867        years: u32,
868        rng: &mut Rng,
869    ) {
870        let mut year = 1i32;
871        let end_year  = year + years as i32;
872        let n_settle  = settlements.len();
873
874        while year < end_year {
875            let events_this_year = rng.range_usize(3);
876            for _ in 0..=events_this_year {
877                if year >= end_year { break; }
878                let kind_roll = rng.range_usize(8);
879                let kind = match kind_roll {
880                    0 => EventKind::War,
881                    1 => EventKind::Plague,
882                    2 => EventKind::NaturalDisaster,
883                    3 => EventKind::FoundedSettlement,
884                    4 => EventKind::RulerChanged,
885                    5 => EventKind::Discovery,
886                    6 => EventKind::Trade,
887                    _ => EventKind::Rebellion,
888                };
889
890                let n_affected = if n_settle == 0 { 0 } else {
891                    rng.range_usize(n_settle.min(3)) + 1
892                };
893                let affected: Vec<u32> = if n_settle > 0 {
894                    let mut ids: Vec<u32> = settlements.iter().map(|s| s.id).collect();
895                    rng.shuffle(&mut ids);
896                    ids.into_iter().take(n_affected).collect()
897                } else {
898                    Vec::new()
899                };
900
901                let description = Self::describe(&kind, &affected, settlements, year, rng);
902                self.events.push(HistoryEvent { year, kind, affected_settlements: affected, description });
903            }
904            // Advance time by 1–10 years
905            year += rng.range_i32(1, 10);
906        }
907    }
908
909    fn describe(
910        kind: &EventKind,
911        affected: &[u32],
912        settlements: &[Settlement],
913        year: i32,
914        rng: &mut Rng,
915    ) -> String {
916        let settle_name = |id: u32| -> &str {
917            settlements.iter().find(|s| s.id == id).map(|s| s.name.as_str()).unwrap_or("Unknown")
918        };
919
920        let first = affected.first().copied().unwrap_or(0);
921        let second = affected.get(1).copied();
922
923        match kind {
924            EventKind::War => {
925                let a = settle_name(first);
926                let b = second.map(settle_name).unwrap_or("foreign powers");
927                format!("Year {year}: War erupted between {a} and {b}.")
928            }
929            EventKind::Plague => {
930                let a = settle_name(first);
931                format!("Year {year}: A plague ravaged {a}, killing thousands.")
932            }
933            EventKind::NaturalDisaster => {
934                let disasters = &["earthquake","flood","volcanic eruption","drought","wildfire"];
935                let d = rng.pick(disasters).copied().unwrap_or("storm");
936                let a = settle_name(first);
937                format!("Year {year}: A great {d} struck near {a}.")
938            }
939            EventKind::FoundedSettlement => {
940                let a = settle_name(first);
941                format!("Year {year}: The settlement of {a} was founded.")
942            }
943            EventKind::RulerChanged => {
944                let a = settle_name(first);
945                format!("Year {year}: A new ruler came to power in {a}.")
946            }
947            EventKind::Discovery => {
948                let things = &["ancient ruins","a new trade route","a rich vein of ore","a sacred spring","a lost tome"];
949                let thing = rng.pick(things).copied().unwrap_or("something remarkable");
950                let a = settle_name(first);
951                format!("Year {year}: Explorers from {a} discovered {thing}.")
952            }
953            EventKind::Trade => {
954                let a = settle_name(first);
955                let b = second.map(settle_name).unwrap_or("distant lands");
956                format!("Year {year}: A prosperous trade agreement was forged between {a} and {b}.")
957            }
958            EventKind::Rebellion => {
959                let a = settle_name(first);
960                format!("Year {year}: The people of {a} rose in rebellion against their rulers.")
961            }
962        }
963    }
964}
965
966impl Default for WorldHistory {
967    fn default() -> Self { Self::new() }
968}
969
970// ── WorldBuilder ──────────────────────────────────────────────────────────────
971
972/// Convenience builder that orchestrates all world-generation steps.
973pub struct WorldBuilder {
974    pub width:  usize,
975    pub height: usize,
976    pub seed:   u64,
977}
978
979impl WorldBuilder {
980    pub fn new(width: usize, height: usize, seed: u64) -> Self {
981        Self { width, height, seed }
982    }
983
984    /// Generate a complete world. Returns (map, settlements, history).
985    pub fn build(&self) -> (WorldMap, Vec<Settlement>, WorldHistory) {
986        let mut rng = Rng::new(self.seed);
987        let mut map = WorldMap::new(self.width, self.height);
988
989        // Terrain
990        let heightmap = HeightmapWorld::default();
991        heightmap.apply(&mut map, self.seed);
992
993        // Climate
994        let climate = ClimateSimulator::default();
995        climate.apply(&mut map);
996
997        // Rivers
998        let rivers = RiverSystem::default();
999        rivers.apply(&mut map, &mut rng);
1000
1001        // Classify biomes
1002        let classifier = BiomeClassifier::new();
1003        for y in 0..self.height {
1004            for x in 0..self.width {
1005                let (e, t, m) = {
1006                    let c = map.get(x, y);
1007                    (c.elevation, c.temperature, c.moisture)
1008                };
1009                map.get_mut(x, y).biome = classifier.classify(t, m, e);
1010            }
1011        }
1012
1013        // Settlements
1014        let culture = [Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy];
1015        let c = culture[rng.range_usize(5)];
1016        let mut name_gen = NameGenerator::new(c);
1017        let mut placer = SettlementPlacer::default();
1018        placer.num_settlements = 20;
1019        let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1020
1021        // Roads
1022        let roads = RoadNetwork::default();
1023        roads.build_roads(&mut map, &settlements, &mut rng);
1024
1025        // History
1026        let mut history = WorldHistory::new();
1027        history.generate(&settlements, 500, &mut rng);
1028
1029        (map, settlements, history)
1030    }
1031}
1032
1033// ── Tests ─────────────────────────────────────────────────────────────────────
1034
1035#[cfg(test)]
1036mod tests {
1037    use super::*;
1038
1039    fn small_map() -> WorldMap {
1040        let mut map = WorldMap::new(64, 48);
1041        let hm = HeightmapWorld::default();
1042        hm.apply(&mut map, 42);
1043        map
1044    }
1045
1046    #[test]
1047    fn heightmap_has_land_and_ocean() {
1048        let map = small_map();
1049        let has_land  = map.cells.iter().any(|c| c.elevation > 0.0);
1050        let has_ocean = map.cells.iter().any(|c| c.elevation < 0.0);
1051        assert!(has_land,  "expected some land cells");
1052        assert!(has_ocean, "expected some ocean cells");
1053    }
1054
1055    #[test]
1056    fn climate_assigns_temperature() {
1057        let mut map = small_map();
1058        let climate = ClimateSimulator::default();
1059        climate.apply(&mut map);
1060        let any_nonzero = map.cells.iter().any(|c| c.temperature != 0.0);
1061        assert!(any_nonzero);
1062    }
1063
1064    #[test]
1065    fn climate_assigns_moisture() {
1066        let mut map = small_map();
1067        let climate = ClimateSimulator::default();
1068        climate.apply(&mut map);
1069        let any_moisture = map.cells.iter().any(|c| c.moisture > 0.0);
1070        assert!(any_moisture);
1071    }
1072
1073    #[test]
1074    fn rivers_carve_some_cells() {
1075        let mut map = small_map();
1076        let climate = ClimateSimulator::default();
1077        climate.apply(&mut map);
1078        let mut rng = Rng::new(99);
1079        let rivers = RiverSystem { num_rivers: 3, min_source_elev: 0.4, erosion_amount: 0.01 };
1080        rivers.apply(&mut map, &mut rng);
1081        let river_cells = map.cells.iter().filter(|c| c.river_id.is_some()).count();
1082        assert!(river_cells > 0, "expected river cells");
1083    }
1084
1085    #[test]
1086    fn biome_classifier_covers_all_ids() {
1087        let clf = BiomeClassifier::new();
1088        // Sample the classifier at various points
1089        let biomes = [
1090            clf.classify(-0.8,  0.1, 0.5),
1091            clf.classify( 0.8,  0.05, 0.3),
1092            clf.classify( 0.2,  0.6, 0.3),
1093            clf.classify(-0.5,  0.4, 0.4),
1094            clf.classify( 0.6,  0.7, 0.3),
1095        ];
1096        assert!(biomes.iter().any(|b| *b != BiomeId::OCEAN));
1097    }
1098
1099    #[test]
1100    fn settlement_placer_places_settlements() {
1101        let mut rng = Rng::new(7);
1102        let mut map = small_map();
1103        let climate = ClimateSimulator::default();
1104        climate.apply(&mut map);
1105        let mut name_gen = NameGenerator::new(Culture::Fantasy);
1106        let mut placer = SettlementPlacer::default();
1107        placer.num_settlements = 5;
1108        let settlements = placer.place(&mut map, &mut name_gen, &mut rng);
1109        assert!(!settlements.is_empty(), "should place at least one settlement");
1110    }
1111
1112    #[test]
1113    fn settlement_names_non_empty() {
1114        let mut rng = Rng::new(1234);
1115        let gen = NameGenerator::new(Culture::Norse);
1116        for _ in 0..5 {
1117            let name = gen.generate(&mut rng);
1118            assert!(!name.is_empty(), "name should not be empty");
1119        }
1120    }
1121
1122    #[test]
1123    fn road_network_finds_path() {
1124        let map = small_map();
1125        let roads = RoadNetwork::default();
1126        let path = roads.find_path(&map, 5, 5, 20, 20);
1127        assert!(!path.is_empty(), "A* should find a path");
1128    }
1129
1130    #[test]
1131    fn world_history_generates_events() {
1132        let mut rng = Rng::new(42);
1133        let settlements = vec![
1134            Settlement::new(1, 10, 10, SettlementKind::Town,    "Testville".into()),
1135            Settlement::new(2, 30, 30, SettlementKind::City,    "Cityburg".into()),
1136            Settlement::new(3, 50, 10, SettlementKind::Village, "Hamlet".into()),
1137        ];
1138        let mut history = WorldHistory::new();
1139        history.generate(&settlements, 100, &mut rng);
1140        assert!(!history.events.is_empty(), "should generate events");
1141    }
1142
1143    #[test]
1144    fn world_history_event_years_monotone() {
1145        let mut rng = Rng::new(55);
1146        let settlements = vec![Settlement::new(1, 5, 5, SettlementKind::Village, "A".into())];
1147        let mut history = WorldHistory::new();
1148        history.generate(&settlements, 200, &mut rng);
1149        let years: Vec<i32> = history.events.iter().map(|e| e.year).collect();
1150        let sorted = years.windows(2).all(|w| w[0] <= w[1]);
1151        assert!(sorted, "events should be in chronological order");
1152    }
1153
1154    #[test]
1155    fn world_builder_full_pipeline() {
1156        let builder = WorldBuilder::new(32, 24, 42);
1157        let (map, settlements, history) = builder.build();
1158        assert_eq!(map.width, 32);
1159        assert_eq!(map.height, 24);
1160        assert!(!history.events.is_empty());
1161    }
1162
1163    #[test]
1164    fn name_generator_all_cultures() {
1165        let mut rng = Rng::new(7);
1166        for culture in &[Culture::Norse, Culture::Arabic, Culture::Japanese, Culture::Latin, Culture::Fantasy] {
1167            let gen = NameGenerator::new(*culture);
1168            let name = gen.generate(&mut rng);
1169            assert!(!name.is_empty(), "Culture {:?} produced empty name", culture);
1170        }
1171    }
1172
1173    #[test]
1174    fn biome_params_all_twelve() {
1175        let params = BiomeParams::all();
1176        assert_eq!(params.len(), 12);
1177    }
1178
1179    #[test]
1180    fn world_map_idx_roundtrip() {
1181        let map = WorldMap::new(50, 40);
1182        for y in 0..40 {
1183            for x in 0..50 {
1184                let i = map.idx(x, y);
1185                assert_eq!(i % 50, x);
1186                assert_eq!(i / 50, y);
1187            }
1188        }
1189    }
1190}