Skip to main content

proof_engine/procedural/
mod.rs

1//! Procedural content generation — dungeons, rooms, spawn tables, loot, names.
2//!
3//! This module drives the chaos-RPG world generation system. All content is
4//! generated from seed-based deterministic random functions so worlds are
5//! reproducible.
6//!
7//! ## Subsystems
8//! - `dungeon`   — BSP dungeon floor layout
9//! - `spawn`     — creature/item spawn tables with weighted probability
10//! - `names`     — procedural name generation (markov chains)
11//! - `loot`      — loot tables with rarity tiers
12//! - `encounter` — encounter difficulty scaling
13
14pub mod dungeon;
15pub mod spawn;
16pub mod names;
17pub mod loot;
18pub mod world;
19pub mod items;
20
21pub use dungeon::{DungeonFloor, Room, Corridor, DungeonTheme};
22pub use spawn::{SpawnTable, SpawnEntry, SpawnResult};
23pub use names::{NameGenerator, NameStyle};
24pub use loot::{LootTable, LootTier, LootDrop};
25
26// ── Seeded RNG ─────────────────────────────────────────────────────────────────
27
28/// Lightweight seeded pseudo-random number generator (xoshiro256**).
29/// Used throughout procedural generation for reproducibility.
30#[derive(Clone, Debug)]
31pub struct Rng {
32    state: [u64; 4],
33}
34
35impl Rng {
36    pub fn new(seed: u64) -> Self {
37        // Splitmix64 initialization to spread seed entropy
38        let mut s = seed;
39        let mut next = || {
40            s = s.wrapping_add(0x9e3779b97f4a7c15);
41            let mut z = s;
42            z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
43            z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb);
44            z ^ (z >> 31)
45        };
46        Self { state: [next(), next(), next(), next()] }
47    }
48
49    fn rol64(x: u64, k: u32) -> u64 {
50        (x << k) | (x >> (64 - k))
51    }
52
53    /// Next u64 (xoshiro256**).
54    pub fn next_u64(&mut self) -> u64 {
55        let result = Self::rol64(self.state[1].wrapping_mul(5), 7).wrapping_mul(9);
56        let t = self.state[1] << 17;
57        self.state[2] ^= self.state[0];
58        self.state[3] ^= self.state[1];
59        self.state[1] ^= self.state[2];
60        self.state[0] ^= self.state[3];
61        self.state[2] ^= t;
62        self.state[3] = Self::rol64(self.state[3], 45);
63        result
64    }
65
66    /// Next f32 in `[0, 1)`.
67    pub fn next_f32(&mut self) -> f32 {
68        (self.next_u64() >> 11) as f32 / (1u64 << 53) as f32
69    }
70
71    /// Next f32 in `[min, max)`.
72    pub fn range_f32(&mut self, min: f32, max: f32) -> f32 {
73        min + self.next_f32() * (max - min)
74    }
75
76    /// Next usize in `[0, n)`.
77    pub fn range_usize(&mut self, n: usize) -> usize {
78        (self.next_u64() % n as u64) as usize
79    }
80
81    /// Next i32 in `[min, max]`.
82    pub fn range_i32(&mut self, min: i32, max: i32) -> i32 {
83        min + (self.next_u64() % ((max - min + 1) as u64)) as i32
84    }
85
86    /// Bernoulli: true with probability `p ∈ [0, 1]`.
87    pub fn chance(&mut self, p: f32) -> bool {
88        self.next_f32() < p
89    }
90
91    /// Shuffle a slice in-place (Fisher-Yates).
92    pub fn shuffle<T>(&mut self, slice: &mut [T]) {
93        for i in (1..slice.len()).rev() {
94            let j = self.range_usize(i + 1);
95            slice.swap(i, j);
96        }
97    }
98
99    /// Pick a random element from a slice.
100    pub fn pick<'a, T>(&mut self, slice: &'a [T]) -> Option<&'a T> {
101        if slice.is_empty() { return None; }
102        Some(&slice[self.range_usize(slice.len())])
103    }
104
105    /// Pick a random element from a weighted list. Weights can be any positive f32.
106    pub fn pick_weighted<'a, T>(&mut self, items: &'a [(T, f32)]) -> Option<&'a T> {
107        let total: f32 = items.iter().map(|(_, w)| *w).sum();
108        if total <= 0.0 { return None; }
109        let mut r = self.next_f32() * total;
110        for (item, weight) in items {
111            r -= weight;
112            if r <= 0.0 { return Some(item); }
113        }
114        items.last().map(|(t, _)| t)
115    }
116
117    /// Gaussian sample with given mean and stddev (Box-Muller).
118    pub fn gaussian(&mut self, mean: f32, stddev: f32) -> f32 {
119        let u1 = self.next_f32().max(1e-10);
120        let u2 = self.next_f32();
121        let z  = (-2.0 * u1.ln()).sqrt() * (u2 * std::f32::consts::TAU).cos();
122        mean + z * stddev
123    }
124
125    /// Poisson-distributed random integer with given lambda.
126    pub fn poisson(&mut self, lambda: f32) -> u32 {
127        let l = (-lambda).exp();
128        let mut k = 0u32;
129        let mut p = 1.0_f32;
130        loop {
131            k += 1;
132            p *= self.next_f32();
133            if p <= l { break; }
134        }
135        k - 1
136    }
137
138    /// Fork: create a child RNG seeded from this one (deterministic).
139    pub fn fork(&mut self) -> Rng {
140        Rng::new(self.next_u64())
141    }
142}