//! The `bsp` module implements a Binary-Space Partitioning based dungeon generator. //! Produces a tile array and a player start position for a given seed. use rand::{Rng, SeedableRng}; use std::cmp; // for min/max use crate::map::{MAP_SIZE_USIZE, TILE_COUNT, Tile}; use crate::{const_pos, pos::Pos}; /// `MIN_LEAF_SIZE` is the minimum width/height for a leaf to be considered "splittable". const MIN_LEAF_SIZE: usize = 8; /// `MIN_ROOM_SIZE` is the minimum room size (w/h). const MIN_ROOM_SIZE: usize = 3; /// `MAX_ROOM_SIZE` is the maximum room size (w/h). const MAX_ROOM_SIZE: usize = 12; /// The `Rect` type represents a rectangle inside the map (x,y,width,height). /// (x,y) is the top-left corner. #[derive(Debug, Clone, Copy)] struct Rect { x: usize, y: usize, w: usize, h: usize, } // since this is all "internal", you likely have to use unit tests and not doctests impl Rect { fn new(x: usize, y: usize, w: usize, h: usize) -> Self { Self { x, y, w, h } } /// Returns the center point (cx, cy) of the rectangle. fn center(&self) -> (usize, usize) { let cx = self.x + self.w / 2; let cy = self.y + self.h / 2; (cx, cy) } } /// The `Node` type represents a node in the BSP tree. /// Has optional left and right children, and an optional room rectangle. #[derive(Debug)] struct Node { rect: Rect, // Area this node covers left: Option>, // Left child (if not leaf) right: Option>, // Right child (if not leaf) room: Option, // Room created in this node (if leaf) } impl Node { fn new(rect: Rect) -> Self { Self { rect, left: None, right: None, room: None, } } /// Try to split the node. Returns true if split happened. /// Splitting is done either horizontally or vertically, /// depending on the dimensions of the node. fn split(&mut self, rng: &mut R) -> bool { // Already split if self.left.is_some() || self.right.is_some() { return false; } // Determine if we can split let can_split_h = self.rect.h >= (MIN_LEAF_SIZE * 2); let can_split_v = self.rect.w >= (MIN_LEAF_SIZE * 2); if !can_split_h && !can_split_v { return false; } // Choose orientation: prefer the longer side let split_h = if self.rect.w > self.rect.h { false } else if self.rect.h > self.rect.w { true } else { rng.random_bool(0.5) }; // Choose split coordinate with margin so each side can contain a room if split_h { // horizontal split => choose y // avoid too close to borders of rect let min = cmp::max( self.rect.y + MIN_ROOM_SIZE + 1, self.rect.y + MIN_LEAF_SIZE + 1, ); let max = cmp::min( self.rect.y + self.rect.h - MIN_ROOM_SIZE - 1, self.rect.y + self.rect.h - MIN_LEAF_SIZE - 1, ); if max <= min { return false; } // Add some heterogeneity: choose random position between min..max let split_y = rng.random_range(min..=max); let top_h = split_y - self.rect.y; let bottom_h = self.rect.h - top_h; if top_h < MIN_LEAF_SIZE || bottom_h < MIN_LEAF_SIZE { return false; } let top = Rect::new(self.rect.x, self.rect.y, self.rect.w, top_h); let bottom = Rect::new(self.rect.x, split_y, self.rect.w, bottom_h); self.left = Some(Box::new(Self::new(top))); self.right = Some(Box::new(Self::new(bottom))); } else { // vertical split => choose x let min = cmp::max( self.rect.x + MIN_ROOM_SIZE + 1, self.rect.x + MIN_LEAF_SIZE + 1, ); let max = cmp::min( self.rect.x + self.rect.w - MIN_ROOM_SIZE - 1, self.rect.x + self.rect.w - MIN_LEAF_SIZE - 1, ); if max <= min { return false; } let split_x = rng.random_range(min..=max); let left_w = split_x - self.rect.x; let right_w = self.rect.w - left_w; if left_w < MIN_LEAF_SIZE || right_w < MIN_LEAF_SIZE { return false; } let left_rect = Rect::new(self.rect.x, self.rect.y, left_w, self.rect.h); let right_rect = Rect::new(split_x, self.rect.y, right_w, self.rect.h); self.left = Some(Box::new(Self::new(left_rect))); self.right = Some(Box::new(Self::new(right_rect))); } true } /// Create a room inside this node (called for leaves). /// Room size and position chosen randomly. fn create_room(&mut self, rng: &mut R) { if self.left.is_some() || self.right.is_some() { // This is not a leaf if let Some(left) = &mut self.left { left.create_room(rng); } if let Some(right) = &mut self.right { right.create_room(rng); } return; } // Leaf: choose room size // Ensure room fits inside rect with at least 1 tile border to walls let max_w = (self.rect.w - 2).max(MIN_ROOM_SIZE); let max_h = (self.rect.h - 2).max(MIN_ROOM_SIZE); let room_w = rng.random_range(MIN_ROOM_SIZE..=max_w.min(MAX_ROOM_SIZE)); let room_h = rng.random_range(MIN_ROOM_SIZE..=max_h.min(MAX_ROOM_SIZE)); // Choose position inside rect, leaving at least 1 tile to the border let x_range = (self.rect.x + 1)..=(self.rect.x + self.rect.w - 1 - room_w); let y_range = (self.rect.y + 1)..=(self.rect.y + self.rect.h - 1 - room_h); // The ranges should be valid; if not, fallback to center let rx = if x_range.is_empty() { self.rect.x + (self.rect.w.saturating_sub(room_w)) / 2 } else { rng.random_range(x_range) }; let ry = if y_range.is_empty() { self.rect.y + (self.rect.h.saturating_sub(room_h)) / 2 } else { rng.random_range(y_range) }; self.room = Some(Rect::new(rx, ry, room_w, room_h)); } /// Return a vector of references to all leaf nodes (in-order). fn collect_leaves<'a>(&'a self, out: &mut Vec<&'a Self>) { if self.left.is_none() && self.right.is_none() { out.push(self); return; } if let Some(left) = &self.left { left.collect_leaves(out); } if let Some(right) = &self.right { right.collect_leaves(out); } } /// Return the "room center" for this subtree: if node has a room, return its center, /// else try left then right. fn room_center(&self) -> Option<(usize, usize)> { if let Some(room) = &self.room { return Some(room.center()); } if let Some(left) = &self.left && let Some(center) = left.room_center() { return Some(center); } if let Some(right) = &self.right && let Some(center) = right.room_center() { return Some(center); } None } /// Return a point (x,y) on the border of the room in this node. fn random_point_in_room(&self, rng: &mut R) -> (usize, usize) { if let Some(room) = &self.room { let rx = rng.random_range(room.x..(room.x + room.w)); let ry = rng.random_range(room.y..(room.y + room.h)); (rx, ry) } else { // No room: return center of rect self.rect.center() } } /// Connect rooms of child nodes recursively and collect corridors to carve. /// /// `corridors` is appended with pairs of points (x1,y1,x2,y2) describing corridors to carve. fn connect_children( &self, corridors: &mut Vec<(usize, usize, usize, usize)>, rng: &mut rand::rngs::StdRng, ) { if let (Some(left), Some(right)) = (&self.left, &self.right) { // Connect a room (or corridor endpoint) from left subtree to right subtree // Select a random point from each side let (x1, y1) = left.random_point_in_room(rng); let (x2, y2) = right.random_point_in_room(rng); corridors.push((x1, y1, x2, y2)); left.connect_children(corridors, rng); right.connect_children(corridors, rng); } // leaf: nothing to connect! } } /// Carve a room rectangle into the map (tile array) by setting tiles inside to Air. fn carve_room(tiles: &mut [Tile; TILE_COUNT], room: &Rect) { for yy in room.y..(room.y + room.h) { for xx in room.x..(room.x + room.w) { let idx = xx + yy * MAP_SIZE_USIZE; tiles[idx] = Tile::Air; } } } /// Carve a horizontal corridor from x1..=x2 at y (inclusive) fn carve_h_corridor(tiles: &mut [Tile; TILE_COUNT], x1: usize, x2: usize, y: usize) { let (sx, ex) = if x1 <= x2 { (x1, x2) } else { (x2, x1) }; for x in sx..=ex { let idx = x + y * MAP_SIZE_USIZE; tiles[idx] = Tile::Air; } } /// Carve a vertical corridor from y1..=y2 at x fn carve_v_corridor(tiles: &mut [Tile; TILE_COUNT], y1: usize, y2: usize, x: usize) { let (sy, ey) = if y1 <= y2 { (y1, y2) } else { (y2, y1) }; for y in sy..=ey { let idx = x + y * MAP_SIZE_USIZE; tiles[idx] = Tile::Air; } } /// Top-level generator function for the dungeon using BSP. /// Returns (tiles, player_start). pub fn generate(seed: u64) -> (Box<[Tile; TILE_COUNT]>, Pos) { let mut rng = rand::rngs::StdRng::seed_from_u64(seed); // Initialize all tiles to walls let mut tiles_box: Box<[Tile; TILE_COUNT]> = Box::new([Tile::Wall; TILE_COUNT]); // Rooms and coorridors will be carved as `Air` into the tile array. // Build root rect (whole map) let root_rect = Rect::new(0, 0, MAP_SIZE_USIZE, MAP_SIZE_USIZE); let mut root = Node::new(root_rect); // Repeatedly try splitting nodes to produce the tree. // Collect leaves and try to split them until no splits happen or we reach a cap. // Number of splits proportional to map size to avoid infinite loops. let max_attempts = 2000usize; let mut attempts = 0usize; while attempts < max_attempts { attempts += 1; // Collect mutable references to current leaves let mut leaves = vec![]; { // Collect leaves with a stack let mut stack = vec![&mut root]; while let Some(node) = stack.pop() { if node.left.is_none() && node.right.is_none() { leaves.push(node as *mut Node); } else { if let Some(left) = &mut node.left { stack.push(left); } if let Some(right) = &mut node.right { stack.push(right); } } } } let mut splitted_any = false; // Try splitting each leaf in order for &node_ptr in leaves.iter() { // Pointers obtained from boxed root remain valid while root lives // TODO: Store nodes in smart pointers to avoid unsafe? let node = unsafe { &mut *node_ptr }; // Attempt to split if possible if node.split(&mut rng) { splitted_any = true; } } // If no splits happened, we're done if !splitted_any { break; } } // Create rooms in all leaves root.create_room(&mut rng); // Carve all rooms into the tile array let mut leaves = vec![]; root.collect_leaves(&mut leaves); for leaf in leaves.iter() { if let Some(room) = &leaf.room { carve_room(&mut tiles_box, room); } } // Collect corridors (pairs of centers) by connecting children bottom-up let mut corridors = Vec::new(); root.connect_children(&mut corridors, &mut rng); // Carve corridors. For each corridor (x1,y1,x2,y2), carve straight or L-shape. for (x1, y1, x2, y2) in corridors { // If aligned horizontally or vertically, carve straight if x1 == x2 { carve_v_corridor(&mut tiles_box, y1, y2, x1); } else if y1 == y2 { carve_h_corridor(&mut tiles_box, x1, x2, y1); } else { // Choose whether to go horizontal then vertical or vertical then horizontal if rng.random_bool(0.5) { carve_h_corridor(&mut tiles_box, x1, x2, y1); carve_v_corridor(&mut tiles_box, y1, y2, x2); } else { carve_v_corridor(&mut tiles_box, y1, y2, x1); carve_h_corridor(&mut tiles_box, x1, x2, y2); } } } // Choose player start randomly in the center of one of the rooms // TODO: Better Pos creation #[expect(clippy::cast_possible_truncation)] let player_start = root .room_center() .map(|(cx, cy)| Pos::new(cx as u16, cy as u16).unwrap_or(const_pos!(1, 1))) .unwrap_or(const_pos!(1, 1)); // Choose random location among air tiles /* let mut player_start = Pos::new_unchecked(1, 1); let mut found = false; for pos in Pos::values() { if tiles_box[pos.idx()] == Tile::Air { player_start = pos; found = true; break; } } // If we didn't find any air (extremely unlikely), put start at (1,1) and carve it if !found { player_start = Pos::new_unchecked(1, 1); let idx = player_start.idx(); tiles_box[idx] = Tile::Air; }*/ // Return tiles and player_start (tiles_box, player_start) } /// BSP Unit Tests #[cfg(test)] mod tests { use super::*; use crate::map::MAP_SIZE; #[test] fn test_rect_center() { let rect = Rect::new(2, 2, 4, 4); let (cx, cy) = rect.center(); assert_eq!(cx, 4); assert_eq!(cy, 4); } #[test] fn test_node_split() { let rect = Rect::new(0, 0, 20, 20); let mut node = Node::new(rect); let mut rng = rand::rngs::StdRng::seed_from_u64(12345); let splitted = node.split(&mut rng); assert!(splitted); assert!(node.left.is_some()); assert!(node.right.is_some()); } #[test] fn test_node_create_room() { let rect = Rect::new(0, 0, 20, 20); let mut node = Node::new(rect); let mut rng = rand::rngs::StdRng::seed_from_u64(12345); node.create_room(&mut rng); assert!(node.room.is_some()); match &node.room { Some(room) => { assert!(room.w >= MIN_ROOM_SIZE && room.h >= MIN_ROOM_SIZE); assert!(room.x >= 1 && room.y >= 1); assert!(room.x + room.w < MAP_SIZE_USIZE); assert!(room.y + room.h < MAP_SIZE_USIZE); } None => panic!("Room should be created"), } } #[test] fn test_node_collect_leaves() { let rect = Rect::new(0, 0, 20, 20); let mut node = Node::new(rect); let mut rng = rand::rngs::StdRng::seed_from_u64(12345); node.split(&mut rng); let mut leaves = Vec::new(); node.collect_leaves(&mut leaves); assert!(!leaves.is_empty()); } #[test] fn test_node_connect_children() { let rect = Rect::new(0, 0, 20, 20); let mut node = Node::new(rect); let mut rng = rand::rngs::StdRng::seed_from_u64(12345); node.split(&mut rng); let mut corridors = Vec::new(); node.connect_children(&mut corridors, &mut rng); assert!(!corridors.is_empty()); } #[test] fn test_generate() { let seed = 12345u64; let (tiles, player_start) = generate(seed); // Check that tiles contain some Air tiles let air_count = tiles.iter().filter(|&&t| t == Tile::Air).count(); assert!(air_count > 0); // Check that player_start is within bounds assert!(player_start.xy().0 < MAP_SIZE); assert!(player_start.xy().1 < MAP_SIZE); } }