Skip to main content

map_gen_2d/
bsp.rs

1use core::fmt;
2use std::collections::HashMap;
3
4use crate::{Point, Tile};
5use anyhow::bail;
6use rand::{rngs::StdRng, Rng};
7/// Map generated with binary search partitioning. The map must have a size of atleast (20,20).
8///
9/// Credit to https://gamedevelopment.tutsplus.com/tutorials/how-to-use-bsp-trees-to-generate-game-maps--gamedev-12268 and https://github.com/whostolemyhat/dungeon
10/// for algorithm and rust implementation help.
11pub struct BSPMap {
12    size: Point,
13    tiles: HashMap<Point, Tile>,
14    rooms: Vec<Room>,
15    min_room_size: Point,
16    max_room_size: Point,
17}
18impl BSPMap {
19    pub fn new(
20        size: Point,
21        mut seed: StdRng,
22        min_room_size: Point,
23        max_room_size: Point,
24    ) -> Result<Self, anyhow::Error> {
25        if size.x < 20 || size.y < 20 {
26            bail!("Size of a BSP_Map needs to be greater than or equal x : 20, y : 20")
27        }
28        if min_room_size.x >= max_room_size.x {
29            bail!("Minimum room size (x) needs to be less than maximum room size (x).")
30        }
31        if min_room_size.y >= max_room_size.y {
32            bail!("Minimum room size (y) needs to be less than maximum room size (y).")
33        }
34        if max_room_size.x >= size.x {
35            bail!("Maximum room size (x) must be less than map size (x).")
36        }
37        if max_room_size.y >= size.y {
38            bail!("Maximum room size (y) must be less than map size (y).")
39        }
40        let mut map = BSPMap {
41            size: size,
42            tiles: HashMap::new(),
43            rooms: Vec::new(),
44            min_room_size,
45            max_room_size,
46        };
47        map.place_rooms(&mut seed, map.min_room_size, map.max_room_size);
48        // Hard wall around left
49        for y in 0..size.y {
50            map.tiles.insert(Point::new(0, y), Tile::Wall);
51            map.tiles.insert(Point::new(map.size.x, y), Tile::Wall);
52
53        }
54        // hard wall on top
55        for x in 0..size.x {
56            map.tiles.insert(Point::new(x, 0), Tile::Wall);
57            map.tiles.insert(Point::new(x, map.size.y), Tile::Wall);
58        }
59        let mut walls : Vec<Point> = Vec::new();
60        // Walls around tiles
61        for tile in map.tiles.iter() {
62            // check all eight points around
63            if map.tiles.get(&Point::new(tile.0.x + 1, tile.0.y)).is_none() {
64               walls.push(Point::new(tile.0.x + 1, tile.0.y))
65            }
66            if map.tiles.get(&Point::new(tile.0.x + 1, tile.0.y + 1)).is_none() {
67                walls.push(Point::new(tile.0.x + 1, tile.0.y + 1))
68            }
69            if map.tiles.get(&Point::new(tile.0.x, tile.0.y + 1)).is_none() {
70                walls.push(Point::new(tile.0.x, tile.0.y + 1))
71            }
72            if tile.0.x != 0 && map.tiles.get(&Point::new(tile.0.x - 1, tile.0.y + 1)).is_none() {
73                walls.push(Point::new(tile.0.x - 1, tile.0.y + 1))
74            }
75            if tile.0.x != 0 && map.tiles.get(&Point::new(tile.0.x - 1, tile.0.y)).is_none() {
76                walls.push(Point::new(tile.0.x - 1, tile.0.y))
77            }
78            if tile.0.x != 0 && tile.0.y != 0 && map.tiles.get(&Point::new(tile.0.x - 1, tile.0.y - 1)).is_none() {
79                walls.push(Point::new(tile.0.x - 1, tile.0.y - 1))
80            }
81            if tile.0.y != 0 && map.tiles.get(&Point::new(tile.0.x, tile.0.y - 1)).is_none() {
82                walls.push(Point::new(tile.0.x, tile.0.y - 1))
83            }
84            if tile.0.y != 0 && map.tiles.get(&Point::new(tile.0.x + 1, tile.0.y - 1)).is_none() {
85                walls.push(Point::new(tile.0.x + 1, tile.0.y - 1))
86            }
87        }
88        // Insert walls
89        for wall in walls.iter(){
90            map.tiles.insert(wall.clone(), Tile::Wall);
91        }
92        Ok(map)
93    }
94
95    pub fn get_tiles(&self) -> &HashMap<Point, Tile> {
96        &self.tiles
97    }
98
99    pub fn get_size(&self) -> &Point {
100        &self.size
101    }
102
103    pub fn get_rooms(&self) -> &Vec<Room> {
104        &&self.rooms
105    }
106    fn place_rooms(&mut self, rng: &mut StdRng, min_room_size: Point, max_room_size: Point) {
107        let mut root = Leaf::new(Point { x: 0, y: 0 }, self.size);
108        // generate leaves
109        root.generate(rng, &min_room_size, &max_room_size);
110        // generate rooms in leaves
111        root.create_rooms(rng, &min_room_size);
112        // Loop over leaves spawning rooms
113        for leaf in root.iter() {
114            if leaf.is_leaf() {
115                if let Some(room) = leaf.get_room() {
116                    self.add_room(&room);
117                }
118            }
119
120            for corridor in &leaf.corridors {
121                self.add_room(&corridor);
122            }
123        }
124    }
125
126    pub fn add_room(&mut self, room: &Room) {
127        for x in 0..room.size.x {
128            for y in 0..room.size.y {
129                self.tiles.insert(
130                    Point::new(room.position.x + x, room.position.y + y),
131                    Tile::Floor,
132                );
133            }
134        }
135        self.rooms.push(room.clone());
136    }
137}
138
139impl fmt::Display for BSPMap {
140    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141        // for tile in self.tiles.iter() {
142        //     match self.tiles.get(&tile.0) {
143        //         Some(x) => write!(f, "{}", x)?,
144        //         None => write!(f, "x")?,
145        //     }
146        // }
147        for row in 0..=self.size.x {
148            for col in 0..=self.size.y {
149                match self.tiles.get(&Point::new(row, col)) {
150                    Some(x) => write!(f, "{}", x)?,
151                    None => write!(f, "x")?,
152                }
153            }
154            write!(f, "\n")?
155        }
156        Ok(())
157    }
158}
159
160#[derive(Clone, Copy)]
161pub struct Room {
162    position: Point,
163    size: Point,
164}
165
166impl Room {
167    pub fn new(position: Point, size: Point) -> Self {
168        Room { position, size }
169    }
170    /// Calculates if two rooms intersect with one another.
171    /// Rust port of https://stackoverflow.com/questions/20925818/algorithm-to-check-if-two-boxes-overlap
172    pub fn intersects(&self, other: &Room) -> bool {
173        let x_intersect: bool = ((self.position.x + self.size.x) > other.position.x)
174            && (other.position.x + (other.size.x) > self.position.x);
175        let y_intersect: bool = ((self.position.y + self.size.y) > other.position.y)
176            && (other.position.y + (other.size.y) > self.position.y);
177        x_intersect && y_intersect
178    }
179}
180
181pub struct Leaf {
182    /// Top left corner (x,y)
183    position: Point,
184    /// Size of leaf (x,y)
185    size: Point,
186    left_child: Option<Box<Leaf>>,
187    right_child: Option<Box<Leaf>>,
188    room: Option<Room>,
189    corridors: Vec<Room>,
190}
191
192impl Leaf {
193    pub fn new(position: Point, size: Point) -> Self {
194        Leaf {
195            position,
196            size,
197            left_child: None,
198            right_child: None,
199            room: None,
200            corridors: Vec::new(),
201        }
202    }
203    pub fn split(
204        &mut self,
205        rng: &mut StdRng,
206        min_room_size: &Point,
207        max_room_size: &Point,
208    ) -> bool {
209        if self.left_child.is_some() || self.right_child.is_some() {
210            return false;
211        }
212        // init
213        let split_horizontal: bool;
214        // determine split direction
215        if (self.size.x > self.size.y) && (self.size.x as f32 / self.size.y as f32) >= 1.25 {
216            split_horizontal = false;
217        } else if (self.size.y > self.size.x) && (self.size.y as f32 / self.size.x as f32) >= 1.25 {
218            split_horizontal = true;
219        } else {
220            split_horizontal = rng.gen_bool(0.5);
221        };
222
223        // determine where we are going to split
224        let split = if split_horizontal == true {
225            rng.gen_range(min_room_size.x..=max_room_size.y as usize)
226        } else {
227            rng.gen_range(min_room_size.y..=max_room_size.y as usize)
228        };
229        // split
230        if split_horizontal {
231            self.left_child = Some(Box::new(Leaf::new(
232                Point::new(self.position.x, self.position.y),
233                Point::new(self.size.x, split),
234            )));
235            if split >= self.size.y {
236                return false;
237            } else {
238                self.right_child = Some(Box::new(Leaf::new(
239                    Point::new(self.position.x, self.position.y + split),
240                    Point::new(self.size.x, self.size.y - split),
241                )));
242            }
243        } else {
244            self.left_child = Some(Box::new(Leaf::new(
245                Point::new(self.position.x, self.position.y),
246                Point::new(split, self.size.y),
247            )));
248            if split >= self.size.x {
249                return false;
250            } else {
251                self.right_child = Some(Box::new(Leaf::new(
252                    Point::new(self.position.x + split, self.position.y),
253                    Point::new(self.size.x - split, self.size.y),
254                )));
255            }
256        }
257        true
258    }
259
260    fn is_leaf(&self) -> bool {
261        match self.left_child {
262            None => match self.right_child {
263                None => true,
264                Some(_) => false,
265            },
266            Some(_) => false,
267        }
268    }
269
270    fn generate(&mut self, rng: &mut StdRng, min_room_size: &Point, max_room_size: &Point) {
271        if self.is_leaf() {
272            if self.split(rng, min_room_size, max_room_size) {
273                self.left_child
274                    .as_mut()
275                    .unwrap()
276                    .generate(rng, min_room_size, max_room_size);
277                self.right_child
278                    .as_mut()
279                    .unwrap()
280                    .generate(rng, min_room_size, max_room_size);
281            }
282        }
283    }
284
285    fn create_rooms(&mut self, rng: &mut StdRng, min_room_size: &Point) {
286        // If it is not a end leaf.
287        if let Some(ref mut room) = self.left_child {
288            room.as_mut().create_rooms(rng, min_room_size);
289        };
290        // If it is not a end leaf.
291        if let Some(ref mut room) = self.right_child {
292            room.as_mut().create_rooms(rng, min_room_size);
293        };
294
295        // if last level, add a room
296        if self.is_leaf() {
297            let width: usize;
298            if min_room_size.x >= self.size.x {
299                width = min_room_size.x;
300            } else {
301                width = rng.gen_range(min_room_size.x..=self.size.x);
302            }
303
304            let height: usize;
305            if min_room_size.y >= self.size.y {
306                height = min_room_size.y;
307            } else {
308                height = rng.gen_range(min_room_size.y..=self.size.y);
309            }
310            let x: usize;
311            if self.size.x as f32 - width as f32 <= 0.0 {
312                x = 0
313            } else {
314                x = rng.gen_range(0..=(self.size.x - width));
315            }
316
317            let y: usize;
318            if self.size.y as f32 - height as f32 <= 0.0 {
319                y = 0
320            } else {
321                y = rng.gen_range(0..=(self.size.y - height));
322            }
323            self.room = Some(Room::new(
324                Point::new(x + self.position.x, y + self.position.y),
325                Point::new(width, height),
326            ));
327        }
328        if let (Some(ref mut left), Some(ref mut right)) =
329            (&mut self.left_child, &mut self.right_child)
330        {
331            create_corridors(rng, left, right);
332        };
333    }
334
335    fn get_room(&self) -> Option<Room> {
336        if self.is_leaf() {
337            return self.room;
338        }
339
340        let mut left_room: Option<Room> = None;
341        let mut right_room: Option<Room> = None;
342
343        if let Some(ref room) = self.left_child {
344            left_room = room.get_room();
345        }
346
347        if let Some(ref room) = self.right_child {
348            right_room = room.get_room();
349        }
350        match (left_room, right_room) {
351            (None, None) => None,
352            (Some(room), _) => Some(room),
353            (_, Some(room)) => Some(room),
354        }
355    }
356
357    fn iter(&self) -> LeafIterator {
358        LeafIterator::new(&self)
359    }
360}
361
362fn create_corridors(rng: &mut StdRng, left: &mut Box<Leaf>, right: &mut Box<Leaf>) {
363    if let (Some(left_room), Some(right_room)) = (left.get_room(), right.get_room()) {
364        // Get random x position and y position
365        let left_point = Point::new(
366            rng.gen_range(left_room.position.x..=(left_room.position.x + left_room.size.x)),
367            rng.gen_range(left_room.position.y..=(left_room.position.y + left_room.size.y)),
368        );
369        // Get random x position and y position
370        let right_point = Point::new(
371            rng.gen_range(right_room.position.x..=(right_room.position.x + right_room.size.x)),
372            rng.gen_range(right_room.position.y..=(right_room.position.y + right_room.size.y)),
373        );
374
375        if left_point.y <= right_point.y {
376            left.corridors.push(vert_corridor(left_point.x, left_point.y, right_point.y));
377        } else {
378            left.corridors.push(vert_corridor(left_point.x, right_point.y, left_point.y));
379        }
380
381        if left_point.x <= right_point.x {
382            left.corridors.push(horz_corridor(left_point.x, right_point.y, right_point.x));
383        } else {
384            left.corridors.push(horz_corridor(right_point.x, right_point.y, left_point.x));
385        }
386    };
387}
388
389fn horz_corridor(start_x: usize, start_y: usize, end_x: usize) -> Room {
390    Room::new(Point { x: start_x, y: start_y }, Point { x : end_x - start_x + 1, y : 1})
391}
392
393fn vert_corridor(start_x: usize, start_y: usize, end_y: usize) -> Room {
394    Room::new(Point { x: start_x, y: start_y }, Point { x: 1, y : end_y - start_y})
395}
396
397struct LeafIterator<'a> {
398    current_node: Option<&'a Leaf>,
399    right_nodes: Vec<&'a Leaf>,
400}
401
402impl<'a> LeafIterator<'a> {
403    fn new(root: &'a Leaf) -> LeafIterator<'a> {
404        let mut iter = LeafIterator {
405            right_nodes: vec![],
406            current_node: None,
407        };
408
409        iter.add_left_subtree(root);
410        iter
411    }
412
413    fn add_left_subtree(&mut self, node: &'a Leaf) {
414        if let Some(ref left) = node.left_child {
415            self.right_nodes.push(&*left);
416        }
417        if let Some(ref right) = node.right_child {
418            self.right_nodes.push(&*right);
419        }
420
421        self.current_node = Some(node);
422    }
423}
424
425impl<'a> Iterator for LeafIterator<'a> {
426    type Item = &'a Leaf;
427
428    fn next(&mut self) -> Option<Self::Item> {
429        let result = self.current_node.take();
430        if let Some(rest) = self.right_nodes.pop() {
431            self.add_left_subtree(rest);
432        }
433
434        match result {
435            Some(leaf) => Some(&*leaf),
436            None => None,
437        }
438    }
439}
440
441#[cfg(test)]
442mod test {
443    use rand::SeedableRng;
444
445    use crate::Point;
446
447    use super::{Leaf, Room};
448
449    // === Room intersect testing ===
450    #[test]
451    fn non_intersect_touching_walls() {
452        let room_one = Room::new(Point { x: 0, y: 0 }, Point { x: 10, y: 10 });
453        let room_two = Room::new(Point { x: 10, y: 10 }, Point { x: 10, y: 10 });
454        assert_eq!(false, room_one.intersects(&room_two));
455    }
456
457    #[test]
458    fn non_intersect_nothing_touch() {
459        let room_one = Room::new(Point { x: 0, y: 0 }, Point { x: 10, y: 10 });
460        let room_two = Room::new(Point { x: 15, y: 15 }, Point { x: 10, y: 10 });
461        assert_eq!(false, room_one.intersects(&room_two));
462    }
463
464    #[test]
465    fn intersect_top_right_corner() {
466        let room_one = Room::new(Point { x: 0, y: 0 }, Point { x: 10, y: 10 });
467        let room_two = Room::new(Point { x: 9, y: 9 }, Point { x: 123, y: 321 });
468        assert_eq!(true, room_one.intersects(&room_two));
469    }
470
471    #[test]
472    fn full_intersect() {
473        let room_one = Room::new(Point { x: 0, y: 0 }, Point { x: 10, y: 10 });
474        let room_two = Room::new(Point { x: 0, y: 0 }, Point { x: 20, y: 20 });
475        assert_eq!(true, room_one.intersects(&room_two));
476    }
477
478    #[test]
479    fn intersect_up_shift() {
480        let room_one = Room::new(Point { x: 0, y: 0 }, Point { x: 10, y: 10 });
481        let room_two = Room::new(Point { x: 0, y: 5 }, Point { x: 10, y: 10 });
482        assert_eq!(true, room_one.intersects(&room_two));
483    }
484    // === Leaf split testing ===
485    // Split conditions:
486    // - Will not split if leaf size (x or y) is less than 1/15  (x or y) of the map
487    // - Split horizontal if leaf height is >125% of width.
488    // - Split vertical if leaf width is >125%  of height.
489    // - Random split if both leaf width and height are within 125% of each other.
490    #[test]
491    fn test_no_split_too_small_horz() {}
492
493    #[test]
494    fn test_no_split_too_small_vert() {}
495
496    #[test]
497    fn test_leaf_split_horz() {
498        // Vertically large room
499        let mut vert_room = Leaf::new(Point::new(0, 0), Point::new(20, 50));
500
501        let split = vert_room.split(
502            &mut SeedableRng::seed_from_u64(123),
503            &Point::new(10, 10),
504            &Point::new(15, 15),
505        );
506        assert_eq!(split, true);
507        let left_child = vert_room.left_child.unwrap();
508        let right_child = vert_room.right_child.unwrap();
509
510        // Now that we have split horizontally, the left child should be moved to the
511        assert_eq!(left_child.position, vert_room.position);
512        assert_eq!(
513            left_child.size.x + left_child.size.y <= vert_room.size.x + vert_room.size.y,
514            true
515        );
516
517        assert_eq!(right_child.position.y >= vert_room.position.y, true);
518        assert_eq!(
519            right_child.size.x + right_child.size.y <= vert_room.size.x + vert_room.size.y,
520            true
521        );
522    }
523    // ==============================
524}