tatami_dungeon/
lib.rs

1//! Tatami is a roguelike dungeon generation algorithm that creates a multi-floor dungeon layout from a series of randomly oriented, interconnected rectangles.
2//!
3//! The library attempts to provide many of the common features found in roguelikes, such as stairs, teleporters, items, enemies and traps. It is intended to be used as a base upon which a fully featured game can be built on.
4
5use image::{imageops::overlay, open, GenericImageView, ImageError, Rgb, RgbImage};
6use itertools::Itertools;
7use noise::{NoiseFn, Perlin};
8use num_integer::Roots;
9use rand::distributions::WeightedIndex;
10use rand::prelude::*;
11use rand_chacha::ChaCha20Rng;
12use rayon::prelude::*;
13use rustc_hash::{FxHashMap, FxHashSet};
14use serde::{Deserialize, Serialize};
15use std::cmp::{self, Ordering};
16use std::collections::{BinaryHeap, VecDeque};
17use std::path::Path;
18use std::sync::{Arc, Mutex};
19use strum::IntoEnumIterator;
20use strum_macros::EnumIter;
21
22/// The parameters used to generate a dungeon.
23///
24/// Values can be in units of cells or tiles. Dungeon is first generated on a smaller grid of cells, which are then translated to a larger number of tiles. This is what gives the dungeon its rectangular structure.
25#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
26pub struct GenerateDungeonParams {
27    /// Width and height of dungeon in terms of cells.
28    pub dimensions: (u32, u32),
29    /// Number of tiles to translate cells to when outputting the final dungeon.
30    pub tiles_per_cell: u32,
31    /// Number of floors in the dungeon.
32    pub num_floors: u32,
33    /// Minimum number of main rooms per floor. These rooms are connected by corridors.
34    pub min_rooms_per_floor: u32,
35    /// Maximum number of main rooms per floor. These rooms are connected by corridors.
36    pub max_rooms_per_floor: u32,
37    /// Minimum dimensions in cells for main rooms.
38    pub min_room_dimensions: (u32, u32),
39    /// Maximum dimensions in cells for main rooms.
40    pub max_room_dimensions: (u32, u32),
41    /// Whether or not main rooms can connect directly with each other. If false, connections require a corridor in between.
42    pub adjacent_rooms_allowed: bool,
43    /// Minimum dimensions in cells for corridors.
44    pub min_corridor_dimensions: (u32, u32),
45    /// Maximum dimensions in cells for corridors.
46    pub max_corridor_dimensions: (u32, u32),
47    /// Dimensions in tiles for walls.
48    pub wall_dimensions: (u32, u32),
49    /// Offset in tiles for walls.
50    pub wall_offset: (i32, i32),
51    /// The minimum amount of shared cells required on the x and y axes for a room to be considered a neighbor.
52    pub min_shared_cells: (usize, usize),
53    /// This value decreases the likelihood of a corridor having one side much longer than the other.
54    pub squareness: f32,
55    /// Minimum downward stairs per floor. Number of upward stairs will always be equal to the number of downward stairs in the previous floor.
56    pub min_stairs_per_floor: u32,
57    /// Maximum downward stairs per floor. Number of upward stairs will always be equal to the number of downward stairs in the previous floor.
58    pub max_stairs_per_floor: u32,
59    /// Minimum pairs of teleporters per floor.
60    pub min_teleporters_per_floor: u32,
61    /// Maximum pairs of teleporters per floor.
62    pub max_teleporters_per_floor: u32,
63    /// Minimum number of items per main room or corridor.
64    pub min_items_per_room: u32,
65    /// Maximum number of items per main room or corridor.
66    pub max_items_per_room: u32,
67    /// Maximum value for item rarity score. Minimum will always be one. Items tend to be rarer in farther rooms or floors.
68    pub max_item_rarity: u32,
69    /// Scale to be used for the item density noise map.
70    pub item_density_scale: f32,
71    /// Scale to be used for the item rarity noise map.
72    pub item_rarity_scale: f32,
73    /// Minimum number of enemies per main room or corridor.
74    pub min_enemies_per_room: u32,
75    /// Maximum number of items per main room or corridor.
76    pub max_enemies_per_room: u32,
77    /// Maximum value for enemy difficulty score. Minimum will always be one. Enemies tend to be more difficult in farther rooms or floors.
78    pub max_enemy_difficulty: u32,
79    /// Scale to be used for the enemy density noise map.
80    pub enemy_density_scale: f32,
81    /// Scale to be used for the enemy difficulty noise map.
82    pub enemy_difficulty_scale: f32,
83    /// Minimum number of traps per main room or corridor.
84    pub min_traps_per_room: u32,
85    /// Maximum number of enemies per main room or corridor.
86    pub max_traps_per_room: u32,
87    /// Maximum value for trap difficulty score. Minimum will always be one. Traps tend to be more difficult in farther rooms or floors.
88    pub max_trap_difficulty: u32,
89    /// Scale to be used for the trap density noise map.
90    pub trap_density_scale: f32,
91    /// Scale to be used for the trap difficulty noise map.
92    pub trap_difficulty_scale: f32,
93    /// The difficulty of the farthest room vs the difficulty of the starting room.
94    pub difficulty_ratio: f32,
95}
96
97impl Default for GenerateDungeonParams {
98    fn default() -> Self {
99        Self {
100            dimensions: (32, 32),
101            tiles_per_cell: 6,
102            num_floors: 5,
103            min_rooms_per_floor: 16,
104            max_rooms_per_floor: 16,
105            min_room_dimensions: (2, 2),
106            max_room_dimensions: (4, 4),
107            adjacent_rooms_allowed: false,
108            min_corridor_dimensions: (1, 1),
109            max_corridor_dimensions: (8, 8),
110            wall_dimensions: (2, 2),
111            wall_offset: (0, 0),
112            min_shared_cells: (1, 1),
113            squareness: 1.0,
114            min_stairs_per_floor: 3,
115            max_stairs_per_floor: 3,
116            min_teleporters_per_floor: 3,
117            max_teleporters_per_floor: 3,
118            min_items_per_room: 1,
119            max_items_per_room: 5,
120            max_item_rarity: 100,
121            item_density_scale: 0.5,
122            item_rarity_scale: 2.0,
123            min_enemies_per_room: 1,
124            max_enemies_per_room: 5,
125            max_enemy_difficulty: 100,
126            enemy_density_scale: 0.5,
127            enemy_difficulty_scale: 2.0,
128            min_traps_per_room: 1,
129            max_traps_per_room: 5,
130            max_trap_difficulty: 100,
131            trap_density_scale: 0.5,
132            trap_difficulty_scale: 2.0,
133            difficulty_ratio: 5.0,
134        }
135    }
136}
137
138// A cell in the mock-up grid
139#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
140struct Cell {
141    pub i: u32,
142    pub j: u32,
143}
144
145impl Cell {
146    pub fn direction_to(&self, other: &Self) -> Option<Direction> {
147        if other.i < self.i && self.i - other.i == 1 && self.j == other.j {
148            Some(Direction::Left)
149        } else if other.i > self.i && other.i - self.i == 1 && self.j == other.j {
150            Some(Direction::Right)
151        } else if other.j < self.j && self.j - other.j == 1 && self.i == other.i {
152            Some(Direction::Up)
153        } else if other.j > self.j && other.j - self.j == 1 && self.i == other.i {
154            Some(Direction::Down)
155        } else {
156            None
157        }
158    }
159}
160
161/// A connection in the mock-up.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
163pub struct CellConnection {
164    /// ID of the connected to room.
165    pub id: u32,
166    /// Direction the connected to room is in.
167    pub direction: Direction,
168}
169
170// A room in the mock-up.
171#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
172struct CellRoom {
173    pub id: u32,
174    pub kind: RoomKind,
175    pub cell: Cell,
176    pub width: u32,
177    pub height: u32,
178    pub connections: Vec<CellConnection>,
179}
180
181impl CellRoom {
182    pub fn center(&self) -> Cell {
183        Cell {
184            i: self.cell.i + self.width / 2,
185            j: self.cell.j + self.height / 2,
186        }
187    }
188
189    pub fn cells(&self) -> impl Iterator<Item = Cell> + '_ {
190        (0..self.width).flat_map(move |x| {
191            (0..self.height).map(move |y| Cell {
192                i: self.cell.i + x,
193                j: self.cell.j + y,
194            })
195        })
196    }
197
198    pub fn shared_cells<'a>(&'a self, other: &'a Self) -> impl Iterator<Item = Cell> + 'a {
199        self.cells().filter(|cell| {
200            other
201                .cells()
202                .any(|other| cell.direction_to(&other).is_some())
203        })
204    }
205
206    pub fn direction_to(&self, other: &Self) -> Option<Direction> {
207        self.cells()
208            .find_map(|cell| other.cells().find_map(|other| cell.direction_to(&other)))
209    }
210}
211
212/// X and Y position of a tile.
213#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Deserialize, Serialize)]
214pub struct Position {
215    pub x: u32,
216    pub y: u32,
217}
218
219impl Position {
220    /// Adjacent positions in cardinal directions.
221    pub fn adjacent_4(&self, dimensions: (u32, u32)) -> Vec<Self> {
222        let mut positions = Vec::new();
223
224        if self.x > 0 {
225            positions.push(Self {
226                x: self.x - 1,
227                ..*self
228            });
229        }
230
231        if self.x < dimensions.0 - 1 {
232            positions.push(Self {
233                x: self.x + 1,
234                ..*self
235            });
236        }
237
238        if self.y > 0 {
239            positions.push(Self {
240                y: self.y - 1,
241                ..*self
242            });
243        }
244
245        if self.y < dimensions.1 - 1 {
246            positions.push(Self {
247                y: self.y + 1,
248                ..*self
249            });
250        }
251
252        positions
253    }
254
255    /// Adjacent positions in cardinal and diagonal directions.
256    pub fn adjacent_8(&self, dimensions: (u32, u32)) -> Vec<Self> {
257        let mut positions = Vec::new();
258
259        if self.x > 0 {
260            positions.push(Self {
261                x: self.x - 1,
262                ..*self
263            });
264
265            if self.y > 0 {
266                positions.push(Self {
267                    x: self.x - 1,
268                    y: self.y - 1,
269                });
270            }
271
272            if self.y < dimensions.1 - 1 {
273                positions.push(Self {
274                    x: self.x - 1,
275                    y: self.y + 1,
276                });
277            }
278        }
279
280        if self.x < dimensions.0 - 1 {
281            positions.push(Self {
282                x: self.x + 1,
283                ..*self
284            });
285
286            if self.y > 0 {
287                positions.push(Self {
288                    x: self.x + 1,
289                    y: self.y - 1,
290                });
291            }
292
293            if self.y < dimensions.1 - 1 {
294                positions.push(Self {
295                    x: self.x + 1,
296                    y: self.y + 1,
297                });
298            }
299        }
300
301        if self.y > 0 {
302            positions.push(Self {
303                y: self.y - 1,
304                ..*self
305            });
306        }
307
308        if self.y < dimensions.1 - 1 {
309            positions.push(Self {
310                y: self.y + 1,
311                ..*self
312            });
313        }
314
315        positions
316    }
317
318    /// Distance between two positions
319    pub fn distance(&self, other: Self) -> u32 {
320        let dx = self.x as i32 - other.x as i32;
321        let dy = self.y as i32 - other.y as i32;
322        (dx * dx + dy * dy).sqrt() as u32
323    }
324}
325
326/// Cardinal directions iterated in clockwise order.
327#[derive(
328    Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Hash, Deserialize, Serialize, EnumIter,
329)]
330pub enum Direction {
331    Up,
332    Right,
333    Down,
334    Left,
335}
336
337impl Direction {
338    /// Opposite of current direction.
339    pub fn opposite(&self) -> Self {
340        match self {
341            Self::Left => Self::Right,
342            Self::Right => Self::Left,
343            Self::Up => Self::Down,
344            Self::Down => Self::Up,
345        }
346    }
347}
348
349/// Connection from one room to another.
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
351pub struct Connection {
352    /// ID of the connected to room.
353    pub id: u32,
354    /// Direction the connected to room is in.
355    pub direction: Direction,
356    /// Top-left position of the door connecting the room.
357    pub door: Position,
358}
359
360/// Stairs leading to a different floor.
361#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
362pub struct Stair {
363    /// ID of stairs.
364    pub id: u32,
365    /// Tile position of stairs.
366    pub position: Position,
367    /// Leads to next floor if true, leads to previous floor if false.
368    pub downwards: bool,
369    /// ID of stairs in other floor these stairs are connected to.
370    pub connected: u32,
371}
372
373/// Teleporter providing instant transportation to another on the same floor.
374#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
375pub struct Teleporter {
376    /// ID of teleporter.
377    pub id: u32,
378    /// Tile position of teleporter.
379    pub position: Position,
380    /// ID of teleporter that this teleporter is linked to.
381    pub connected: u32,
382}
383
384/// An item that can be used to equipped by the player.
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
386pub struct Item {
387    /// ID of item instance.
388    pub id: u32,
389    /// Tile position of item.
390    pub position: Position,
391    /// Rarity value of item. This value can be used to decide which item is placed at this location.
392    pub rarity: u32,
393}
394
395/// An enemy that moves toward and attacks the player.
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
397pub struct Enemy {
398    /// ID of enemy instance.
399    pub id: u32,
400    /// Tile position of enemy.
401    pub position: Position,
402    /// Difficulty value of enemy. This value can be used to decide which enemy is placed at this location.
403    pub difficulty: u32,
404}
405
406/// A trap that is hidden from the player and creates a negative effect when stepped on.
407#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
408pub struct Trap {
409    /// ID of trap instance.
410    pub id: u32,
411    /// Tile position of trap.
412    pub position: Position,
413    /// Difficulty value of trap. This value can be used to decide which trap is placed at this location.
414    pub difficulty: u32,
415}
416
417/// Type of room.
418#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, EnumIter)]
419pub enum RoomKind {
420    /// Generated first using binary space partitioning.
421    Main,
422    /// Main rooms are connected by a series of corridors.
423    Corridor,
424}
425
426/// A traversable room in the dungeon.
427#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
428pub struct Room {
429    /// ID of room.
430    pub id: u32,
431    /// Type of room (Main or Corridor).
432    pub kind: RoomKind,
433    /// Floor number (indexed by zero) this room is on.
434    pub floor: u32,
435    /// Position of the top-left tile of the room.
436    pub position: Position,
437    /// Width (in tiles) of room.
438    pub width: u32,
439    /// Height (in tiles) of room.
440    pub height: u32,
441    /// Each room is connected to at least one other room by a door.
442    pub connections: Vec<Connection>,
443    /// Difficulty value of the room. Based on how far the room is from the starting room.
444    pub difficulty: f32,
445    /// List of stairs in the room.
446    pub stairs: Vec<Stair>,
447    /// List of teleporters in the room.
448    pub teleporters: Vec<Teleporter>,
449    /// List of items in the room.
450    pub items: Vec<Item>,
451    /// List of enemies in the room.
452    pub enemies: Vec<Enemy>,
453    /// List of traps in the room.
454    pub traps: Vec<Trap>,
455    /// Dimensions in tiles for walls.
456    pub wall_dimensions: (u32, u32),
457    /// Offset in tiles for walls.
458    pub wall_offset: (i32, i32),
459}
460
461impl Room {
462    /// Center tile of the room (picks top-left if even number of tiles).
463    pub fn center(&self) -> Position {
464        Position {
465            x: self.position.x + self.width / 2,
466            y: self.position.y + self.height / 2,
467        }
468    }
469
470    /// Iterator over all floor tile positions in the room.
471    pub fn positions(&self) -> impl Iterator<Item = Position> + '_ {
472        let left_wall =
473            (self.wall_dimensions.0 as f32 / 2.0 + self.wall_offset.0 as f32).floor() as u32;
474        let right_wall =
475            (self.wall_dimensions.0 as f32 / 2.0 - self.wall_offset.0 as f32).ceil() as u32;
476        let top_wall =
477            (self.wall_dimensions.1 as f32 / 2.0 + self.wall_offset.1 as f32).floor() as u32;
478        let bottom_wall =
479            (self.wall_dimensions.1 as f32 / 2.0 - self.wall_offset.1 as f32).ceil() as u32;
480
481        (left_wall..self.width - right_wall).flat_map(move |i| {
482            (top_wall..self.height - bottom_wall).map(move |j| Position {
483                x: self.position.x + i,
484                y: self.position.y + j,
485            })
486        })
487    }
488
489    /// Iterator over all empty floor tile positions in the room.
490    pub fn empty_positions(&self) -> impl Iterator<Item = Position> + '_ {
491        self.positions().filter(|position| {
492            !self.stairs.iter().any(|stair| stair.position == *position)
493                && !self
494                    .teleporters
495                    .iter()
496                    .any(|teleporter| teleporter.position == *position)
497                && !self.items.iter().any(|item| item.position == *position)
498                && !self.enemies.iter().any(|enemy| enemy.position == *position)
499                && !self.traps.iter().any(|trap| trap.position == *position)
500        })
501    }
502
503    fn from_cell_room(
504        params: &GenerateDungeonParams,
505        room: &CellRoom,
506        floor: u32,
507        rooms: &FxHashMap<u32, CellRoom>,
508        kept: &FxHashSet<u32>,
509        doors: &FxHashMap<(u32, u32), Position>,
510    ) -> Self {
511        Self {
512            id: room.id,
513            kind: room.kind,
514            floor,
515            position: Position {
516                x: room.cell.i * params.tiles_per_cell,
517                y: room.cell.j * params.tiles_per_cell,
518            },
519            width: room.width * params.tiles_per_cell,
520            height: room.height * params.tiles_per_cell,
521            connections: room
522                .connections
523                .iter()
524                .filter(|connection| {
525                    rooms.get(&connection.id).unwrap().kind == RoomKind::Main
526                        || kept.contains(&connection.id)
527                })
528                .map(|connection| Connection {
529                    id: connection.id,
530                    direction: connection.direction,
531                    door: doors[&(room.id, connection.id)],
532                })
533                .collect(),
534            difficulty: 0.0,
535            stairs: Vec::new(),
536            teleporters: Vec::new(),
537            items: Vec::new(),
538            enemies: Vec::new(),
539            traps: Vec::new(),
540            wall_dimensions: params.wall_dimensions,
541            wall_offset: params.wall_offset,
542        }
543    }
544}
545
546/// A single tile in the dungeon.
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
548pub enum Tile {
549    /// Traversable.
550    Floor,
551    /// Non-traversable.
552    Wall,
553}
554
555/// A floor of the dungeon.
556#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
557pub struct Floor {
558    /// The index of this floor, starting at zero for the top floor.
559    pub number: u32,
560    /// The list of tiles in the floor.
561    pub tiles: Vec<Vec<Tile>>,
562    /// The list of rooms in the floor.
563    pub rooms: Vec<Room>,
564}
565
566impl Floor {
567    /// Get the tile at the position.
568    pub fn tile_at(&self, position: Position) -> Tile {
569        self.tiles[position.x as usize][position.y as usize]
570    }
571}
572
573/// A procedurally generated dungeon, complete with rooms, items and enemies.
574#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
575pub struct Dungeon {
576    /// Parameters used to generate the dungeon.
577    pub params: GenerateDungeonParams,
578    /// A list of floors in the dungeon.
579    pub floors: Vec<Floor>,
580    /// The ID of the room the player starts in.
581    pub starting_room_id: u32,
582    /// The initial tile position of the player.
583    pub player_position: Position,
584}
585
586/// An issue run into during dungeon generation.
587#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
588pub enum TatamiError {
589    /// Dungeons must contain at least one floor.
590    NoFloors,
591    /// Floors must contain at least two rooms.
592    NoRooms,
593    /// Walls cannot have widths or heights of zero.
594    NoWalls,
595    /// Rooms cannot have widths or heights of zero.
596    InvalidRoomDimensions,
597    /// Neighboring rooms must have at least one shared cell.
598    NoSharedCells,
599    /// The minimum value for a parameter was set to more than the maximum value.
600    MinGreaterThanMax,
601    /// Dimensions of dungeon were insufficient for room partition size. Increase dimensions or decrease either max_rooms_per_floor or max_room_dimensions.
602    TooSmall,
603}
604
605impl Dungeon {
606    /// Generate a dungeon with a random seed and default parameters.
607    pub fn generate() -> Result<Self, TatamiError> {
608        let mut rng = thread_rng();
609        let mut seed = [0u8; 32];
610        rng.fill(&mut seed);
611
612        Self::generate_with_seed(seed)
613    }
614
615    /// Generate a dungeon with default parameters.
616    pub fn generate_with_seed(seed: [u8; 32]) -> Result<Self, TatamiError> {
617        Self::generate_with_seed_and_params(seed, GenerateDungeonParams::default())
618    }
619
620    /// Generate a dungeon with a random seed.
621    pub fn generate_with_params(params: GenerateDungeonParams) -> Result<Self, TatamiError> {
622        let mut rng = thread_rng();
623        let mut seed = [0u8; 32];
624        rng.fill(&mut seed);
625
626        Self::generate_with_seed_and_params(seed, params)
627    }
628
629    /// Generate a dungeon with the provided seed and parameters.
630    pub fn generate_with_seed_and_params(
631        seed: [u8; 32],
632        params: GenerateDungeonParams,
633    ) -> Result<Self, TatamiError> {
634        if params.num_floors == 0 {
635            return Err(TatamiError::NoFloors);
636        }
637
638        if params.min_rooms_per_floor <= 1 || params.max_rooms_per_floor <= 1 {
639            return Err(TatamiError::NoRooms);
640        }
641
642        if params.wall_dimensions.0 == 0 || params.wall_dimensions.1 == 0 {
643            return Err(TatamiError::NoWalls);
644        }
645
646        if params.min_shared_cells.0 == 0 || params.min_shared_cells.1 == 0 {
647            return Err(TatamiError::NoSharedCells);
648        }
649
650        if params.min_room_dimensions.0 == 0 || params.min_room_dimensions.1 == 0 {
651            return Err(TatamiError::InvalidRoomDimensions);
652        }
653
654        if params.max_room_dimensions.0 == 0 || params.max_room_dimensions.1 == 0 {
655            return Err(TatamiError::InvalidRoomDimensions);
656        }
657
658        if params.min_corridor_dimensions.0 == 0 || params.min_corridor_dimensions.1 == 0 {
659            return Err(TatamiError::InvalidRoomDimensions);
660        }
661
662        if params.max_corridor_dimensions.0 == 0 || params.max_corridor_dimensions.1 == 0 {
663            return Err(TatamiError::InvalidRoomDimensions);
664        }
665
666        if params.min_rooms_per_floor > params.max_rooms_per_floor {
667            return Err(TatamiError::MinGreaterThanMax);
668        }
669
670        if params.min_room_dimensions.0 > params.max_room_dimensions.0 {
671            return Err(TatamiError::MinGreaterThanMax);
672        }
673
674        if params.min_room_dimensions.1 > params.max_room_dimensions.1 {
675            return Err(TatamiError::MinGreaterThanMax);
676        }
677
678        if params.min_corridor_dimensions.0 > params.max_corridor_dimensions.0 {
679            return Err(TatamiError::MinGreaterThanMax);
680        }
681
682        if params.min_corridor_dimensions.1 > params.max_corridor_dimensions.1 {
683            return Err(TatamiError::MinGreaterThanMax);
684        }
685
686        if params.min_stairs_per_floor > params.max_stairs_per_floor {
687            return Err(TatamiError::MinGreaterThanMax);
688        }
689
690        if params.min_teleporters_per_floor > params.max_teleporters_per_floor {
691            return Err(TatamiError::MinGreaterThanMax);
692        }
693
694        if params.min_items_per_room > params.max_items_per_room {
695            return Err(TatamiError::MinGreaterThanMax);
696        }
697
698        if params.min_enemies_per_room > params.max_enemies_per_room {
699            return Err(TatamiError::MinGreaterThanMax);
700        }
701
702        if params.min_traps_per_room > params.max_traps_per_room {
703            return Err(TatamiError::MinGreaterThanMax);
704        }
705
706        let (width, height) = params.dimensions;
707
708        let depth = (params.max_rooms_per_floor as f32).log(2.0).ceil() as u32;
709        let partitions = 2_u32.pow((depth + 1) / 2);
710
711        let partition_width = if params.adjacent_rooms_allowed {
712            width / partitions
713        } else {
714            (width / partitions).checked_sub(2).unwrap_or(0)
715        };
716        let partition_height = if params.adjacent_rooms_allowed {
717            height / partitions
718        } else {
719            (height / partitions).checked_sub(2).unwrap_or(0)
720        };
721
722        if params.max_room_dimensions.0 > partition_width
723            || params.max_room_dimensions.1 > partition_height
724        {
725            return Err(TatamiError::TooSmall);
726        }
727
728        let mut rng = ChaCha20Rng::from_seed(seed);
729
730        let mut floors: Vec<Floor> = Vec::with_capacity(params.num_floors as usize);
731        let mut prev_stairs = Vec::new();
732        let mut room_id = 0;
733        let mut teleporter_id = 0;
734        for i in 0..params.num_floors {
735            let floor = generate_floor(&mut rng, i, params, &prev_stairs, room_id, teleporter_id);
736            prev_stairs = floor
737                .rooms
738                .iter()
739                .flat_map(|room| room.stairs.iter().filter(|stair| stair.downwards).copied())
740                .collect();
741
742            if let Some(last_floor) = floors.last_mut() {
743                let upward_stairs = floor
744                    .rooms
745                    .iter()
746                    .flat_map(|room| room.stairs.iter().filter(|stair| !stair.downwards).copied());
747                for stair in upward_stairs {
748                    let last_stair = last_floor
749                        .rooms
750                        .iter_mut()
751                        .find_map(|room| {
752                            room.stairs
753                                .iter_mut()
754                                .find(|other| other.position == stair.position)
755                        })
756                        .unwrap();
757                    last_stair.connected = stair.id;
758                }
759            }
760
761            room_id += width * height;
762            teleporter_id += floor
763                .rooms
764                .iter()
765                .map(|room| room.teleporters.len() as u32)
766                .sum::<u32>();
767
768            floors.push(floor);
769        }
770
771        let starting_room = floors[0]
772            .rooms
773            .iter()
774            .filter(|room| room.kind == RoomKind::Main)
775            .choose(&mut rng)
776            .unwrap();
777        let starting_room_id = starting_room.id;
778        let player_position = starting_room.center();
779
780        let item_density_map = Perlin::new(rng.gen());
781        let item_rarity_map = Perlin::new(rng.gen());
782        let enemy_density_map = Perlin::new(rng.gen());
783        let enemy_difficulty_map = Perlin::new(rng.gen());
784        let trap_density_map = Perlin::new(rng.gen());
785        let trap_difficulty_map = Perlin::new(rng.gen());
786
787        let rooms = FxHashMap::from_iter(
788            floors
789                .iter()
790                .flat_map(|floor| floor.rooms.clone())
791                .map(|room| (room.id, room)),
792        );
793        let costs = FxHashMap::from_iter(
794            rooms
795                .par_iter()
796                .filter_map(
797                    |(id, room)| match pathfind_rooms(starting_room_id, room.id, &rooms) {
798                        Some((_, cost)) => Some((*id, cost)),
799                        None => None,
800                    },
801                )
802                .collect::<Vec<(u32, u32)>>(),
803        );
804        let max_cost = costs.values().copied().max().unwrap();
805        let max_area = rooms
806            .values()
807            .map(|room| room.width * room.height)
808            .max()
809            .unwrap();
810
811        let rng = Arc::new(Mutex::new(rng));
812        floors.par_iter_mut().for_each(|floor| {
813            floor.rooms.par_iter_mut().for_each(|room| {
814                let mut rng = rng.lock().unwrap();
815                populate_room(
816                    room,
817                    &mut *rng,
818                    &params,
819                    &costs,
820                    max_cost,
821                    max_area,
822                    &item_density_map,
823                    &item_rarity_map,
824                    &enemy_density_map,
825                    &enemy_difficulty_map,
826                    &trap_density_map,
827                    &trap_difficulty_map,
828                );
829            })
830        });
831
832        Ok(Self {
833            params,
834            floors,
835            starting_room_id,
836            player_position,
837        })
838    }
839
840    /// Output an image representation of all floors in the dungeon.
841    pub fn output_as_image<P: AsRef<Path> + Clone + Sync>(
842        &self,
843        path: P,
844        spritesheet: P,
845        tile_size: u32,
846    ) -> Result<(), ImageError> {
847        let player_image = open(spritesheet.clone())
848            .unwrap()
849            .into_rgb8()
850            .view(tile_size * 2, tile_size * 4, tile_size, tile_size)
851            .to_image();
852
853        let (width, height) = self.params.dimensions;
854        let cols = (self.floors.len() as f32).sqrt().ceil() as u32;
855        let rows = (self.floors.len() as f32 / cols as f32).ceil() as u32;
856        let width = (width * self.params.tiles_per_cell) as u32;
857        let height = (height * self.params.tiles_per_cell) as u32;
858        let padding = tile_size * 8;
859        let mut img = RgbImage::from_pixel(
860            cols * width * tile_size + padding * (cols - 1),
861            rows * height * tile_size + padding * (rows - 1),
862            Rgb([255, 255, 255]),
863        );
864
865        let images: Vec<_> = (0..self.floors.len())
866            .collect::<Vec<_>>()
867            .par_iter()
868            .map(|i| {
869                let col = *i as u32 % cols;
870                let row = *i as u32 / cols;
871                let floor_image = self.floor_to_image(*i, &spritesheet, tile_size);
872                (col, row, floor_image)
873            })
874            .collect();
875
876        for (col, row, floor_image) in images {
877            overlay(
878                &mut img,
879                &floor_image,
880                ((col * width) * tile_size) as i64 + (col * padding) as i64,
881                ((row * height) * tile_size) as i64 + (row * padding) as i64,
882            );
883        }
884
885        overlay(
886            &mut img,
887            &player_image,
888            (self.player_position.x * tile_size) as i64,
889            (self.player_position.y * tile_size) as i64,
890        );
891
892        img.save(path)?;
893
894        Ok(())
895    }
896
897    /// Output an image representation of the given floor.
898    pub fn output_floor_as_image<P: AsRef<Path>>(
899        &self,
900        floor_number: u32,
901        path: P,
902        spritesheet: P,
903        tile_size: u32,
904    ) -> Result<(), ImageError> {
905        let img = self.floor_to_image(floor_number as usize, spritesheet, tile_size);
906
907        img.save(path)?;
908
909        Ok(())
910    }
911
912    // Create an image from floor data
913    fn floor_to_image<P: AsRef<Path>>(
914        &self,
915        index: usize,
916        spritesheet: P,
917        tile_size: u32,
918    ) -> RgbImage {
919        let floor = &self.floors[index];
920        let (width, height) = self.params.dimensions;
921
922        let spritesheet = open(spritesheet).unwrap().into_rgb8();
923        let floor_tile_image = spritesheet.view(0, 0, tile_size, tile_size).to_image();
924        let wall_tile_image = spritesheet
925            .view(tile_size, 0, tile_size, tile_size)
926            .to_image();
927        let stairs_down_image = spritesheet
928            .view(tile_size * 2, 0, tile_size, tile_size)
929            .to_image();
930        let stairs_up_image = spritesheet
931            .view(tile_size * 3, 0, tile_size, tile_size)
932            .to_image();
933        let teleporter_image = spritesheet
934            .view(tile_size * 4, 0, tile_size, tile_size)
935            .to_image();
936        let common_item_image = spritesheet
937            .view(0, tile_size, tile_size, tile_size)
938            .to_image();
939        let uncommon_item_image = spritesheet
940            .view(tile_size, tile_size, tile_size, tile_size)
941            .to_image();
942        let rare_item_image = spritesheet
943            .view(tile_size * 2, tile_size, tile_size, tile_size)
944            .to_image();
945        let epic_item_image = spritesheet
946            .view(tile_size * 3, tile_size, tile_size, tile_size)
947            .to_image();
948        let legendary_item_image = spritesheet
949            .view(tile_size * 4, tile_size, tile_size, tile_size)
950            .to_image();
951        let common_enemy_image = spritesheet
952            .view(0, tile_size * 2, tile_size, tile_size)
953            .to_image();
954        let uncommon_enemy_image = spritesheet
955            .view(tile_size, tile_size * 2, tile_size, tile_size)
956            .to_image();
957        let rare_enemy_image = spritesheet
958            .view(tile_size * 2, tile_size * 2, tile_size, tile_size)
959            .to_image();
960        let epic_enemy_image = spritesheet
961            .view(tile_size * 3, tile_size * 2, tile_size, tile_size)
962            .to_image();
963        let legendary_enemy_image = spritesheet
964            .view(tile_size * 4, tile_size * 2, tile_size, tile_size)
965            .to_image();
966        let common_trap_image = spritesheet
967            .view(0, tile_size * 3, tile_size, tile_size)
968            .to_image();
969        let uncommon_trap_image = spritesheet
970            .view(tile_size, tile_size * 3, tile_size, tile_size)
971            .to_image();
972        let rare_trap_image = spritesheet
973            .view(tile_size * 2, tile_size * 3, tile_size, tile_size)
974            .to_image();
975        let epic_trap_image = spritesheet
976            .view(tile_size * 3, tile_size * 3, tile_size, tile_size)
977            .to_image();
978        let legendary_trap_image = spritesheet
979            .view(tile_size * 4, tile_size * 3, tile_size, tile_size)
980            .to_image();
981        let door_image = spritesheet
982            .view(0, tile_size * 4, tile_size * 2, tile_size * 2)
983            .to_image();
984
985        let width = width * self.params.tiles_per_cell;
986        let height = height * self.params.tiles_per_cell;
987        let mut img = RgbImage::new(width as u32 * tile_size, height as u32 * tile_size);
988
989        for x in 0..width {
990            for y in 0..height {
991                match floor.tiles[x as usize][y as usize] {
992                    Tile::Floor => overlay(
993                        &mut img,
994                        &floor_tile_image,
995                        (x * tile_size) as i64,
996                        (y * tile_size) as i64,
997                    ),
998                    Tile::Wall => overlay(
999                        &mut img,
1000                        &wall_tile_image,
1001                        (x * tile_size) as i64,
1002                        (y * tile_size) as i64,
1003                    ),
1004                }
1005            }
1006        }
1007
1008        for room in &floor.rooms {
1009            for connection in &room.connections {
1010                overlay(
1011                    &mut img,
1012                    &door_image,
1013                    (connection.door.x * tile_size) as i64,
1014                    (connection.door.y * tile_size) as i64,
1015                )
1016            }
1017
1018            for stair in &room.stairs {
1019                if stair.downwards {
1020                    overlay(
1021                        &mut img,
1022                        &stairs_down_image,
1023                        (stair.position.x * tile_size) as i64,
1024                        (stair.position.y * tile_size) as i64,
1025                    );
1026                } else {
1027                    overlay(
1028                        &mut img,
1029                        &stairs_up_image,
1030                        (stair.position.x * tile_size) as i64,
1031                        (stair.position.y * tile_size) as i64,
1032                    );
1033                }
1034            }
1035
1036            for teleporter in &room.teleporters {
1037                overlay(
1038                    &mut img,
1039                    &teleporter_image,
1040                    (teleporter.position.x * tile_size) as i64,
1041                    (teleporter.position.y * tile_size) as i64,
1042                );
1043            }
1044
1045            for item in &room.items {
1046                let common_rarity = self.params.max_item_rarity / 5;
1047                let uncommon_rarity = self.params.max_item_rarity * 2 / 5;
1048                let rare_rarity = self.params.max_item_rarity * 3 / 5;
1049                let epic_rarity = self.params.max_item_rarity * 4 / 5;
1050
1051                if item.rarity <= common_rarity {
1052                    overlay(
1053                        &mut img,
1054                        &common_item_image,
1055                        (item.position.x * tile_size) as i64,
1056                        (item.position.y * tile_size) as i64,
1057                    );
1058                } else if item.rarity <= uncommon_rarity {
1059                    overlay(
1060                        &mut img,
1061                        &uncommon_item_image,
1062                        (item.position.x * tile_size) as i64,
1063                        (item.position.y * tile_size) as i64,
1064                    );
1065                } else if item.rarity <= rare_rarity {
1066                    overlay(
1067                        &mut img,
1068                        &rare_item_image,
1069                        (item.position.x * tile_size) as i64,
1070                        (item.position.y * tile_size) as i64,
1071                    );
1072                } else if item.rarity <= epic_rarity {
1073                    overlay(
1074                        &mut img,
1075                        &epic_item_image,
1076                        (item.position.x * tile_size) as i64,
1077                        (item.position.y * tile_size) as i64,
1078                    );
1079                } else {
1080                    overlay(
1081                        &mut img,
1082                        &legendary_item_image,
1083                        (item.position.x * tile_size) as i64,
1084                        (item.position.y * tile_size) as i64,
1085                    );
1086                }
1087            }
1088
1089            for enemy in &room.enemies {
1090                let common_difficulty = self.params.max_enemy_difficulty / 5;
1091                let uncommon_difficulty = self.params.max_enemy_difficulty * 2 / 5;
1092                let rare_difficulty = self.params.max_enemy_difficulty * 3 / 5;
1093                let epic_difficulty = self.params.max_enemy_difficulty * 4 / 5;
1094
1095                if enemy.difficulty <= common_difficulty {
1096                    overlay(
1097                        &mut img,
1098                        &common_enemy_image,
1099                        (enemy.position.x * tile_size) as i64,
1100                        (enemy.position.y * tile_size) as i64,
1101                    );
1102                } else if enemy.difficulty <= uncommon_difficulty {
1103                    overlay(
1104                        &mut img,
1105                        &uncommon_enemy_image,
1106                        (enemy.position.x * tile_size) as i64,
1107                        (enemy.position.y * tile_size) as i64,
1108                    );
1109                } else if enemy.difficulty <= rare_difficulty {
1110                    overlay(
1111                        &mut img,
1112                        &rare_enemy_image,
1113                        (enemy.position.x * tile_size) as i64,
1114                        (enemy.position.y * tile_size) as i64,
1115                    );
1116                } else if enemy.difficulty <= epic_difficulty {
1117                    overlay(
1118                        &mut img,
1119                        &epic_enemy_image,
1120                        (enemy.position.x * tile_size) as i64,
1121                        (enemy.position.y * tile_size) as i64,
1122                    );
1123                } else {
1124                    overlay(
1125                        &mut img,
1126                        &legendary_enemy_image,
1127                        (enemy.position.x * tile_size) as i64,
1128                        (enemy.position.y * tile_size) as i64,
1129                    );
1130                }
1131            }
1132
1133            for trap in &room.traps {
1134                let common_difficulty = self.params.max_trap_difficulty / 5;
1135                let uncommon_difficulty = self.params.max_trap_difficulty * 2 / 5;
1136                let rare_difficulty = self.params.max_trap_difficulty * 3 / 5;
1137                let epic_difficulty = self.params.max_trap_difficulty * 4 / 5;
1138
1139                if trap.difficulty <= common_difficulty {
1140                    overlay(
1141                        &mut img,
1142                        &common_trap_image,
1143                        (trap.position.x * tile_size) as i64,
1144                        (trap.position.y * tile_size) as i64,
1145                    );
1146                } else if trap.difficulty <= uncommon_difficulty {
1147                    overlay(
1148                        &mut img,
1149                        &uncommon_trap_image,
1150                        (trap.position.x * tile_size) as i64,
1151                        (trap.position.y * tile_size) as i64,
1152                    );
1153                } else if trap.difficulty <= rare_difficulty {
1154                    overlay(
1155                        &mut img,
1156                        &rare_trap_image,
1157                        (trap.position.x * tile_size) as i64,
1158                        (trap.position.y * tile_size) as i64,
1159                    );
1160                } else if trap.difficulty <= epic_difficulty {
1161                    overlay(
1162                        &mut img,
1163                        &epic_trap_image,
1164                        (trap.position.x * tile_size) as i64,
1165                        (trap.position.y * tile_size) as i64,
1166                    );
1167                } else {
1168                    overlay(
1169                        &mut img,
1170                        &legendary_trap_image,
1171                        (trap.position.x * tile_size) as i64,
1172                        (trap.position.y * tile_size) as i64,
1173                    );
1174                }
1175            }
1176        }
1177
1178        img
1179    }
1180}
1181
1182// Generate rooms for a floor in the dungeon, then add stairs and teleporters
1183fn generate_floor<R: Rng>(
1184    rng: &mut R,
1185    floor_number: u32,
1186    params: GenerateDungeonParams,
1187    prev_stairs: &[Stair],
1188    room_id: u32,
1189    teleporter_id: u32,
1190) -> Floor {
1191    let (width, height) = params.dimensions;
1192    let prev_stair_cells: Box<[Cell]> = prev_stairs
1193        .iter()
1194        .map(|stair| Cell {
1195            i: stair.position.x / params.tiles_per_cell,
1196            j: stair.position.y / params.tiles_per_cell,
1197        })
1198        .collect();
1199
1200    loop {
1201        let mut grid = Vec::with_capacity(width as usize);
1202        for i in 0..width {
1203            grid.push(Vec::with_capacity(height as usize));
1204            for _ in 0..height {
1205                grid[i as usize].push(false);
1206            }
1207        }
1208
1209        let num_rooms = rng.gen_range(params.min_rooms_per_floor..=params.max_rooms_per_floor);
1210        let mut rooms = generate_main_rooms(
1211            &mut grid,
1212            rng,
1213            width,
1214            height,
1215            room_id,
1216            num_rooms,
1217            params.min_room_dimensions,
1218            params.max_room_dimensions,
1219            params.adjacent_rooms_allowed,
1220        );
1221        rooms.extend(generate_corridors(
1222            &mut grid,
1223            rng,
1224            room_id + rooms.len() as u32,
1225            width,
1226            height,
1227            params.min_corridor_dimensions,
1228            params.max_corridor_dimensions,
1229            params.squareness,
1230        ));
1231
1232        let mut kept = None;
1233        for _ in 0..2 {
1234            connect_corridors(rng, &mut rooms, params.min_shared_cells);
1235            if let Some(kept_) = connect_rooms(rng, &rooms, &prev_stair_cells) {
1236                kept = Some(kept_);
1237                break;
1238            }
1239        }
1240
1241        if let Some(kept) = kept {
1242            let (tiles, doors) = generate_tiles(
1243                &rooms,
1244                &kept,
1245                width,
1246                height,
1247                params.tiles_per_cell,
1248                params.wall_dimensions,
1249                params.wall_offset,
1250            );
1251
1252            let mut rooms: Vec<_> = rooms
1253                .values()
1254                .filter(|room| room.kind == RoomKind::Main || kept.contains(&room.id))
1255                .map(|room| {
1256                    Room::from_cell_room(&params, room, floor_number, &rooms, &kept, &doors)
1257                })
1258                .collect();
1259
1260            let last_stair_id = prev_stairs.iter().map(|stair| stair.id).max().unwrap_or(0);
1261            for (i, stair) in prev_stairs.iter().enumerate() {
1262                for room in &mut rooms {
1263                    if room.positions().contains(&stair.position) {
1264                        room.stairs.push(Stair {
1265                            id: last_stair_id + i as u32,
1266                            position: stair.position,
1267                            downwards: false,
1268                            connected: stair.id,
1269                        });
1270                        break;
1271                    }
1272                }
1273            }
1274            let last_stair_id = last_stair_id + prev_stairs.len() as u32;
1275
1276            let num_stairs =
1277                rng.gen_range(params.min_stairs_per_floor..=params.max_stairs_per_floor);
1278            for i in 0..num_stairs {
1279                let room = rooms.choose_mut(rng).unwrap();
1280                let position = room.empty_positions().choose(rng).unwrap();
1281                room.stairs.push(Stair {
1282                    id: last_stair_id + i,
1283                    position,
1284                    downwards: true,
1285                    connected: 0,
1286                });
1287            }
1288
1289            let num_teleporters =
1290                rng.gen_range(params.min_teleporters_per_floor..=params.max_teleporters_per_floor);
1291            for i in 0..num_teleporters {
1292                let id1 = teleporter_id + i * 2;
1293                let id2 = teleporter_id + i * 2 + 1;
1294
1295                for (id, connected) in [(id1, id2), (id2, id1)] {
1296                    let room = rooms.choose_mut(rng).unwrap();
1297                    let position = room.empty_positions().choose(rng).unwrap();
1298                    room.teleporters.push(Teleporter {
1299                        id,
1300                        position,
1301                        connected,
1302                    });
1303                }
1304            }
1305
1306            return Floor {
1307                number: floor_number,
1308                tiles,
1309                rooms,
1310            };
1311        }
1312    }
1313}
1314
1315// Populate the room with items, enemies and traps based on noise maps and distance from starting room
1316fn populate_room<R: Rng>(
1317    room: &mut Room,
1318    rng: &mut R,
1319    params: &GenerateDungeonParams,
1320    costs: &FxHashMap<u32, u32>,
1321    max_cost: u32,
1322    max_area: u32,
1323    item_density_map: &Perlin,
1324    item_rarity_map: &Perlin,
1325    enemy_density_map: &Perlin,
1326    enemy_difficulty_map: &Perlin,
1327    trap_density_map: &Perlin,
1328    trap_difficulty_map: &Perlin,
1329) {
1330    let (width, height) = params.dimensions;
1331
1332    let cost = match costs.get(&room.id) {
1333        Some(cost) => *cost,
1334        None => return,
1335    };
1336    room.difficulty = 1.0 / params.difficulty_ratio
1337        + cost as f32 / max_cost as f32 * (1.0 - 1.0 / params.difficulty_ratio);
1338
1339    let density_ratio = (room.width * room.height) as f32 / max_area as f32;
1340
1341    let min_items = params.min_items_per_room;
1342    let max_items =
1343        min_items + ((params.max_items_per_room - min_items) as f32 * density_ratio) as u32;
1344    let item_density_noise = get_room_noise(room, item_density_map, width, height);
1345    let num_items = if min_items == max_items {
1346        max_items
1347    } else {
1348        weighted_random(
1349            rng,
1350            min_items,
1351            max_items,
1352            item_density_noise,
1353            params.item_density_scale,
1354        )
1355    };
1356
1357    for _ in 0..num_items {
1358        let position = room.empty_positions().choose(rng).unwrap();
1359
1360        let item_rarity_noise = get_room_noise(room, item_rarity_map, width, height);
1361        let rarity = weighted_random(
1362            rng,
1363            1,
1364            params.max_item_rarity,
1365            item_rarity_noise,
1366            params.item_rarity_scale * (1.0 - room.difficulty),
1367        );
1368
1369        room.items.push(Item {
1370            id: rng.gen(),
1371            position,
1372            rarity,
1373        });
1374    }
1375
1376    let min_enemies = params.min_enemies_per_room;
1377    let max_enemies =
1378        min_enemies + ((params.max_enemies_per_room - min_enemies) as f32 * density_ratio) as u32;
1379    let enemy_density_noise = get_room_noise(room, enemy_density_map, width, height);
1380    let num_enemies = if min_enemies == max_enemies {
1381        max_enemies
1382    } else {
1383        weighted_random(
1384            rng,
1385            min_enemies,
1386            max_enemies,
1387            enemy_density_noise,
1388            params.enemy_density_scale,
1389        )
1390    };
1391
1392    for _ in 0..num_enemies {
1393        let position = room.empty_positions().choose(rng).unwrap();
1394
1395        let enemy_difficulty_noise = get_room_noise(room, &enemy_difficulty_map, width, height);
1396        let difficulty = weighted_random(
1397            rng,
1398            1,
1399            params.max_enemy_difficulty,
1400            enemy_difficulty_noise,
1401            params.enemy_difficulty_scale * (1.0 - room.difficulty),
1402        );
1403
1404        room.enemies.push(Enemy {
1405            id: rng.gen(),
1406            position,
1407            difficulty,
1408        });
1409    }
1410
1411    let min_traps = params.min_traps_per_room;
1412    let max_traps =
1413        min_traps + ((params.max_traps_per_room - min_traps) as f32 * density_ratio) as u32;
1414    let trap_density_noise = get_room_noise(room, trap_density_map, width, height);
1415    let num_traps = if min_traps == max_traps {
1416        max_traps
1417    } else {
1418        weighted_random(
1419            rng,
1420            min_traps,
1421            max_traps,
1422            trap_density_noise,
1423            params.trap_density_scale,
1424        )
1425    };
1426
1427    for _ in 0..num_traps {
1428        let position = room.empty_positions().choose(rng).unwrap();
1429
1430        let trap_difficulty_noise = get_room_noise(room, trap_difficulty_map, width, height);
1431        let difficulty = weighted_random(
1432            rng,
1433            1,
1434            params.max_trap_difficulty,
1435            trap_difficulty_noise,
1436            params.trap_difficulty_scale * (1.0 - room.difficulty),
1437        );
1438
1439        room.traps.push(Trap {
1440            id: rng.gen(),
1441            position,
1442            difficulty,
1443        });
1444    }
1445}
1446
1447// Generate zero to one main rooms for each partition
1448fn generate_main_rooms<R: Rng>(
1449    grid: &mut [Vec<bool>],
1450    rng: &mut R,
1451    width: u32,
1452    height: u32,
1453    room_id: u32,
1454    num_rooms: u32,
1455    min_room_dimensions: (u32, u32),
1456    max_room_dimensions: (u32, u32),
1457    adjacent_rooms_allowed: bool,
1458) -> FxHashMap<u32, CellRoom> {
1459    let split_horizontally = rng.gen();
1460    let depth = (num_rooms as f32).log(2.0).ceil() as u32;
1461    let mut partitions = create_partitions(0, 0, width, height, split_horizontally, depth);
1462    partitions.shuffle(rng);
1463
1464    let rooms = FxHashMap::from_iter(partitions.iter().take(num_rooms as usize).enumerate().map(
1465        |(index, (i, j, width, height))| {
1466            let id = room_id + index as u32;
1467            let room_width = rng.gen_range(min_room_dimensions.0..=max_room_dimensions.0);
1468            let room_height = rng.gen_range(min_room_dimensions.1..=max_room_dimensions.1);
1469
1470            let (i, j) = if adjacent_rooms_allowed {
1471                let i = rng.gen_range(*i..=i + width - room_width);
1472                let j = rng.gen_range(*j..=j + height - room_height);
1473                (i, j)
1474            } else {
1475                let i = rng.gen_range(i + 1..=i + width - 1 - room_width);
1476                let j = rng.gen_range(j + 1..=j + height - 1 - room_height);
1477                (i, j)
1478            };
1479
1480            let room = CellRoom {
1481                id,
1482                kind: RoomKind::Main,
1483                cell: Cell { i, j },
1484                width: room_width,
1485                height: room_height,
1486                connections: Vec::new(),
1487            };
1488            (id, room)
1489        },
1490    ));
1491
1492    for room in rooms.values() {
1493        for x in 0..room.width {
1494            for y in 0..room.height {
1495                let i = (room.cell.i + x) as usize;
1496                let j = (room.cell.j + y) as usize;
1497                grid[i][j] = true;
1498            }
1499        }
1500    }
1501
1502    rooms
1503}
1504
1505// Use binary space partitioning to recursively split the grid into rectangles of equal dimensions
1506fn create_partitions(
1507    i: u32,
1508    j: u32,
1509    width: u32,
1510    height: u32,
1511    split_horizontally: bool,
1512    depth: u32,
1513) -> Vec<(u32, u32, u32, u32)> {
1514    if depth == 0 {
1515        return vec![(i, j, width, height)];
1516    }
1517
1518    if split_horizontally {
1519        [
1520            // Left
1521            create_partitions(i, j, width / 2, height, false, depth - 1),
1522            // Right
1523            create_partitions(i + width / 2, j, width / 2, height, false, depth - 1),
1524        ]
1525        .concat()
1526    } else {
1527        [
1528            // Top
1529            create_partitions(i, j, width, height / 2, true, depth - 1),
1530            // Bottom
1531            create_partitions(i, j + height / 2, width, height / 2, true, depth - 1),
1532        ]
1533        .concat()
1534    }
1535}
1536
1537// Fill the mock-up grid with random rectangles
1538fn generate_corridors<R: Rng>(
1539    grid: &mut [Vec<bool>],
1540    rng: &mut R,
1541    room_id: u32,
1542    width: u32,
1543    height: u32,
1544    min_corridor_dimensions: (u32, u32),
1545    max_corridor_dimensions: (u32, u32),
1546    squareness: f32,
1547) -> FxHashMap<u32, CellRoom> {
1548    let mut corridors = Vec::new();
1549    let mut iter = 0;
1550    loop {
1551        // Every 10 iterations, add new 1x1 squares to random positions in the grid
1552        if iter % 10 == 0 {
1553            let mut empty_cells: Vec<_> = cells(width, height)
1554                .filter(|cell| !grid[cell.i as usize][cell.j as usize])
1555                .collect();
1556
1557            if empty_cells.is_empty() {
1558                return FxHashMap::default();
1559            }
1560
1561            empty_cells.shuffle(rng);
1562
1563            let empty_cells = &empty_cells[0..cmp::max(empty_cells.len() / 8, 1)];
1564
1565            for cell in empty_cells {
1566                grid[cell.i as usize][cell.j as usize] = true;
1567            }
1568
1569            let len = corridors.len() as u32;
1570            corridors.extend(empty_cells.iter().enumerate().map(|(i, cell)| CellRoom {
1571                id: room_id + len + i as u32,
1572                kind: RoomKind::Corridor,
1573                cell: *cell,
1574                width: 1,
1575                height: 1,
1576                connections: Vec::new(),
1577            }))
1578        }
1579
1580        // For each corridor, pick a random direction and grow in it if all adjacent spaces are available
1581        corridors.shuffle(rng);
1582        corridors.iter_mut().for_each(|corridor| {
1583            let weights = if corridor.width > corridor.height {
1584                let n = (corridor.width - corridor.height) as f32 * squareness;
1585                [1.0 + n, 1.0, 1.0 + n, 1.0]
1586            } else if corridor.width < corridor.height {
1587                let n = (corridor.height - corridor.width) as f32 * squareness;
1588                [1.0, 1.0 + n, 1.0, 1.0 + n]
1589            } else {
1590                [1.0, 1.0, 1.0, 1.0]
1591            };
1592            let dist = WeightedIndex::new(&weights).unwrap();
1593            let direction = Direction::iter().nth(dist.sample(rng)).unwrap();
1594
1595            match direction {
1596                Direction::Left => {
1597                    if corridor.cell.i == 0 || corridor.width >= max_corridor_dimensions.0 {
1598                        return;
1599                    }
1600                    if (0..corridor.height)
1601                        .map(|y| Cell {
1602                            i: corridor.cell.i - 1,
1603                            j: corridor.cell.j + y,
1604                        })
1605                        .any(|cell| grid[cell.i as usize][cell.j as usize])
1606                    {
1607                        return;
1608                    }
1609                }
1610                Direction::Right => {
1611                    if corridor.cell.i + corridor.width >= width
1612                        || corridor.width >= max_corridor_dimensions.0
1613                    {
1614                        return;
1615                    }
1616                    if (0..corridor.height)
1617                        .map(|y| Cell {
1618                            i: corridor.cell.i + corridor.width,
1619                            j: corridor.cell.j + y,
1620                        })
1621                        .any(|cell| grid[cell.i as usize][cell.j as usize])
1622                    {
1623                        return;
1624                    }
1625                }
1626                Direction::Up => {
1627                    if corridor.cell.j == 0 || corridor.height >= max_corridor_dimensions.1 {
1628                        return;
1629                    }
1630                    if (0..corridor.width)
1631                        .map(|x| Cell {
1632                            i: corridor.cell.i + x,
1633                            j: corridor.cell.j - 1,
1634                        })
1635                        .any(|cell| grid[cell.i as usize][cell.j as usize])
1636                    {
1637                        return;
1638                    }
1639                }
1640                Direction::Down => {
1641                    if corridor.cell.j + corridor.height >= height
1642                        || corridor.height >= max_corridor_dimensions.1
1643                    {
1644                        return;
1645                    }
1646                    if (0..corridor.width)
1647                        .map(|x| Cell {
1648                            i: corridor.cell.i + x,
1649                            j: corridor.cell.j + corridor.height,
1650                        })
1651                        .any(|cell| grid[cell.i as usize][cell.j as usize])
1652                    {
1653                        return;
1654                    }
1655                }
1656            };
1657
1658            match direction {
1659                Direction::Left => {
1660                    corridor.cell.i -= 1;
1661                    corridor.width += 1;
1662                }
1663                Direction::Right => {
1664                    corridor.width += 1;
1665                }
1666                Direction::Up => {
1667                    corridor.cell.j -= 1;
1668                    corridor.height += 1;
1669                }
1670                Direction::Down => {
1671                    corridor.height += 1;
1672                }
1673            };
1674
1675            corridor.cells().for_each(|cell| {
1676                grid[cell.i as usize][cell.j as usize] = true;
1677            });
1678        });
1679
1680        // If all cells in grid are filled, break out of the loop
1681        if cells(width, height).all(|cell| grid[cell.i as usize][cell.j as usize]) {
1682            break;
1683        }
1684
1685        iter += 1;
1686    }
1687
1688    FxHashMap::from_iter(
1689        corridors
1690            .iter()
1691            .filter(|room| {
1692                room.width >= min_corridor_dimensions.0 && room.height >= min_corridor_dimensions.1
1693            })
1694            .map(|room| (room.id, room.clone())),
1695    )
1696}
1697
1698// Creates connections between all main rooms and corridors
1699fn connect_corridors<R: Rng>(
1700    rng: &mut R,
1701    rooms: &mut FxHashMap<u32, CellRoom>,
1702    min_shared_cells: (usize, usize),
1703) {
1704    let mut to_connect: Vec<_> = rooms.keys().copied().collect();
1705    to_connect.shuffle(rng);
1706
1707    for id in to_connect {
1708        let room = rooms.get(&id).unwrap();
1709        if room.connections.len() >= 3 {
1710            continue;
1711        }
1712
1713        let area = room.height * room.width;
1714        let chance = (area - 1) as f32 / (area * 4) as f32;
1715        let num_connections = if room.connections.len() == 2 {
1716            1
1717        } else if rng.gen::<f32>() < chance {
1718            2
1719        } else {
1720            1
1721        };
1722
1723        let room_connections = room.connections.clone();
1724        let mut connections = neighbors(&room, &rooms, min_shared_cells)
1725            .filter(|connection| {
1726                let connected = rooms.get(&connection.id).unwrap();
1727                !room_connections
1728                    .iter()
1729                    .any(|other| connection.id == other.id)
1730                    && connected.connections.len() < 3
1731            })
1732            .collect::<Vec<_>>();
1733        connections.shuffle(rng);
1734
1735        for connection in connections.iter().take(num_connections) {
1736            let connected = rooms.get_mut(&connection.id).unwrap();
1737            connected.connections.push(CellConnection {
1738                id,
1739                direction: connection.direction.opposite(),
1740            });
1741
1742            let room = rooms.get_mut(&id).unwrap();
1743            room.connections.push(*connection);
1744        }
1745    }
1746}
1747
1748// Each main room pathfinds to another main room through corridors based on their connections. Unused corridors are deleted.
1749fn connect_rooms<R: Rng>(
1750    rng: &mut R,
1751    rooms: &FxHashMap<u32, CellRoom>,
1752    prev_stair_cells: &[Cell],
1753) -> Option<FxHashSet<u32>> {
1754    let main_rooms: FxHashMap<u32, CellRoom> = FxHashMap::from_iter(
1755        rooms
1756            .iter()
1757            .filter(|(_, room)| room.kind == RoomKind::Main)
1758            .map(|(id, room)| (*id, room.clone())),
1759    );
1760
1761    let start = *main_rooms.keys().choose(rng).unwrap();
1762    let mut queue = VecDeque::new();
1763    let mut visited = FxHashSet::default();
1764    visited.reserve(main_rooms.len());
1765    queue.push_back(start);
1766    visited.insert(start);
1767
1768    let mut kept: FxHashSet<u32> = FxHashSet::default();
1769    let mut paths = Vec::with_capacity(main_rooms.len() - 1);
1770    while let Some(id) = queue.pop_front() {
1771        let room = main_rooms.get(&id).unwrap();
1772
1773        let other = main_rooms
1774            .values()
1775            .filter_map(|other| {
1776                if visited.contains(&other.id) {
1777                    None
1778                } else {
1779                    Some((other, cell_room_distance(room, other)))
1780                }
1781            })
1782            .sorted_by(|(_, a_dist), (_, b_dist)| a_dist.cmp(&b_dist))
1783            .take(3)
1784            .choose(rng);
1785
1786        if let Some((other, _)) = other {
1787            if let Some((path, _)) = pathfind_cell_rooms(room.id, other.id, &rooms) {
1788                let path = &path[1..path.len() - 1];
1789                kept.extend(path);
1790                paths.push(path.to_vec());
1791            } else {
1792                return None;
1793            }
1794
1795            queue.push_back(other.id);
1796            visited.insert(other.id);
1797        }
1798    }
1799
1800    // Ensure that rooms for upward stairs are included in the floor.
1801    let stair_rooms: Vec<_> = rooms
1802        .values()
1803        .filter(|room| {
1804            room.kind == RoomKind::Corridor
1805                && !kept.contains(&room.id)
1806                && room.cells().any(|cell| prev_stair_cells.contains(&cell))
1807        })
1808        .collect();
1809    for room in stair_rooms {
1810        let goal = if rng.gen() {
1811            *main_rooms.keys().choose(rng).unwrap()
1812        } else {
1813            *kept.iter().choose(rng).unwrap()
1814        };
1815        if let Some((path, _)) = pathfind_cell_rooms(room.id, goal, &rooms) {
1816            kept.insert(room.id);
1817            if path.len() >= 2 {
1818                let path = &path[1..path.len() - 1];
1819                kept.extend(path);
1820            }
1821        } else {
1822            return None;
1823        }
1824    }
1825
1826    Some(kept)
1827}
1828
1829// Generate a grid of tiles for the floor based on rooms and doors
1830fn generate_tiles(
1831    rooms: &FxHashMap<u32, CellRoom>,
1832    kept: &FxHashSet<u32>,
1833    width: u32,
1834    height: u32,
1835    tiles_per_cell: u32,
1836    wall_dimensions: (u32, u32),
1837    wall_offset: (i32, i32),
1838) -> (Vec<Vec<Tile>>, FxHashMap<(u32, u32), Position>) {
1839    let mut tiles = Vec::with_capacity((width * tiles_per_cell) as usize);
1840    for x in 0..width * tiles_per_cell {
1841        tiles.push(Vec::with_capacity((height * tiles_per_cell) as usize));
1842        for _ in 0..height * tiles_per_cell {
1843            tiles[x as usize].push(Tile::Wall);
1844        }
1845    }
1846
1847    let mut doors = FxHashMap::default();
1848    for room in rooms.values() {
1849        if room.kind == RoomKind::Corridor && !kept.contains(&room.id) {
1850            continue;
1851        }
1852
1853        let left_wall = (wall_dimensions.0 as f32 / 2.0 + wall_offset.0 as f32).floor() as u32;
1854        let right_wall = (wall_dimensions.0 as f32 / 2.0 - wall_offset.0 as f32).ceil() as u32;
1855        let top_wall = (wall_dimensions.1 as f32 / 2.0 + wall_offset.1 as f32).floor() as u32;
1856        let bottom_wall = (wall_dimensions.1 as f32 / 2.0 - wall_offset.1 as f32).ceil() as u32;
1857
1858        let i = room.cell.i * tiles_per_cell;
1859        let j = room.cell.j * tiles_per_cell;
1860        let width = room.width * tiles_per_cell;
1861        let height = room.height * tiles_per_cell;
1862
1863        for x in left_wall..width - right_wall {
1864            for y in top_wall..height - bottom_wall {
1865                tiles[(i + x) as usize][(j + y) as usize] = Tile::Floor;
1866            }
1867        }
1868
1869        for connection in &room.connections {
1870            let connected = rooms.get(&connection.id).unwrap();
1871            if connected.kind == RoomKind::Corridor && !kept.contains(&connection.id) {
1872                continue;
1873            }
1874
1875            let shared_cells = room.shared_cells(connected);
1876            let (door, door_tiles) = match connection.direction {
1877                Direction::Left => {
1878                    let js: Vec<_> = shared_cells.map(|cell| cell.j).collect();
1879                    let min_j = js.iter().min().unwrap();
1880                    let max_j = js.iter().max().unwrap();
1881
1882                    let x = i;
1883                    let y = (min_j * tiles_per_cell + (max_j + 1) * tiles_per_cell - 1) / 2;
1884
1885                    let door_tiles: Vec<_> = (0..left_wall)
1886                        .flat_map(|i| [Position { x: x + i, y }, Position { x: x + i, y: y + 1 }])
1887                        .collect();
1888                    (Position { x: x - 1, y }, door_tiles)
1889                }
1890                Direction::Right => {
1891                    let js: Vec<_> = shared_cells.map(|cell| cell.j).collect();
1892                    let min_j = js.iter().min().unwrap();
1893                    let max_j = js.iter().max().unwrap();
1894
1895                    let x = i + width - right_wall;
1896                    let y = (min_j * tiles_per_cell + (max_j + 1) * tiles_per_cell - 1) / 2;
1897
1898                    let door_tiles: Vec<_> = (0..right_wall)
1899                        .flat_map(|i| [Position { x: x + i, y }, Position { x: x + i, y: y + 1 }])
1900                        .collect();
1901                    (
1902                        Position {
1903                            x: x + right_wall - 1,
1904                            y,
1905                        },
1906                        door_tiles,
1907                    )
1908                }
1909                Direction::Up => {
1910                    let is: Vec<_> = shared_cells.map(|cell| cell.i).collect();
1911                    let min_i = is.iter().min().unwrap();
1912                    let max_i = is.iter().max().unwrap();
1913
1914                    let x = (min_i * tiles_per_cell + (max_i + 1) * tiles_per_cell - 1) / 2;
1915                    let y = j;
1916
1917                    let door_tiles: Vec<_> = (0..top_wall)
1918                        .flat_map(|j| [Position { x, y: y + j }, Position { x: x + 1, y: y + j }])
1919                        .collect();
1920                    (Position { x, y: y - 1 }, door_tiles)
1921                }
1922                Direction::Down => {
1923                    let is: Vec<_> = shared_cells.map(|cell| cell.i).collect();
1924                    let min_i = is.iter().min().unwrap();
1925                    let max_i = is.iter().max().unwrap();
1926
1927                    let x = (min_i * tiles_per_cell + (max_i + 1) * tiles_per_cell - 1) / 2;
1928                    let y = j + height - bottom_wall;
1929
1930                    let door_tiles: Vec<_> = (0..bottom_wall)
1931                        .flat_map(|j| [Position { x, y: y + j }, Position { x: x + 1, y: y + j }])
1932                        .collect();
1933                    (
1934                        Position {
1935                            x,
1936                            y: y + bottom_wall - 1,
1937                        },
1938                        door_tiles,
1939                    )
1940                }
1941            };
1942
1943            for position in &door_tiles {
1944                tiles[position.x as usize][position.y as usize] = Tile::Floor;
1945            }
1946
1947            doors.insert((room.id, connection.id), door);
1948        }
1949    }
1950
1951    (tiles, doors)
1952}
1953
1954// An iterator over all cells in a floor
1955fn cells(width: u32, height: u32) -> impl Iterator<Item = Cell> {
1956    (0..width).flat_map(move |i| (0..height).map(move |j| Cell { i, j }))
1957}
1958
1959// An iterator over all adjacent rooms
1960fn neighbors(
1961    room: &CellRoom,
1962    rooms: &FxHashMap<u32, CellRoom>,
1963    min_shared_cells: (usize, usize),
1964) -> impl Iterator<Item = CellConnection> {
1965    rooms
1966        .values()
1967        .filter_map(|other| match room.direction_to(other) {
1968            Some(direction) if room.id != other.id => {
1969                let count = room.shared_cells(other).count();
1970                match direction {
1971                    Direction::Left | Direction::Right if count >= min_shared_cells.1 => {
1972                        Some(CellConnection {
1973                            id: other.id,
1974                            direction,
1975                        })
1976                    }
1977                    Direction::Up | Direction::Down if count >= min_shared_cells.0 => {
1978                        Some(CellConnection {
1979                            id: other.id,
1980                            direction,
1981                        })
1982                    }
1983                    _ => None,
1984                }
1985            }
1986            _ => None,
1987        })
1988        .sorted_by(|a, b| a.direction.cmp(&b.direction))
1989}
1990
1991// Average perlin noise for a room
1992fn get_room_noise(room: &Room, perlin: &Perlin, width: u32, height: u32) -> f32 {
1993    let noise_sum: f32 = (0..room.width)
1994        .flat_map(|i| {
1995            (0..room.height).map(move |j| {
1996                perlin.get([
1997                    (room.floor * width) as f64 + (room.position.x + i) as f64 * 0.01,
1998                    (room.floor * height) as f64 + (room.position.y + j) as f64 * 0.01,
1999                ]) as f32
2000            })
2001        })
2002        .sum();
2003    let area = (room.width * room.height) as f32;
2004    noise_sum / area
2005}
2006
2007// Pick a random value from a range of integers weighted by a noise value
2008// Larger values are less likely to be picked than smaller ones
2009fn weighted_random<R: Rng>(rng: &mut R, min: u32, max: u32, noise: f32, scale: f32) -> u32 {
2010    let values: Vec<_> = (min..=max).collect();
2011    let noise = (noise + 1.0) / 2.0;
2012    let weights: Vec<_> = (0..values.len())
2013        .map(|i| ((i + 1) as f32).powf(1.0 + scale * noise))
2014        .rev()
2015        .collect();
2016    let dist = WeightedIndex::new(weights).unwrap();
2017    values[dist.sample(rng)]
2018}
2019
2020#[derive(Debug, Clone, PartialEq, Eq)]
2021struct Frontier {
2022    priority: u32,
2023    id: u32,
2024}
2025
2026impl Ord for Frontier {
2027    fn cmp(&self, other: &Self) -> Ordering {
2028        other.priority.cmp(&self.priority)
2029    }
2030}
2031
2032impl PartialOrd for Frontier {
2033    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2034        Some(self.cmp(other))
2035    }
2036}
2037
2038// Pathfind between mock-up rooms based on their connections
2039fn pathfind_cell_rooms(
2040    start: u32,
2041    goal: u32,
2042    rooms: &FxHashMap<u32, CellRoom>,
2043) -> Option<(Vec<u32>, u32)> {
2044    let mut frontier = BinaryHeap::new();
2045    let mut came_from = FxHashMap::default();
2046    let mut costs = FxHashMap::default();
2047
2048    frontier.push(Frontier {
2049        priority: 0,
2050        id: start,
2051    });
2052    costs.insert(start, 0);
2053
2054    let goal_room = rooms.get(&goal).unwrap();
2055    while let Some(Frontier { priority: _, id }) = frontier.pop() {
2056        if id == goal {
2057            break;
2058        }
2059
2060        let room = rooms.get(&id).unwrap();
2061        for connection in &room.connections {
2062            let new_cost = costs.get(&id).unwrap() + 1;
2063            if !costs.contains_key(&connection.id) || new_cost < *costs.get(&connection.id).unwrap()
2064            {
2065                let connected_room = rooms.get(&connection.id).unwrap();
2066                frontier.push(Frontier {
2067                    priority: new_cost + cell_room_distance(connected_room, goal_room),
2068                    id: connection.id,
2069                });
2070                came_from.insert(connection.id, Some(id));
2071                costs.insert(connection.id, new_cost);
2072            }
2073        }
2074    }
2075
2076    let mut id = goal;
2077    let mut path = Vec::new();
2078
2079    while id != start {
2080        path.push(id);
2081        id = match came_from.get(&id) {
2082            Some(Some(id)) => *id,
2083            _ => return None,
2084        };
2085    }
2086    path.push(start);
2087    path.reverse();
2088
2089    let cost = costs.get(&goal).unwrap();
2090
2091    Some((path, *cost))
2092}
2093
2094// Distance between the center of two mock-up rooms
2095fn cell_room_distance(a: &CellRoom, b: &CellRoom) -> u32 {
2096    let a = a.center();
2097    let b = b.center();
2098    let (i_diff, j_diff) = (a.i as f32 - b.i as f32, a.j as f32 - b.j as f32);
2099    (i_diff * i_diff + j_diff * j_diff).sqrt() as u32
2100}
2101
2102// Pathfind between floor rooms based on their connections, stairs and teleporters
2103fn pathfind_rooms(start: u32, goal: u32, rooms: &FxHashMap<u32, Room>) -> Option<(Vec<u32>, u32)> {
2104    let mut frontier = BinaryHeap::new();
2105    let mut came_from = FxHashMap::default();
2106    let mut costs = FxHashMap::default();
2107
2108    frontier.push(Frontier {
2109        priority: 0,
2110        id: start,
2111    });
2112    costs.insert(start, 0);
2113
2114    let goal_room = rooms.get(&goal).unwrap();
2115    while let Some(Frontier { priority: _, id }) = frontier.pop() {
2116        if id == goal {
2117            break;
2118        }
2119
2120        let room = rooms.get(&id).unwrap();
2121
2122        // Doors have a cost of 1, stairs have a cost of 5, teleporters have a cost of 2
2123        let connections = room
2124            .connections
2125            .iter()
2126            .map(|connection| (connection.id, 1))
2127            .chain(room.stairs.iter().filter_map(|stair| {
2128                rooms.values().find_map(|room| {
2129                    room.stairs.iter().find_map(|other| {
2130                        if stair.connected == other.id {
2131                            Some((room.id, 5))
2132                        } else {
2133                            None
2134                        }
2135                    })
2136                })
2137            }))
2138            .chain(room.teleporters.iter().filter_map(|teleporter| {
2139                rooms.values().find_map(|room| {
2140                    room.teleporters.iter().find_map(|other| {
2141                        if teleporter.connected == other.id {
2142                            Some((room.id, 2))
2143                        } else {
2144                            None
2145                        }
2146                    })
2147                })
2148            }));
2149
2150        for (connection_id, cost) in connections {
2151            let new_cost = costs.get(&id).unwrap() + cost;
2152            if !costs.contains_key(&connection_id) || new_cost < *costs.get(&connection_id).unwrap()
2153            {
2154                let connected_room = rooms.get(&connection_id).unwrap();
2155                frontier.push(Frontier {
2156                    priority: new_cost + room_distance(connected_room, goal_room),
2157                    id: connection_id,
2158                });
2159                came_from.insert(connection_id, Some(id));
2160                costs.insert(connection_id, new_cost);
2161            }
2162        }
2163    }
2164
2165    let mut id = goal;
2166    let mut path = Vec::new();
2167
2168    while id != start {
2169        path.push(id);
2170        id = match came_from.get(&id) {
2171            Some(Some(id)) => *id,
2172            _ => return None,
2173        };
2174    }
2175    path.push(start);
2176    path.reverse();
2177
2178    let cost = costs.get(&goal).unwrap();
2179
2180    Some((path, *cost))
2181}
2182
2183// Distance between the center of two floor rooms
2184fn room_distance(a: &Room, b: &Room) -> u32 {
2185    let a = a.center();
2186    let b = b.center();
2187    let (x_diff, y_diff) = (a.x as f32 - b.x as f32, a.y as f32 - b.y as f32);
2188    (x_diff * x_diff + y_diff * y_diff).sqrt() as u32
2189}
2190
2191#[cfg(test)]
2192mod tests {
2193    use super::*;
2194    use std::time::{Duration, Instant};
2195
2196    // Use `cargo test --release -- --nocapture`
2197    #[test]
2198    fn bench_dungeon_generation() {
2199        let mut elapsed = Duration::from_secs(0);
2200        for _ in 0..100 {
2201            let now = Instant::now();
2202            assert!(Dungeon::generate().is_ok());
2203            elapsed += now.elapsed();
2204        }
2205        println!("{:?}", elapsed / 100);
2206    }
2207
2208    #[test]
2209    fn output_dungeon_as_image() {
2210        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2211            num_floors: 9,
2212            ..GenerateDungeonParams::default()
2213        });
2214        assert!(res.is_ok());
2215        let dungeon = res.unwrap();
2216
2217        let res = dungeon.output_as_image("images/dungeon.png", "images/spritesheet.png", 8);
2218        assert!(res.is_ok());
2219    }
2220
2221    #[test]
2222    fn output_floor_as_image() {
2223        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2224            dimensions: (16, 16),
2225            min_rooms_per_floor: 4,
2226            max_rooms_per_floor: 4,
2227            min_room_dimensions: (1, 1),
2228            max_room_dimensions: (2, 2),
2229            wall_dimensions: (2, 3),
2230            wall_offset: (0, 1),
2231            adjacent_rooms_allowed: true,
2232            ..GenerateDungeonParams::default()
2233        });
2234        assert!(res.is_ok());
2235        let dungeon = res.unwrap();
2236
2237        let res =
2238            dungeon.output_floor_as_image(0, "images/floor-1.png", "images/spritesheet.png", 8);
2239        assert!(res.is_ok());
2240    }
2241
2242    #[test]
2243    fn error_no_floors() {
2244        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2245            num_floors: 0,
2246            ..GenerateDungeonParams::default()
2247        });
2248        assert!(res.is_err());
2249        assert_eq!(res.unwrap_err(), TatamiError::NoFloors);
2250    }
2251
2252    #[test]
2253    fn error_no_rooms() {
2254        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2255            min_rooms_per_floor: 0,
2256            max_rooms_per_floor: 0,
2257            ..GenerateDungeonParams::default()
2258        });
2259        assert!(res.is_err());
2260        assert_eq!(res.unwrap_err(), TatamiError::NoRooms);
2261
2262        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2263            min_rooms_per_floor: 0,
2264            max_rooms_per_floor: 1,
2265            ..GenerateDungeonParams::default()
2266        });
2267        assert!(res.is_err());
2268        assert_eq!(res.unwrap_err(), TatamiError::NoRooms);
2269    }
2270
2271    #[test]
2272    fn error_no_walls() {
2273        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2274            wall_dimensions: (0, 0),
2275            ..GenerateDungeonParams::default()
2276        });
2277        assert!(res.is_err());
2278        assert_eq!(res.unwrap_err(), TatamiError::NoWalls);
2279
2280        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2281            wall_dimensions: (1, 0),
2282            ..GenerateDungeonParams::default()
2283        });
2284        assert!(res.is_err());
2285        assert_eq!(res.unwrap_err(), TatamiError::NoWalls);
2286
2287        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2288            wall_dimensions: (0, 1),
2289            ..GenerateDungeonParams::default()
2290        });
2291        assert!(res.is_err());
2292        assert_eq!(res.unwrap_err(), TatamiError::NoWalls);
2293    }
2294
2295    #[test]
2296    fn error_no_shared_cells() {
2297        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2298            min_shared_cells: (0, 0),
2299            ..GenerateDungeonParams::default()
2300        });
2301        assert!(res.is_err());
2302        assert_eq!(res.unwrap_err(), TatamiError::NoSharedCells);
2303
2304        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2305            min_shared_cells: (1, 0),
2306            ..GenerateDungeonParams::default()
2307        });
2308        assert!(res.is_err());
2309        assert_eq!(res.unwrap_err(), TatamiError::NoSharedCells);
2310
2311        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2312            min_shared_cells: (0, 1),
2313            ..GenerateDungeonParams::default()
2314        });
2315        assert!(res.is_err());
2316        assert_eq!(res.unwrap_err(), TatamiError::NoSharedCells);
2317    }
2318
2319    #[test]
2320    fn error_invalid_room_dimensions() {
2321        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2322            min_room_dimensions: (0, 0),
2323            max_room_dimensions: (0, 0),
2324            ..GenerateDungeonParams::default()
2325        });
2326        assert!(res.is_err());
2327        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2328
2329        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2330            min_room_dimensions: (0, 0),
2331            max_room_dimensions: (1, 1),
2332            ..GenerateDungeonParams::default()
2333        });
2334        assert!(res.is_err());
2335        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2336
2337        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2338            min_room_dimensions: (1, 0),
2339            max_room_dimensions: (1, 1),
2340            ..GenerateDungeonParams::default()
2341        });
2342        assert!(res.is_err());
2343        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2344
2345        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2346            min_room_dimensions: (0, 1),
2347            max_room_dimensions: (1, 1),
2348            ..GenerateDungeonParams::default()
2349        });
2350        assert!(res.is_err());
2351        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2352
2353        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2354            min_corridor_dimensions: (0, 0),
2355            max_corridor_dimensions: (0, 0),
2356            ..GenerateDungeonParams::default()
2357        });
2358        assert!(res.is_err());
2359        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2360
2361        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2362            min_corridor_dimensions: (0, 0),
2363            max_corridor_dimensions: (1, 1),
2364            ..GenerateDungeonParams::default()
2365        });
2366        assert!(res.is_err());
2367        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2368
2369        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2370            min_corridor_dimensions: (1, 0),
2371            max_corridor_dimensions: (1, 1),
2372            ..GenerateDungeonParams::default()
2373        });
2374        assert!(res.is_err());
2375        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2376
2377        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2378            min_corridor_dimensions: (0, 1),
2379            max_corridor_dimensions: (1, 1),
2380            ..GenerateDungeonParams::default()
2381        });
2382        assert!(res.is_err());
2383        assert_eq!(res.unwrap_err(), TatamiError::InvalidRoomDimensions);
2384    }
2385
2386    #[test]
2387    fn error_min_greater_than_max() {
2388        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2389            min_rooms_per_floor: 3,
2390            max_rooms_per_floor: 2,
2391            ..GenerateDungeonParams::default()
2392        });
2393        assert!(res.is_err());
2394        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2395
2396        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2397            min_room_dimensions: (2, 2),
2398            max_room_dimensions: (1, 1),
2399            ..GenerateDungeonParams::default()
2400        });
2401        assert!(res.is_err());
2402        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2403
2404        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2405            min_room_dimensions: (1, 2),
2406            max_room_dimensions: (2, 1),
2407            ..GenerateDungeonParams::default()
2408        });
2409        assert!(res.is_err());
2410        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2411
2412        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2413            min_room_dimensions: (2, 1),
2414            max_room_dimensions: (1, 2),
2415            ..GenerateDungeonParams::default()
2416        });
2417        assert!(res.is_err());
2418        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2419
2420        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2421            min_corridor_dimensions: (2, 2),
2422            max_corridor_dimensions: (1, 1),
2423            ..GenerateDungeonParams::default()
2424        });
2425        assert!(res.is_err());
2426        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2427
2428        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2429            min_corridor_dimensions: (1, 2),
2430            max_corridor_dimensions: (2, 1),
2431            ..GenerateDungeonParams::default()
2432        });
2433        assert!(res.is_err());
2434        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2435
2436        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2437            min_corridor_dimensions: (2, 1),
2438            max_corridor_dimensions: (1, 2),
2439            ..GenerateDungeonParams::default()
2440        });
2441        assert!(res.is_err());
2442        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2443
2444        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2445            min_stairs_per_floor: 2,
2446            max_stairs_per_floor: 1,
2447            ..GenerateDungeonParams::default()
2448        });
2449        assert!(res.is_err());
2450        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2451
2452        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2453            min_teleporters_per_floor: 2,
2454            max_teleporters_per_floor: 1,
2455            ..GenerateDungeonParams::default()
2456        });
2457        assert!(res.is_err());
2458        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2459
2460        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2461            min_items_per_room: 2,
2462            max_items_per_room: 1,
2463            ..GenerateDungeonParams::default()
2464        });
2465        assert!(res.is_err());
2466        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2467
2468        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2469            min_enemies_per_room: 2,
2470            max_enemies_per_room: 1,
2471            ..GenerateDungeonParams::default()
2472        });
2473        assert!(res.is_err());
2474        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2475
2476        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2477            min_traps_per_room: 2,
2478            max_traps_per_room: 1,
2479            ..GenerateDungeonParams::default()
2480        });
2481        assert!(res.is_err());
2482        assert_eq!(res.unwrap_err(), TatamiError::MinGreaterThanMax);
2483    }
2484
2485    #[test]
2486    fn error_too_small() {
2487        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2488            dimensions: (2, 2),
2489            num_floors: 1,
2490            min_rooms_per_floor: 2,
2491            max_rooms_per_floor: 2,
2492            min_room_dimensions: (1, 1),
2493            max_room_dimensions: (1, 1),
2494            adjacent_rooms_allowed: true,
2495            min_stairs_per_floor: 0,
2496            max_stairs_per_floor: 0,
2497            ..GenerateDungeonParams::default()
2498        });
2499        assert!(res.is_ok());
2500
2501        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2502            dimensions: (10, 10),
2503            num_floors: 1,
2504            min_rooms_per_floor: 4,
2505            max_rooms_per_floor: 4,
2506            min_room_dimensions: (1, 1),
2507            max_room_dimensions: (3, 3),
2508            adjacent_rooms_allowed: false,
2509            ..GenerateDungeonParams::default()
2510        });
2511        assert!(res.is_ok());
2512
2513        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2514            dimensions: (10, 10),
2515            num_floors: 1,
2516            min_rooms_per_floor: 4,
2517            max_rooms_per_floor: 4,
2518            min_room_dimensions: (1, 1),
2519            max_room_dimensions: (5, 5),
2520            adjacent_rooms_allowed: true,
2521            ..GenerateDungeonParams::default()
2522        });
2523        assert!(res.is_ok());
2524
2525        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2526            dimensions: (16, 16),
2527            num_floors: 1,
2528            min_rooms_per_floor: 8,
2529            max_rooms_per_floor: 8,
2530            min_room_dimensions: (1, 1),
2531            max_room_dimensions: (2, 2),
2532            adjacent_rooms_allowed: false,
2533            ..GenerateDungeonParams::default()
2534        });
2535        assert!(res.is_ok());
2536
2537        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2538            dimensions: (16, 16),
2539            num_floors: 1,
2540            min_rooms_per_floor: 8,
2541            max_rooms_per_floor: 8,
2542            min_room_dimensions: (1, 1),
2543            max_room_dimensions: (4, 4),
2544            adjacent_rooms_allowed: true,
2545            ..GenerateDungeonParams::default()
2546        });
2547        assert!(res.is_ok());
2548
2549        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2550            dimensions: (10, 10),
2551            num_floors: 1,
2552            min_rooms_per_floor: 5,
2553            max_rooms_per_floor: 5,
2554            min_room_dimensions: (1, 1),
2555            max_room_dimensions: (3, 3),
2556            adjacent_rooms_allowed: false,
2557            ..GenerateDungeonParams::default()
2558        });
2559        assert!(res.is_err());
2560        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2561
2562        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2563            dimensions: (10, 10),
2564            num_floors: 1,
2565            min_rooms_per_floor: 4,
2566            max_rooms_per_floor: 5,
2567            min_room_dimensions: (1, 1),
2568            max_room_dimensions: (3, 3),
2569            adjacent_rooms_allowed: false,
2570            ..GenerateDungeonParams::default()
2571        });
2572        assert!(res.is_err());
2573        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2574
2575        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2576            dimensions: (10, 10),
2577            num_floors: 1,
2578            min_rooms_per_floor: 4,
2579            max_rooms_per_floor: 4,
2580            min_room_dimensions: (1, 1),
2581            max_room_dimensions: (4, 4),
2582            adjacent_rooms_allowed: false,
2583            ..GenerateDungeonParams::default()
2584        });
2585        assert!(res.is_err());
2586        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2587
2588        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2589            dimensions: (10, 10),
2590            num_floors: 1,
2591            min_rooms_per_floor: 4,
2592            max_rooms_per_floor: 4,
2593            min_room_dimensions: (1, 1),
2594            max_room_dimensions: (3, 4),
2595            adjacent_rooms_allowed: false,
2596            ..GenerateDungeonParams::default()
2597        });
2598        assert!(res.is_err());
2599        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2600
2601        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2602            dimensions: (10, 10),
2603            num_floors: 1,
2604            min_rooms_per_floor: 4,
2605            max_rooms_per_floor: 4,
2606            min_room_dimensions: (1, 1),
2607            max_room_dimensions: (4, 3),
2608            adjacent_rooms_allowed: false,
2609            ..GenerateDungeonParams::default()
2610        });
2611        assert!(res.is_err());
2612        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2613
2614        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2615            dimensions: (10, 10),
2616            num_floors: 1,
2617            min_rooms_per_floor: 5,
2618            max_rooms_per_floor: 5,
2619            min_room_dimensions: (1, 1),
2620            max_room_dimensions: (4, 4),
2621            adjacent_rooms_allowed: true,
2622            ..GenerateDungeonParams::default()
2623        });
2624        assert!(res.is_err());
2625        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2626
2627        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2628            dimensions: (10, 10),
2629            num_floors: 1,
2630            min_rooms_per_floor: 4,
2631            max_rooms_per_floor: 5,
2632            min_room_dimensions: (1, 1),
2633            max_room_dimensions: (4, 4),
2634            adjacent_rooms_allowed: true,
2635            ..GenerateDungeonParams::default()
2636        });
2637        assert!(res.is_err());
2638        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2639
2640        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2641            dimensions: (10, 10),
2642            num_floors: 1,
2643            min_rooms_per_floor: 4,
2644            max_rooms_per_floor: 4,
2645            min_room_dimensions: (1, 1),
2646            max_room_dimensions: (6, 6),
2647            adjacent_rooms_allowed: true,
2648            ..GenerateDungeonParams::default()
2649        });
2650        assert!(res.is_err());
2651        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2652
2653        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2654            dimensions: (10, 10),
2655            num_floors: 1,
2656            min_rooms_per_floor: 4,
2657            max_rooms_per_floor: 4,
2658            min_room_dimensions: (1, 1),
2659            max_room_dimensions: (5, 6),
2660            adjacent_rooms_allowed: true,
2661            ..GenerateDungeonParams::default()
2662        });
2663        assert!(res.is_err());
2664        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2665
2666        let res = Dungeon::generate_with_params(GenerateDungeonParams {
2667            dimensions: (10, 10),
2668            num_floors: 1,
2669            min_rooms_per_floor: 4,
2670            max_rooms_per_floor: 4,
2671            min_room_dimensions: (1, 1),
2672            max_room_dimensions: (6, 5),
2673            adjacent_rooms_allowed: true,
2674            ..GenerateDungeonParams::default()
2675        });
2676        assert!(res.is_err());
2677        assert_eq!(res.unwrap_err(), TatamiError::TooSmall);
2678    }
2679}