1use 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#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
26pub struct GenerateDungeonParams {
27 pub dimensions: (u32, u32),
29 pub tiles_per_cell: u32,
31 pub num_floors: u32,
33 pub min_rooms_per_floor: u32,
35 pub max_rooms_per_floor: u32,
37 pub min_room_dimensions: (u32, u32),
39 pub max_room_dimensions: (u32, u32),
41 pub adjacent_rooms_allowed: bool,
43 pub min_corridor_dimensions: (u32, u32),
45 pub max_corridor_dimensions: (u32, u32),
47 pub wall_dimensions: (u32, u32),
49 pub wall_offset: (i32, i32),
51 pub min_shared_cells: (usize, usize),
53 pub squareness: f32,
55 pub min_stairs_per_floor: u32,
57 pub max_stairs_per_floor: u32,
59 pub min_teleporters_per_floor: u32,
61 pub max_teleporters_per_floor: u32,
63 pub min_items_per_room: u32,
65 pub max_items_per_room: u32,
67 pub max_item_rarity: u32,
69 pub item_density_scale: f32,
71 pub item_rarity_scale: f32,
73 pub min_enemies_per_room: u32,
75 pub max_enemies_per_room: u32,
77 pub max_enemy_difficulty: u32,
79 pub enemy_density_scale: f32,
81 pub enemy_difficulty_scale: f32,
83 pub min_traps_per_room: u32,
85 pub max_traps_per_room: u32,
87 pub max_trap_difficulty: u32,
89 pub trap_density_scale: f32,
91 pub trap_difficulty_scale: f32,
93 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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
163pub struct CellConnection {
164 pub id: u32,
166 pub direction: Direction,
168}
169
170#[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#[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 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 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 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#[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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
351pub struct Connection {
352 pub id: u32,
354 pub direction: Direction,
356 pub door: Position,
358}
359
360#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
362pub struct Stair {
363 pub id: u32,
365 pub position: Position,
367 pub downwards: bool,
369 pub connected: u32,
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
375pub struct Teleporter {
376 pub id: u32,
378 pub position: Position,
380 pub connected: u32,
382}
383
384#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
386pub struct Item {
387 pub id: u32,
389 pub position: Position,
391 pub rarity: u32,
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
397pub struct Enemy {
398 pub id: u32,
400 pub position: Position,
402 pub difficulty: u32,
404}
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
408pub struct Trap {
409 pub id: u32,
411 pub position: Position,
413 pub difficulty: u32,
415}
416
417#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, EnumIter)]
419pub enum RoomKind {
420 Main,
422 Corridor,
424}
425
426#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
428pub struct Room {
429 pub id: u32,
431 pub kind: RoomKind,
433 pub floor: u32,
435 pub position: Position,
437 pub width: u32,
439 pub height: u32,
441 pub connections: Vec<Connection>,
443 pub difficulty: f32,
445 pub stairs: Vec<Stair>,
447 pub teleporters: Vec<Teleporter>,
449 pub items: Vec<Item>,
451 pub enemies: Vec<Enemy>,
453 pub traps: Vec<Trap>,
455 pub wall_dimensions: (u32, u32),
457 pub wall_offset: (i32, i32),
459}
460
461impl Room {
462 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
548pub enum Tile {
549 Floor,
551 Wall,
553}
554
555#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
557pub struct Floor {
558 pub number: u32,
560 pub tiles: Vec<Vec<Tile>>,
562 pub rooms: Vec<Room>,
564}
565
566impl Floor {
567 pub fn tile_at(&self, position: Position) -> Tile {
569 self.tiles[position.x as usize][position.y as usize]
570 }
571}
572
573#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
575pub struct Dungeon {
576 pub params: GenerateDungeonParams,
578 pub floors: Vec<Floor>,
580 pub starting_room_id: u32,
582 pub player_position: Position,
584}
585
586#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
588pub enum TatamiError {
589 NoFloors,
591 NoRooms,
593 NoWalls,
595 InvalidRoomDimensions,
597 NoSharedCells,
599 MinGreaterThanMax,
601 TooSmall,
603}
604
605impl Dungeon {
606 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 pub fn generate_with_seed(seed: [u8; 32]) -> Result<Self, TatamiError> {
617 Self::generate_with_seed_and_params(seed, GenerateDungeonParams::default())
618 }
619
620 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 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 ¶ms,
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 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 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 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
1182fn 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(¶ms, 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
1315fn 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
1447fn 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
1505fn 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 create_partitions(i, j, width / 2, height, false, depth - 1),
1522 create_partitions(i + width / 2, j, width / 2, height, false, depth - 1),
1524 ]
1525 .concat()
1526 } else {
1527 [
1528 create_partitions(i, j, width, height / 2, true, depth - 1),
1530 create_partitions(i, j + height / 2, width, height / 2, true, depth - 1),
1532 ]
1533 .concat()
1534 }
1535}
1536
1537fn 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 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 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 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
1698fn 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
1748fn 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 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
1829fn 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
1954fn 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
1959fn 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
1991fn 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
2007fn 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
2038fn 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
2094fn 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
2102fn 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 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
2183fn 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 #[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}