Skip to main content

proof_engine/procedural/
dungeon.rs

1//! Dungeon generation — BSP, cellular automata, WFC, room placement, maze algorithms.
2//!
3//! This module provides multiple dungeon-generation strategies plus shared graph
4//! utilities (BFS shortest-path, Kruskal MST, connected-components).  All
5//! generators consume a `super::Rng` so results are fully deterministic from a
6//! seed.
7
8use super::Rng;
9use std::collections::{HashMap, HashSet, VecDeque};
10use glam::IVec2;
11
12// ── Theme ─────────────────────────────────────────────────────────────────────
13
14/// Visual theme for a dungeon floor.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum DungeonTheme {
17    Cave,
18    Cathedral,
19    Laboratory,
20    Temple,
21    Ruins,
22    Void,
23}
24
25impl DungeonTheme {
26    pub fn floor_glyphs(self) -> &'static [char] {
27        match self {
28            DungeonTheme::Cave       => &['.', ',', '\''],
29            DungeonTheme::Cathedral  => &['+', '.', '\u{00B7}'],
30            DungeonTheme::Laboratory => &['.', ':', '\u{00B7}'],
31            DungeonTheme::Temple     => &['\u{256C}', '\u{256A}', '\u{256B}', '.'],
32            DungeonTheme::Ruins      => &['.', ',', '~', '"'],
33            DungeonTheme::Void       => &['.', '\u{00B7}', '\u{2218}', '\u{00B0}'],
34        }
35    }
36
37    pub fn wall_glyphs(self) -> &'static [char] {
38        match self {
39            DungeonTheme::Cave       => &['#', '\u{2588}', '\u{2593}'],
40            DungeonTheme::Cathedral  => &['\u{2588}', '\u{2593}', '\u{2502}', '\u{2500}'],
41            DungeonTheme::Laboratory => &['\u{2588}', '\u{2593}', '\u{2554}', '\u{2557}'],
42            DungeonTheme::Temple     => &['\u{2588}', '\u{2593}', '\u{2560}', '\u{2563}'],
43            DungeonTheme::Ruins      => &['#', '\u{2593}', '%'],
44            DungeonTheme::Void       => &['\u{2593}', '\u{2591}', '\u{2592}'],
45        }
46    }
47
48    pub fn ambient_color(self) -> glam::Vec4 {
49        match self {
50            DungeonTheme::Cave       => glam::Vec4::new(0.4, 0.3, 0.2, 1.0),
51            DungeonTheme::Cathedral  => glam::Vec4::new(0.6, 0.5, 0.8, 1.0),
52            DungeonTheme::Laboratory => glam::Vec4::new(0.3, 0.8, 0.4, 1.0),
53            DungeonTheme::Temple     => glam::Vec4::new(0.8, 0.6, 0.2, 1.0),
54            DungeonTheme::Ruins      => glam::Vec4::new(0.5, 0.5, 0.4, 1.0),
55            DungeonTheme::Void       => glam::Vec4::new(0.1, 0.0, 0.2, 1.0),
56        }
57    }
58}
59
60// ── IRect ─────────────────────────────────────────────────────────────────────
61
62/// Axis-aligned integer rectangle in tile space.
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
64pub struct IRect {
65    pub x: i32,
66    pub y: i32,
67    pub w: i32,
68    pub h: i32,
69}
70
71impl IRect {
72    pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { Self { x, y, w, h } }
73
74    pub fn center(&self) -> IVec2 {
75        IVec2::new(self.x + self.w / 2, self.y + self.h / 2)
76    }
77
78    pub fn center_tuple(&self) -> (i32, i32) {
79        (self.x + self.w / 2, self.y + self.h / 2)
80    }
81
82    pub fn contains(&self, tx: i32, ty: i32) -> bool {
83        tx >= self.x && tx < self.x + self.w && ty >= self.y && ty < self.y + self.h
84    }
85
86    pub fn overlaps(&self, other: &IRect) -> bool {
87        self.x < other.x + other.w && self.x + self.w > other.x
88            && self.y < other.y + other.h && self.y + self.h > other.y
89    }
90
91    pub fn area(&self) -> i32 { self.w * self.h }
92
93    pub fn shrink(&self, margin: i32) -> Option<IRect> {
94        let nw = self.w - margin * 2;
95        let nh = self.h - margin * 2;
96        if nw < 1 || nh < 1 { return None; }
97        Some(IRect::new(self.x + margin, self.y + margin, nw, nh))
98    }
99
100    pub fn expand(&self, amount: i32) -> IRect {
101        IRect::new(self.x - amount, self.y - amount, self.w + amount * 2, self.h + amount * 2)
102    }
103}
104
105// ── Tile ──────────────────────────────────────────────────────────────────────
106
107/// Tile type in the dungeon grid.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum Tile {
110    Wall,
111    Floor,
112    Door,
113    Corridor,
114    Stairs,
115    Void,
116}
117
118impl Tile {
119    pub fn is_walkable(self) -> bool {
120        matches!(self, Tile::Floor | Tile::Door | Tile::Corridor | Tile::Stairs)
121    }
122
123    pub fn is_opaque(self) -> bool {
124        matches!(self, Tile::Wall | Tile::Void)
125    }
126}
127
128// ── RoomType ──────────────────────────────────────────────────────────────────
129
130/// Type/purpose of a room.
131#[derive(Debug, Clone, PartialEq)]
132pub enum RoomType {
133    Normal,
134    Start,
135    Entrance,
136    Exit,
137    Combat(f32),
138    Treasure,
139    Boss,
140    Shop,
141    Puzzle,
142    Rest,
143    Secret,
144    Shrine,
145    Trap,
146}
147
148// ── Room ──────────────────────────────────────────────────────────────────────
149
150/// A room in the dungeon with spatial bounds and graph connections.
151#[derive(Debug, Clone)]
152pub struct Room {
153    pub id:          usize,
154    pub rect:        IRect,
155    pub room_type:   RoomType,
156    pub connections: Vec<usize>,
157    pub tags:        Vec<String>,
158    pub spawns:      Vec<IVec2>,
159    pub visited:     bool,
160}
161
162impl Room {
163    pub fn new(id: usize, rect: IRect) -> Self {
164        Self {
165            id, rect, room_type: RoomType::Normal,
166            connections: Vec::new(), tags: Vec::new(), spawns: Vec::new(), visited: false,
167        }
168    }
169
170    pub fn center(&self) -> IVec2 { self.rect.center() }
171    pub fn bounds(&self) -> IRect { self.rect }
172
173    pub fn generate_spawns(&mut self, rng: &mut Rng, count: usize) {
174        let IRect { x, y, w, h } = self.rect;
175        self.spawns.clear();
176        for _ in 0..count {
177            let sx = rng.range_i32(x + 1, (x + w - 2).max(x + 1));
178            let sy = rng.range_i32(y + 1, (y + h - 2).max(y + 1));
179            self.spawns.push(IVec2::new(sx, sy));
180        }
181    }
182
183    pub fn add_tag(&mut self, tag: impl Into<String>) {
184        let t = tag.into();
185        if !self.tags.contains(&t) { self.tags.push(t); }
186    }
187
188    pub fn has_tag(&self, tag: &str) -> bool {
189        self.tags.iter().any(|t| t == tag)
190    }
191}
192
193// ── Corridor ──────────────────────────────────────────────────────────────────
194
195/// A corridor connecting two rooms.
196#[derive(Debug, Clone)]
197pub struct Corridor {
198    pub from:     usize,
199    pub to:       usize,
200    pub path:     Vec<IVec2>,
201    pub width:    u8,
202    pub has_door: bool,
203    pub bend:     IVec2,
204}
205
206impl Corridor {
207    pub fn new(from: usize, to: usize, from_pos: IVec2, to_pos: IVec2, rng: &mut Rng) -> Self {
208        let bend = if rng.chance(0.5) {
209            IVec2::new(to_pos.x, from_pos.y)
210        } else {
211            IVec2::new(from_pos.x, to_pos.y)
212        };
213        let path = Self::l_path(from_pos, to_pos, bend);
214        Self { from, to, path, width: 1, has_door: rng.chance(0.3), bend }
215    }
216
217    fn l_path(from: IVec2, to: IVec2, bend: IVec2) -> Vec<IVec2> {
218        let mut tiles = Vec::new();
219        let dx1 = (bend.x - from.x).signum();
220        let dy1 = (bend.y - from.y).signum();
221        let mut cur = from;
222        while cur != bend {
223            tiles.push(cur);
224            cur.x += dx1;
225            cur.y += dy1;
226        }
227        tiles.push(bend);
228        let dx2 = (to.x - bend.x).signum();
229        let dy2 = (to.y - bend.y).signum();
230        while cur != to {
231            cur.x += dx2;
232            cur.y += dy2;
233            tiles.push(cur);
234        }
235        tiles
236    }
237
238    pub fn tiles(&self) -> &[IVec2] { &self.path }
239}
240
241// ── DungeonGraph ──────────────────────────────────────────────────────────────
242
243/// A graph of rooms and corridors.
244#[derive(Debug, Clone, Default)]
245pub struct DungeonGraph {
246    pub rooms:     Vec<Room>,
247    pub corridors: Vec<Corridor>,
248    adj: Vec<Vec<(usize, usize)>>,
249}
250
251impl DungeonGraph {
252    pub fn new() -> Self { Self::default() }
253
254    pub fn add_room(&mut self, room: Room) -> usize {
255        let id = self.rooms.len();
256        self.rooms.push(room);
257        self.adj.push(Vec::new());
258        id
259    }
260
261    pub fn add_corridor(&mut self, corridor: Corridor) {
262        let ci = self.corridors.len();
263        let from = corridor.from;
264        let to   = corridor.to;
265        self.corridors.push(corridor);
266        if from < self.adj.len() { self.adj[from].push((to, ci)); }
267        if to   < self.adj.len() { self.adj[to].push((from, ci)); }
268        if from < self.rooms.len() { self.rooms[from].connections.push(to); }
269        if to   < self.rooms.len() { self.rooms[to].connections.push(from); }
270    }
271
272    pub fn connected_components(&self) -> usize {
273        if self.rooms.is_empty() { return 0; }
274        let n = self.rooms.len();
275        let mut parent: Vec<usize> = (0..n).collect();
276        fn find(parent: &mut Vec<usize>, x: usize) -> usize {
277            if parent[x] != x { parent[x] = find(parent, parent[x]); }
278            parent[x]
279        }
280        for c in &self.corridors {
281            let rx = find(&mut parent, c.from);
282            let ry = find(&mut parent, c.to);
283            if rx != ry { parent[rx] = ry; }
284        }
285        let mut roots = HashSet::new();
286        for i in 0..n { roots.insert(find(&mut parent, i)); }
287        roots.len()
288    }
289
290    pub fn is_connected(&self) -> bool {
291        self.connected_components() <= 1
292    }
293
294    pub fn shortest_path(&self, a: usize, b: usize) -> Option<Vec<usize>> {
295        if a >= self.rooms.len() || b >= self.rooms.len() { return None; }
296        if a == b { return Some(vec![a]); }
297        let mut visited = vec![false; self.rooms.len()];
298        let mut prev = vec![usize::MAX; self.rooms.len()];
299        let mut queue = VecDeque::new();
300        visited[a] = true;
301        queue.push_back(a);
302        while let Some(cur) = queue.pop_front() {
303            if cur == b {
304                let mut path = Vec::new();
305                let mut node = b;
306                while node != usize::MAX {
307                    path.push(node);
308                    node = prev[node];
309                }
310                path.reverse();
311                return Some(path);
312            }
313            if cur < self.adj.len() {
314                for &(nb, _) in &self.adj[cur] {
315                    if !visited[nb] {
316                        visited[nb] = true;
317                        prev[nb] = cur;
318                        queue.push_back(nb);
319                    }
320                }
321            }
322        }
323        None
324    }
325
326    pub fn minimum_spanning_tree(&self) -> Vec<(usize, usize)> {
327        let n = self.rooms.len();
328        if n < 2 { return Vec::new(); }
329        let mut edges: Vec<(f32, usize, usize)> = Vec::new();
330        for i in 0..n {
331            for j in (i + 1)..n {
332                let ci = self.rooms[i].center();
333                let cj = self.rooms[j].center();
334                let dx = (ci.x - cj.x) as f32;
335                let dy = (ci.y - cj.y) as f32;
336                edges.push(((dx * dx + dy * dy).sqrt(), i, j));
337            }
338        }
339        edges.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
340        let mut parent: Vec<usize> = (0..n).collect();
341        fn find(parent: &mut Vec<usize>, x: usize) -> usize {
342            if parent[x] != x { parent[x] = find(parent, parent[x]); }
343            parent[x]
344        }
345        let mut mst = Vec::new();
346        for (_, u, v) in edges {
347            let ru = find(&mut parent, u);
348            let rv = find(&mut parent, v);
349            if ru != rv {
350                parent[ru] = rv;
351                mst.push((u, v));
352                if mst.len() == n - 1 { break; }
353            }
354        }
355        mst
356    }
357}
358
359// ── BspSplitter ───────────────────────────────────────────────────────────────
360
361pub struct BspSplitter {
362    pub min_room_size: i32,
363    pub split_jitter:  f32,
364    pub max_depth:     u32,
365}
366
367impl BspSplitter {
368    pub fn new(min_room_size: i32, split_jitter: f32, max_depth: u32) -> Self {
369        Self { min_room_size, split_jitter, max_depth }
370    }
371
372    pub fn generate(&self, width: i32, height: i32, rng: &mut Rng) -> DungeonGraph {
373        let bounds = IRect::new(0, 0, width, height);
374        let mut leaves = Vec::new();
375        self.split_node(bounds, rng, 0, &mut leaves);
376
377        let mut graph = DungeonGraph::new();
378        let n = leaves.len();
379        for (i, rect) in leaves.iter().enumerate() {
380            let mut room = Room::new(i, *rect);
381            let spawn_count = rng.range_usize(3) + 1;
382            room.generate_spawns(rng, spawn_count);
383            graph.add_room(room);
384        }
385
386        let mst_edges = graph.minimum_spanning_tree();
387        for (a, b) in mst_edges {
388            let fp = graph.rooms[a].center();
389            let tp = graph.rooms[b].center();
390            let c  = Corridor::new(a, b, fp, tp, rng);
391            graph.add_corridor(c);
392        }
393        let extra = (n / 5).max(1);
394        let total = graph.rooms.len();
395        for _ in 0..extra {
396            if total < 2 { break; }
397            let a = rng.range_usize(total);
398            let b = rng.range_usize(total);
399            if a != b {
400                let fp = graph.rooms[a].center();
401                let tp = graph.rooms[b].center();
402                let c  = Corridor::new(a, b, fp, tp, rng);
403                graph.add_corridor(c);
404            }
405        }
406
407        if !graph.rooms.is_empty() { graph.rooms[0].room_type = RoomType::Entrance; }
408        let last = graph.rooms.len().saturating_sub(1);
409        if graph.rooms.len() > 1 { graph.rooms[last].room_type = RoomType::Exit; }
410        let specials = graph.rooms.len() / 6;
411        let mut indices: Vec<usize> = (1..graph.rooms.len().saturating_sub(1)).collect();
412        rng.shuffle(&mut indices);
413        for &i in indices.iter().take(specials) {
414            graph.rooms[i].room_type = RoomType::Treasure;
415        }
416        for &i in indices.iter().skip(specials).take(specials) {
417            graph.rooms[i].room_type = RoomType::Combat(rng.range_f32(0.3, 0.9));
418        }
419        if let Some(&bi) = indices.last() {
420            graph.rooms[bi].room_type = RoomType::Boss;
421        }
422
423        graph
424    }
425
426    fn split_node(&self, bounds: IRect, rng: &mut Rng, depth: u32, leaves: &mut Vec<IRect>) {
427        if depth >= self.max_depth
428            || (bounds.w < self.min_room_size * 2 && bounds.h < self.min_room_size * 2)
429        {
430            if let Some(inner) = bounds.shrink(2) {
431                if inner.w >= self.min_room_size && inner.h >= self.min_room_size {
432                    let rw = rng.range_i32(self.min_room_size, inner.w);
433                    let rh = rng.range_i32(self.min_room_size, inner.h);
434                    let rx = rng.range_i32(inner.x, inner.x + inner.w - rw);
435                    let ry = rng.range_i32(inner.y, inner.y + inner.h - rh);
436                    leaves.push(IRect::new(rx, ry, rw, rh));
437                }
438            }
439            return;
440        }
441        let split_h = if bounds.w > bounds.h { false }
442                      else if bounds.h > bounds.w { true }
443                      else { rng.chance(0.5) };
444        let jitter = rng.range_f32(-self.split_jitter, self.split_jitter);
445        let ratio  = (0.5 + jitter).clamp(0.25, 0.75);
446        if split_h {
447            if bounds.h < self.min_room_size * 2 { leaves.push(bounds); return; }
448            let sy = bounds.y + (bounds.h as f32 * ratio) as i32;
449            self.split_node(IRect::new(bounds.x, bounds.y, bounds.w, sy - bounds.y), rng, depth + 1, leaves);
450            self.split_node(IRect::new(bounds.x, sy, bounds.w, bounds.h - (sy - bounds.y)), rng, depth + 1, leaves);
451        } else {
452            if bounds.w < self.min_room_size * 2 { leaves.push(bounds); return; }
453            let sx = bounds.x + (bounds.w as f32 * ratio) as i32;
454            self.split_node(IRect::new(bounds.x, bounds.y, sx - bounds.x, bounds.h), rng, depth + 1, leaves);
455            self.split_node(IRect::new(sx, bounds.y, bounds.w - (sx - bounds.x), bounds.h), rng, depth + 1, leaves);
456        }
457    }
458}
459
460// ── RoomPlacer ────────────────────────────────────────────────────────────────
461
462pub struct RoomPlacer {
463    pub min_room_w:   i32,
464    pub max_room_w:   i32,
465    pub min_room_h:   i32,
466    pub max_room_h:   i32,
467    pub separation:   i32,
468    pub max_attempts: usize,
469}
470
471impl Default for RoomPlacer {
472    fn default() -> Self {
473        Self { min_room_w: 5, max_room_w: 14, min_room_h: 4, max_room_h: 10, separation: 2, max_attempts: 500 }
474    }
475}
476
477impl RoomPlacer {
478    pub fn new(min_w: i32, max_w: i32, min_h: i32, max_h: i32, sep: i32) -> Self {
479        Self { min_room_w: min_w, max_room_w: max_w, min_room_h: min_h, max_room_h: max_h, separation: sep, max_attempts: 500 }
480    }
481
482    pub fn generate(&self, width: i32, height: i32, num_rooms: usize, rng: &mut Rng) -> DungeonGraph {
483        let mut placed: Vec<IRect> = Vec::new();
484        let mut attempts = 0usize;
485        while placed.len() < num_rooms && attempts < self.max_attempts {
486            attempts += 1;
487            let rw = rng.range_i32(self.min_room_w, self.max_room_w);
488            let rh = rng.range_i32(self.min_room_h, self.max_room_h);
489            let rx = rng.range_i32(1, (width - rw - 1).max(2));
490            let ry = rng.range_i32(1, (height - rh - 1).max(2));
491            let candidate = IRect::new(rx, ry, rw, rh);
492            let expanded  = candidate.expand(self.separation);
493            if !placed.iter().any(|r| r.overlaps(&expanded)) { placed.push(candidate); }
494        }
495        let mut graph = DungeonGraph::new();
496        for (i, rect) in placed.iter().enumerate() {
497            let mut room = Room::new(i, *rect);
498            let spawn_count = rng.range_usize(3) + 1;
499            room.generate_spawns(rng, spawn_count);
500            graph.add_room(room);
501        }
502        let mst = graph.minimum_spanning_tree();
503        for (a, b) in mst {
504            let fp = graph.rooms[a].center();
505            let tp = graph.rooms[b].center();
506            let c  = Corridor::new(a, b, fp, tp, rng);
507            graph.add_corridor(c);
508        }
509        let extra = (placed.len() / 5).max(1);
510        let n = graph.rooms.len();
511        for _ in 0..extra {
512            if n < 2 { break; }
513            let a = rng.range_usize(n);
514            let b = (a + 1 + rng.range_usize(n - 1)) % n;
515            let fp = graph.rooms[a].center();
516            let tp = graph.rooms[b].center();
517            let c  = Corridor::new(a, b, fp, tp, rng);
518            graph.add_corridor(c);
519        }
520        if !graph.rooms.is_empty() { graph.rooms[0].room_type = RoomType::Entrance; }
521        let last = graph.rooms.len().saturating_sub(1);
522        if last > 0 { graph.rooms[last].room_type = RoomType::Exit; }
523        graph
524    }
525}
526
527// ── CellularDungeon ───────────────────────────────────────────────────────────
528
529pub struct CellularDungeon {
530    pub width:      usize,
531    pub height:     usize,
532    pub fill_prob:  f32,
533    pub birth:      usize,
534    pub survive:    usize,
535    pub iterations: usize,
536}
537
538impl CellularDungeon {
539    pub fn new(width: usize, height: usize) -> Self {
540        Self { width, height, fill_prob: 0.45, birth: 5, survive: 4, iterations: 5 }
541    }
542
543    pub fn generate(&self, rng: &mut Rng) -> Vec<bool> {
544        let n = self.width * self.height;
545        let mut grid: Vec<bool> = (0..n).map(|_| !rng.chance(self.fill_prob)).collect();
546        self.fill_border(&mut grid, false);
547        let mut next = vec![false; n];
548        for _ in 0..self.iterations {
549            for y in 0..self.height {
550                for x in 0..self.width {
551                    let nb = self.count_neighbours(&grid, x, y);
552                    let alive = grid[y * self.width + x];
553                    next[y * self.width + x] = if alive { nb >= self.survive } else { nb >= self.birth };
554                }
555            }
556            self.fill_border(&mut next, false);
557            std::mem::swap(&mut grid, &mut next);
558        }
559        let largest = self.largest_component(&grid);
560        for i in 0..n { if grid[i] && !largest.contains(&i) { grid[i] = false; } }
561        grid
562    }
563
564    fn fill_border(&self, grid: &mut Vec<bool>, val: bool) {
565        let (w, h) = (self.width, self.height);
566        for x in 0..w { grid[x] = val; grid[(h-1)*w+x] = val; }
567        for y in 0..h { grid[y*w] = val; grid[y*w+w-1] = val; }
568    }
569
570    fn count_neighbours(&self, grid: &[bool], x: usize, y: usize) -> usize {
571        let mut count = 0;
572        for dy in -1i32..=1 { for dx in -1i32..=1 {
573            if dx == 0 && dy == 0 { continue; }
574            let nx = x as i32 + dx; let ny = y as i32 + dy;
575            if nx < 0 || ny < 0 || nx as usize >= self.width || ny as usize >= self.height { count += 1; }
576            else if grid[ny as usize * self.width + nx as usize] { count += 1; }
577        }}
578        count
579    }
580
581    fn largest_component(&self, grid: &[bool]) -> HashSet<usize> {
582        let n = self.width * self.height;
583        let mut visited = vec![false; n];
584        let mut best: HashSet<usize> = HashSet::new();
585        for start in 0..n {
586            if !grid[start] || visited[start] { continue; }
587            let mut comp = HashSet::new();
588            let mut q = VecDeque::new();
589            q.push_back(start); visited[start] = true;
590            while let Some(idx) = q.pop_front() {
591                comp.insert(idx);
592                let (x, y) = ((idx % self.width) as i32, (idx / self.width) as i32);
593                for (dx, dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] {
594                    let (nx, ny) = (x + dx, y + dy);
595                    if nx < 0 || ny < 0 || nx as usize >= self.width || ny as usize >= self.height { continue; }
596                    let ni = ny as usize * self.width + nx as usize;
597                    if grid[ni] && !visited[ni] { visited[ni] = true; q.push_back(ni); }
598                }
599            }
600            if comp.len() > best.len() { best = comp; }
601        }
602        best
603    }
604}
605
606// ── WFC ───────────────────────────────────────────────────────────────────────
607
608#[derive(Debug, Clone)]
609pub struct WfcTile {
610    pub id:     usize,
611    pub name:   String,
612    pub weight: f32,
613    pub allowed_neighbours: [Vec<usize>; 4],
614}
615
616impl WfcTile {
617    pub fn new(id: usize, name: impl Into<String>, weight: f32) -> Self {
618        Self { id, name: name.into(), weight, allowed_neighbours: [Vec::new(), Vec::new(), Vec::new(), Vec::new()] }
619    }
620
621    pub fn allow_neighbour(&mut self, direction: usize, neighbour_id: usize) {
622        if direction < 4 { self.allowed_neighbours[direction].push(neighbour_id); }
623    }
624}
625
626pub struct WfcDungeon {
627    pub width:  usize,
628    pub height: usize,
629    tiles:      Vec<WfcTile>,
630}
631
632impl WfcDungeon {
633    pub fn new(width: usize, height: usize, tiles: Vec<WfcTile>) -> Self {
634        Self { width, height, tiles }
635    }
636
637    pub fn generate(&self, rng: &mut Rng) -> Option<Vec<usize>> {
638        let n = self.width * self.height;
639        let tc = self.tiles.len();
640        if tc == 0 { return None; }
641        let all: Vec<usize> = (0..tc).collect();
642        let mut cells: Vec<Vec<usize>> = vec![all.clone(); n];
643        let max_iter = n * tc + 100;
644        let mut iter = 0;
645        loop {
646            iter += 1;
647            if iter > max_iter { return None; }
648            if cells.iter().all(|c| c.len() == 1) { break; }
649            let ci = cells.iter().enumerate().filter(|(_, c)| c.len() > 1).min_by_key(|(_, c)| c.len()).map(|(i, _)| i)?;
650            let opts = cells[ci].clone();
651            let weighted: Vec<(usize, f32)> = opts.iter().map(|&tid| (tid, self.tiles[tid].weight)).collect();
652            let chosen = rng.pick_weighted(&weighted).copied()?;
653            cells[ci] = vec![chosen];
654            if !self.propagate(&mut cells) { return None; }
655        }
656        Some(cells.iter().map(|c| *c.first().unwrap_or(&0)).collect())
657    }
658
659    fn propagate(&self, cells: &mut Vec<Vec<usize>>) -> bool {
660        let (w, h) = (self.width, self.height);
661        let n = w * h;
662        let mut changed = true;
663        while changed {
664            changed = false;
665            for idx in 0..n {
666                let (x, y) = ((idx % w) as i32, (idx / w) as i32);
667                let nbrs: [(i32, i32, usize); 4] = [(x, y-1, 0),(x, y+1, 1),(x+1, y, 2),(x-1, y, 3)];
668                for (nx, ny, dir) in nbrs {
669                    if nx < 0 || ny < 0 || nx as usize >= w || ny as usize >= h { continue; }
670                    let ni  = ny as usize * w + nx as usize;
671                    let opp = match dir { 0 => 1, 1 => 0, 2 => 3, _ => 2 };
672                    let cur = cells[idx].clone();
673                    let before = cells[ni].len();
674                    cells[ni].retain(|&nt| {
675                        cur.iter().any(|&ct| {
676                            (ct < self.tiles.len() && self.tiles[ct].allowed_neighbours[dir].contains(&nt))
677                            || (nt < self.tiles.len() && self.tiles[nt].allowed_neighbours[opp].contains(&ct))
678                        })
679                    });
680                    if cells[ni].is_empty() { return false; }
681                    if cells[ni].len() < before { changed = true; }
682                }
683            }
684        }
685        true
686    }
687
688    pub fn default_tileset() -> Vec<WfcTile> {
689        let mut wall  = WfcTile::new(0, "wall",  1.0);
690        let mut floor = WfcTile::new(1, "floor", 3.0);
691        let mut door  = WfcTile::new(2, "door",  0.3);
692        for d in 0..4 { wall.allow_neighbour(d, 0); wall.allow_neighbour(d, 1); }
693        for d in 0..4 { floor.allow_neighbour(d, 0); floor.allow_neighbour(d, 1); floor.allow_neighbour(d, 2); }
694        for d in 0..4 { door.allow_neighbour(d, 0); door.allow_neighbour(d, 1); door.allow_neighbour(d, 2); }
695        vec![wall, floor, door]
696    }
697}
698
699// ── DungeonDecorator ──────────────────────────────────────────────────────────
700
701#[derive(Debug, Clone, PartialEq, Eq, Hash)]
702pub enum ObjectKind {
703    Enemy, TreasureChest, Trap, LightSource, SpawnPoint,
704    ShopKeeper, BossMonster, Shrine, Puzzle, RestArea,
705}
706
707#[derive(Debug, Clone)]
708pub struct PlacedObject {
709    pub kind:     ObjectKind,
710    pub position: IVec2,
711    pub metadata: HashMap<String, String>,
712}
713
714impl PlacedObject {
715    pub fn new(kind: ObjectKind, position: IVec2) -> Self {
716        Self { kind, position, metadata: HashMap::new() }
717    }
718    pub fn with_meta(mut self, k: impl Into<String>, v: impl Into<String>) -> Self {
719        self.metadata.insert(k.into(), v.into()); self
720    }
721}
722
723pub struct DungeonDecorator {
724    pub enemy_density: f32,
725    pub trap_density:  f32,
726    pub light_density: f32,
727}
728
729impl Default for DungeonDecorator {
730    fn default() -> Self { Self { enemy_density: 0.05, trap_density: 0.02, light_density: 0.03 } }
731}
732
733impl DungeonDecorator {
734    pub fn new(ed: f32, td: f32, ld: f32) -> Self { Self { enemy_density: ed, trap_density: td, light_density: ld } }
735
736    pub fn decorate(&self, graph: &DungeonGraph, depth: u32, rng: &mut Rng) -> Vec<PlacedObject> {
737        let mut out = Vec::new();
738        for room in &graph.rooms {
739            let area = room.rect.area() as f32;
740            match &room.room_type {
741                RoomType::Entrance => {
742                    out.push(PlacedObject::new(ObjectKind::SpawnPoint, room.center()).with_meta("room_id", room.id.to_string()));
743                    self.lights(room, 2, rng, &mut out);
744                }
745                RoomType::Exit => {
746                    out.push(PlacedObject::new(ObjectKind::SpawnPoint, room.center()).with_meta("type","exit"));
747                }
748                RoomType::Combat(diff) => {
749                    let ne = ((area * self.enemy_density * diff) as usize + 1).min(8);
750                    for _ in 0..ne {
751                        let pos = self.rpos(room, rng);
752                        let lv  = (depth as f32 * diff) as u32 + 1;
753                        out.push(PlacedObject::new(ObjectKind::Enemy, pos).with_meta("level", lv.to_string()));
754                    }
755                    let nt = ((area * self.trap_density) as usize).min(3);
756                    for _ in 0..nt {
757                        out.push(PlacedObject::new(ObjectKind::Trap, self.rpos(room, rng)).with_meta("hidden","true"));
758                    }
759                }
760                RoomType::Treasure => {
761                    let nc = rng.range_usize(2) + 1;
762                    for _ in 0..nc {
763                        out.push(PlacedObject::new(ObjectKind::TreasureChest, self.rpos(room, rng)).with_meta("depth", depth.to_string()));
764                    }
765                    self.lights(room, 1, rng, &mut out);
766                }
767                RoomType::Boss => {
768                    out.push(PlacedObject::new(ObjectKind::BossMonster, room.center()).with_meta("level",(depth*3).to_string()));
769                    out.push(PlacedObject::new(ObjectKind::TreasureChest, self.rpos(room, rng)).with_meta("rarity","legendary"));
770                    self.lights(room, 3, rng, &mut out);
771                }
772                RoomType::Shop => {
773                    out.push(PlacedObject::new(ObjectKind::ShopKeeper, room.center()).with_meta("stock_seed", rng.next_u64().to_string()));
774                    self.lights(room, 2, rng, &mut out);
775                }
776                RoomType::Rest | RoomType::Shrine => {
777                    out.push(PlacedObject::new(ObjectKind::RestArea, room.center()));
778                    self.lights(room, 1, rng, &mut out);
779                }
780                RoomType::Puzzle => {
781                    out.push(PlacedObject::new(ObjectKind::Puzzle, room.center()).with_meta("seed", rng.next_u64().to_string()));
782                }
783                RoomType::Secret => {
784                    out.push(PlacedObject::new(ObjectKind::TreasureChest, self.rpos(room, rng)).with_meta("hidden","true").with_meta("rarity","rare"));
785                }
786                _ => {
787                    let ne = ((area * self.enemy_density) as usize).min(5);
788                    for _ in 0..ne {
789                        out.push(PlacedObject::new(ObjectKind::Enemy, self.rpos(room, rng)).with_meta("level", depth.to_string()));
790                    }
791                }
792            }
793            let nl = ((area * self.light_density) as usize).min(4);
794            for _ in 0..nl { out.push(PlacedObject::new(ObjectKind::LightSource, self.rpos(room, rng))); }
795        }
796        out
797    }
798
799    fn lights(&self, room: &Room, n: usize, rng: &mut Rng, out: &mut Vec<PlacedObject>) {
800        for _ in 0..n { out.push(PlacedObject::new(ObjectKind::LightSource, self.rpos(room, rng))); }
801    }
802
803    fn rpos(&self, room: &Room, rng: &mut Rng) -> IVec2 {
804        let r = room.rect;
805        IVec2::new(
806            rng.range_i32(r.x + 1, (r.x + r.w - 2).max(r.x + 1)),
807            rng.range_i32(r.y + 1, (r.y + r.h - 2).max(r.y + 1)),
808        )
809    }
810}
811
812// ── MazeGenerator ─────────────────────────────────────────────────────────────
813
814#[derive(Debug, Clone, Copy, PartialEq, Eq)]
815pub struct MazeCell {
816    pub walls:   [bool; 4],
817    pub visited: bool,
818}
819
820impl Default for MazeCell {
821    fn default() -> Self { Self { walls: [true; 4], visited: false } }
822}
823
824pub struct MazeGenerator {
825    pub width:  usize,
826    pub height: usize,
827}
828
829impl MazeGenerator {
830    pub fn new(width: usize, height: usize) -> Self { Self { width, height } }
831
832    fn idx(&self, x: usize, y: usize) -> usize { y * self.width + x }
833
834    fn remove_wall(cells: &mut Vec<MazeCell>, a: usize, b: usize, dir: usize) {
835        let opp = [1usize, 0, 3, 2];
836        cells[a].walls[dir]      = false;
837        cells[b].walls[opp[dir]] = false;
838        cells[a].visited = true;
839        cells[b].visited = true;
840    }
841
842    fn nbrs(&self, x: usize, y: usize) -> Vec<(usize, usize, usize)> {
843        let mut v = Vec::new();
844        if y > 0               { v.push((x, y-1, 0)); }
845        if y+1 < self.height   { v.push((x, y+1, 1)); }
846        if x+1 < self.width    { v.push((x+1, y, 2)); }
847        if x > 0               { v.push((x-1, y, 3)); }
848        v
849    }
850
851    pub fn recursive_backtracker(&self, rng: &mut Rng) -> Vec<MazeCell> {
852        let n = self.width * self.height;
853        let mut cells = vec![MazeCell::default(); n];
854        let mut stack = Vec::new();
855        let s = self.idx(0, 0);
856        cells[s].visited = true;
857        stack.push((0usize, 0usize));
858        while let Some(&(cx, cy)) = stack.last() {
859            let mut unvisited: Vec<_> = self.nbrs(cx, cy).into_iter().filter(|&(nx, ny, _)| !cells[self.idx(nx,ny)].visited).collect();
860            if unvisited.is_empty() { stack.pop(); }
861            else {
862                rng.shuffle(&mut unvisited);
863                let (nx, ny, dir) = unvisited[0];
864                let (ai, bi) = (self.idx(cx,cy), self.idx(nx,ny));
865                Self::remove_wall(&mut cells, ai, bi, dir);
866                cells[bi].visited = true;
867                stack.push((nx, ny));
868            }
869        }
870        cells
871    }
872
873    pub fn ellers_algorithm(&self, rng: &mut Rng) -> Vec<MazeCell> {
874        let n = self.width * self.height;
875        let mut cells = vec![MazeCell::default(); n];
876        let (w, h) = (self.width, self.height);
877        let mut set_id = (1..=w).collect::<Vec<usize>>();
878        let mut next_set = w + 1;
879        for y in 0..h {
880            let last = y + 1 == h;
881            for x in 0..(w-1) {
882                let merge = if last { set_id[x] != set_id[x+1] } else { rng.chance(0.5) && set_id[x] != set_id[x+1] };
883                if merge {
884                    let old = set_id[x+1]; let new = set_id[x];
885                    for s in &mut set_id { if *s == old { *s = new; } }
886                    let (ai, bi) = (self.idx(x, y), self.idx(x+1, y));
887                    Self::remove_wall(&mut cells, ai, bi, 2);
888                }
889            }
890            if !last {
891                let mut sets: HashMap<usize, Vec<usize>> = HashMap::new();
892                for x in 0..w { sets.entry(set_id[x]).or_default().push(x); }
893                let mut nid: Vec<usize> = (next_set..next_set+w).collect();
894                next_set += w;
895                for (sid, xs) in &sets {
896                    let nv = rng.range_usize(xs.len()) + 1;
897                    let mut chosen = xs.clone(); rng.shuffle(&mut chosen);
898                    for &cx in chosen.iter().take(nv) {
899                        let (ai, bi) = (self.idx(cx,y), self.idx(cx,y+1));
900                        Self::remove_wall(&mut cells, ai, bi, 1);
901                        nid[cx] = *sid;
902                    }
903                }
904                set_id = nid;
905            }
906        }
907        cells
908    }
909
910    pub fn prims_algorithm(&self, rng: &mut Rng) -> Vec<MazeCell> {
911        let n = self.width * self.height;
912        let mut cells = vec![MazeCell::default(); n];
913        let mut in_maze = vec![false; n];
914        let s = self.idx(0,0); in_maze[s] = true; cells[s].visited = true;
915        let mut frontier: Vec<(usize,usize,usize,usize,usize)> = self.nbrs(0,0).into_iter().map(|(nx,ny,d)| (0,0,nx,ny,d)).collect();
916        while !frontier.is_empty() {
917            let fi = rng.range_usize(frontier.len());
918            let (ax,ay,nx,ny,dir) = frontier.swap_remove(fi);
919            let bi = self.idx(nx,ny);
920            if in_maze[bi] { continue; }
921            in_maze[bi] = true; cells[bi].visited = true;
922            Self::remove_wall(&mut cells, self.idx(ax,ay), bi, dir);
923            for (nnx,nny,nd) in self.nbrs(nx,ny) { if !in_maze[self.idx(nnx,nny)] { frontier.push((nx,ny,nnx,nny,nd)); } }
924        }
925        cells
926    }
927
928    pub fn kruskals_algorithm(&self, rng: &mut Rng) -> Vec<MazeCell> {
929        let n = self.width * self.height;
930        let mut cells = vec![MazeCell::default(); n];
931        let mut edges: Vec<(usize,usize,usize,usize,usize)> = Vec::new();
932        for y in 0..self.height { for x in 0..self.width {
933            if x+1 < self.width  { edges.push((x, y, x+1, y, 2)); }
934            if y+1 < self.height { edges.push((x, y, x, y+1, 1)); }
935        }}
936        rng.shuffle(&mut edges);
937        let mut parent: Vec<usize> = (0..n).collect();
938        fn find(p: &mut Vec<usize>, x: usize) -> usize {
939            if p[x] != x { p[x] = find(p, p[x]); } p[x]
940        }
941        for (ax,ay,bx,by,dir) in edges {
942            let (ai, bi) = (self.idx(ax,ay), self.idx(bx,by));
943            let (ra, rb) = (find(&mut parent, ai), find(&mut parent, bi));
944            if ra != rb { parent[ra] = rb; Self::remove_wall(&mut cells, ai, bi, dir); }
945        }
946        cells
947    }
948
949    pub fn to_tiles(&self, cells: &[MazeCell]) -> Vec<Tile> {
950        let (tw, th) = (self.width*2+1, self.height*2+1);
951        let mut tiles = vec![Tile::Wall; tw*th];
952        for y in 0..self.height { for x in 0..self.width {
953            let cell = cells[self.idx(x,y)];
954            let (tx,ty) = (x*2+1, y*2+1);
955            tiles[ty*tw+tx] = Tile::Floor;
956            if !cell.walls[2] && x+1 < self.width  { tiles[ty*tw+tx+1]     = Tile::Corridor; }
957            if !cell.walls[1] && y+1 < self.height { tiles[(ty+1)*tw+tx]   = Tile::Corridor; }
958        }}
959        tiles
960    }
961}
962
963// ── DungeonFloor (legacy) ──────────────────────────────────────────────────────
964
965#[derive(Debug, Clone)]
966pub struct DungeonFloor {
967    pub width:     usize,
968    pub height:    usize,
969    pub theme:     DungeonTheme,
970    pub rooms:     Vec<Room>,
971    pub corridors: Vec<Corridor>,
972    pub tiles:     Vec<Tile>,
973    pub depth:     u32,
974    pub start:     (i32, i32),
975    pub exit:      (i32, i32),
976    pub boss_room: Option<usize>,
977}
978
979impl DungeonFloor {
980    pub fn generate(seed: u64, depth: u32, theme: DungeonTheme) -> Self {
981        let mut rng = Rng::new(seed ^ (depth as u64).wrapping_mul(0xdeadbeef));
982        let w = (60 + depth as usize * 5).min(200);
983        let h = (40 + depth as usize * 3).min(120);
984        let graph = BspSplitter::new(5, 0.2, 5 + depth/2).generate(w as i32, h as i32, &mut rng);
985        let rooms     = graph.rooms.clone();
986        let corridors = graph.corridors.clone();
987        let start = rooms.first().map(|r| { let c=r.center(); (c.x,c.y) }).unwrap_or((1,1));
988        let exit  = rooms.last() .map(|r| { let c=r.center(); (c.x,c.y) }).unwrap_or((w as i32-2, h as i32-2));
989        let boss_room = rooms.iter().position(|r| r.room_type == RoomType::Boss);
990        let mut tiles = vec![Tile::Wall; w*h];
991        for room in &rooms {
992            let IRect{x,y,w:rw,h:rh} = room.rect;
993            for ty in y..(y+rh) { for tx in x..(x+rw) {
994                if tx>=0 && ty>=0 && (tx as usize)<w && (ty as usize)<h {
995                    tiles[ty as usize*w+tx as usize] = Tile::Floor;
996                }
997            }}
998        }
999        for corr in &corridors {
1000            for pos in corr.tiles() {
1001                let (tx,ty) = (pos.x, pos.y);
1002                if tx>=0 && ty>=0 && (tx as usize)<w && (ty as usize)<h {
1003                    let i = ty as usize*w+tx as usize;
1004                    if tiles[i] == Tile::Wall { tiles[i] = Tile::Corridor; }
1005                }
1006            }
1007            if corr.has_door {
1008                let (bx,by) = (corr.bend.x, corr.bend.y);
1009                if bx>=0 && by>=0 && (bx as usize)<w && (by as usize)<h {
1010                    tiles[by as usize*w+bx as usize] = Tile::Door;
1011                }
1012            }
1013        }
1014        let (ex,ey) = exit;
1015        if ex>=0 && ey>=0 && (ex as usize)<w && (ey as usize)<h {
1016            tiles[ey as usize*w+ex as usize] = Tile::Stairs;
1017        }
1018        Self { width:w, height:h, theme, rooms, corridors, tiles, depth, start, exit, boss_room }
1019    }
1020
1021    pub fn get(&self, x: i32, y: i32) -> Tile {
1022        if x<0||y<0||(x as usize)>=self.width||(y as usize)>=self.height { return Tile::Void; }
1023        self.tiles[y as usize*self.width+x as usize]
1024    }
1025
1026    pub fn reachable_tiles(&self, sx: i32, sy: i32) -> Vec<(i32,i32)> {
1027        let mut visited = vec![false; self.width*self.height];
1028        let mut queue = VecDeque::new();
1029        let mut result = Vec::new();
1030        if self.get(sx,sy).is_walkable() { queue.push_back((sx,sy)); }
1031        while let Some((x,y)) = queue.pop_front() {
1032            if x<0||y<0||(x as usize)>=self.width||(y as usize)>=self.height { continue; }
1033            let idx = y as usize*self.width+x as usize;
1034            if visited[idx] { continue; }
1035            visited[idx] = true;
1036            if self.tiles[idx].is_walkable() {
1037                result.push((x,y));
1038                for (dx,dy) in &[(0i32,1),(0,-1),(1,0),(-1,0)] { queue.push_back((x+dx, y+dy)); }
1039            }
1040        }
1041        result
1042    }
1043
1044    pub fn walkable_count(&self) -> usize { self.tiles.iter().filter(|t| t.is_walkable()).count() }
1045
1046    pub fn room_at(&self, x: i32, y: i32) -> Option<usize> {
1047        self.rooms.iter().position(|r| r.rect.contains(x,y))
1048    }
1049
1050    pub fn doors(&self) -> impl Iterator<Item=(i32,i32)> + '_ {
1051        (0..self.height).flat_map(move |y| (0..self.width).filter_map(move |x| {
1052            if self.tiles[y*self.width+x] == Tile::Door { Some((x as i32, y as i32)) } else { None }
1053        }))
1054    }
1055
1056    pub fn dimensions(&self) -> (usize,usize) { (self.width, self.height) }
1057}
1058
1059#[derive(Debug, Clone)]
1060pub struct FloorMetrics {
1061    pub room_count: usize, pub corridor_count: usize, pub walkable_tiles: usize,
1062    pub total_tiles: usize, pub fill_ratio: f32, pub has_boss: bool,
1063    pub treasure_rooms: usize, pub avg_room_area: f32,
1064}
1065
1066impl FloorMetrics {
1067    pub fn compute(floor: &DungeonFloor) -> Self {
1068        let walkable = floor.walkable_count();
1069        let total    = floor.width * floor.height;
1070        let treasure = floor.rooms.iter().filter(|r| r.room_type == RoomType::Treasure).count();
1071        let avg_area = if floor.rooms.is_empty() { 0.0 } else {
1072            floor.rooms.iter().map(|r| r.rect.area()).sum::<i32>() as f32 / floor.rooms.len() as f32
1073        };
1074        Self { room_count: floor.rooms.len(), corridor_count: floor.corridors.len(), walkable_tiles: walkable,
1075               total_tiles: total, fill_ratio: walkable as f32/total as f32, has_boss: floor.boss_room.is_some(),
1076               treasure_rooms: treasure, avg_room_area: avg_area }
1077    }
1078}
1079
1080// ── Tests ─────────────────────────────────────────────────────────────────────
1081
1082#[cfg(test)]
1083mod tests {
1084    use super::*;
1085
1086    fn rng() -> Rng { Rng::new(42) }
1087
1088    #[test] fn dungeon_floor_generates_rooms() { assert!(!DungeonFloor::generate(42,1,DungeonTheme::Cave).rooms.is_empty()); }
1089    #[test] fn dungeon_floor_start_walkable() { let f=DungeonFloor::generate(99,1,DungeonTheme::Cave); let (sx,sy)=f.start; assert!(f.get(sx,sy).is_walkable()); }
1090    #[test] fn floor_fill_ratio_reasonable() { let m=FloorMetrics::compute(&DungeonFloor::generate(7,1,DungeonTheme::Cave)); assert!(m.fill_ratio>0.01&&m.fill_ratio<0.95,"fill: {}",m.fill_ratio); }
1091
1092    #[test]
1093    fn bsp_splitter_connected() {
1094        let mut r = rng();
1095        let g = BspSplitter::new(5,0.15,4).generate(80,60,&mut r);
1096        assert!(!g.rooms.is_empty());
1097        assert!(g.is_connected());
1098    }
1099
1100    #[test]
1101    fn room_placer_generates_rooms() {
1102        let mut r = rng();
1103        let g = RoomPlacer::default().generate(100,80,10,&mut r);
1104        assert!(g.rooms.len()>=3,"got {}",g.rooms.len());
1105    }
1106
1107    #[test]
1108    fn cellular_has_floor_tiles() {
1109        let mut r = rng();
1110        let grid = CellularDungeon::new(60,40).generate(&mut r);
1111        assert!(grid.iter().filter(|&&b| b).count()>50);
1112    }
1113
1114    #[test]
1115    fn maze_rb_all_visited() {
1116        let mut r = rng();
1117        assert!(MazeGenerator::new(10,10).recursive_backtracker(&mut r).iter().all(|c| c.visited));
1118    }
1119
1120    #[test]
1121    fn maze_prims_all_visited() {
1122        let mut r = rng();
1123        assert!(MazeGenerator::new(8,8).prims_algorithm(&mut r).iter().all(|c| c.visited));
1124    }
1125
1126    #[test]
1127    fn maze_kruskals_all_visited() {
1128        let mut r = rng();
1129        assert!(MazeGenerator::new(8,8).kruskals_algorithm(&mut r).iter().all(|c| c.visited));
1130    }
1131
1132    #[test]
1133    fn graph_shortest_path() {
1134        let mut r = rng();
1135        let g = RoomPlacer::default().generate(80,60,6,&mut r);
1136        if g.rooms.len()>=2 { assert!(g.shortest_path(0,g.rooms.len()-1).is_some()); }
1137    }
1138
1139    #[test]
1140    fn graph_mst_edges() {
1141        let mut r = rng();
1142        let g = RoomPlacer::default().generate(80,60,8,&mut r);
1143        let n = g.rooms.len();
1144        if n>=2 { assert_eq!(g.minimum_spanning_tree().len(), n-1); }
1145    }
1146
1147    #[test]
1148    fn decorator_places_objects() {
1149        let mut r = rng();
1150        let g = BspSplitter::new(6,0.2,4).generate(80,60,&mut r);
1151        assert!(!DungeonDecorator::default().decorate(&g,3,&mut r).is_empty());
1152    }
1153
1154    #[test]
1155    fn wfc_does_not_panic() {
1156        let mut r = rng();
1157        let _ = WfcDungeon::new(8,8,WfcDungeon::default_tileset()).generate(&mut r);
1158    }
1159
1160    #[test]
1161    fn maze_tiles_correct_size() {
1162        let mut r = rng();
1163        let g = MazeGenerator::new(5,5);
1164        let cells = g.recursive_backtracker(&mut r);
1165        assert_eq!(g.to_tiles(&cells).len(), 11*11);
1166    }
1167
1168    #[test]
1169    fn ellers_all_visited() {
1170        let mut r = rng();
1171        assert!(MazeGenerator::new(8,8).ellers_algorithm(&mut r).iter().all(|c| c.visited));
1172    }
1173}