From ab1392bba09f8b58dc496b3a3f5b980bc5b475c8 Mon Sep 17 00:00:00 2001 From: Freya Murphy Date: Wed, 12 Nov 2025 11:01:21 -0500 Subject: dungeon: switch to only using small rng from rand --- dungeon/src/bsp.rs | 29 +++++----- dungeon/src/entity.rs | 4 +- dungeon/src/lib.rs | 43 ++++++++------- dungeon/src/map.rs | 80 +++++---------------------- dungeon/tests/bsp.rs | 135 +++++++++++++++++++++++++++++++++++++++++++++ dungeon/tests/bsp_tests.rs | 128 ------------------------------------------ 6 files changed, 189 insertions(+), 230 deletions(-) create mode 100644 dungeon/tests/bsp.rs delete mode 100644 dungeon/tests/bsp_tests.rs (limited to 'dungeon') diff --git a/dungeon/src/bsp.rs b/dungeon/src/bsp.rs index e0ac001..09b4433 100644 --- a/dungeon/src/bsp.rs +++ b/dungeon/src/bsp.rs @@ -3,7 +3,7 @@ use core::panic; use rand::prelude::IndexedRandom; -use rand::{Rng, SeedableRng}; +use rand::{Rng, rngs::SmallRng}; use std::cmp; // for min/max use crate::Floor; @@ -40,7 +40,7 @@ impl Rect { } /// Returns a random point in this rectangle. - fn random_point(&self, rng: &mut R) -> Pos { + fn random_point(&self, rng: &mut SmallRng) -> Pos { let rx = rng.random_range(self.x..(self.x + self.w)); let ry = rng.random_range(self.y..(self.y + self.h)); Pos::new(rx, ry).unwrap_or(self.center()) @@ -70,7 +70,7 @@ impl Node { /// 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 { + fn split(&mut self, rng: &mut SmallRng) -> bool { // Already split if self.left.is_some() || self.right.is_some() { return false; @@ -150,7 +150,7 @@ impl Node { /// Create a room inside this node (called for leaves). /// Room size and position chosen randomly. - fn create_room(&mut self, rng: &mut R) { + fn create_room(&mut self, rng: &mut SmallRng) { if self.left.is_some() || self.right.is_some() { // This is not a leaf if let Some(left) = &mut self.left { @@ -213,7 +213,7 @@ impl Node { /// Return a random point for a room in this subtree: /// if node has a room, return a randiom point in it, else try left then right. - fn random_point_in_room(&self, rng: &mut R) -> Pos { + fn random_point_in_room(&self, rng: &mut SmallRng) -> Pos { // Base case: if this node has a room, return a random point in it if let Some(room) = &self.room { return room.random_point(rng); @@ -230,7 +230,7 @@ impl Node { /// Connect rooms of child nodes recursively and collect corridors to carve. /// returns corridors: output vector of (Pos, Pos) pairs to connect - fn connect_children(&self, rng: &mut rand::rngs::StdRng) -> Vec<(Pos, Pos)> { + fn connect_children(&self, rng: &mut SmallRng) -> Vec<(Pos, Pos)> { let mut corridors = Vec::new(); if let (Some(left), Some(right)) = (&self.left, &self.right) { @@ -288,9 +288,7 @@ fn carve_v_corridor(tiles: &mut [Tile; TILE_COUNT], y1: u16, y2: u16, x: u16) { /// Top-level generator function for the dungeon using BSP. /// Returns a `Floor` -pub fn generate(seed: u64) -> Floor { - let mut rng = rand::rngs::StdRng::seed_from_u64(seed); - +pub fn generate(seed: u64, mut rng: SmallRng) -> Floor { // Initialize all tiles to walls let mut tiles_box: Box<[Tile; TILE_COUNT]> = Box::new([Tile::Wall; TILE_COUNT]); @@ -403,6 +401,8 @@ pub fn generate(seed: u64) -> Floor { /// BSP Unit Tests #[cfg(test)] mod tests { + use rand::SeedableRng; + use super::*; use crate::map::MAP_SIZE; @@ -418,7 +418,7 @@ mod tests { 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 mut rng = SmallRng::seed_from_u64(12345); let splitted = node.split(&mut rng); assert!(splitted); assert!(node.left.is_some()); @@ -429,7 +429,7 @@ mod tests { 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); + let mut rng = SmallRng::seed_from_u64(12345); node.create_room(&mut rng); assert!(node.room.is_some()); match &node.room { @@ -447,7 +447,7 @@ mod tests { 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); + let mut rng = SmallRng::seed_from_u64(12345); node.split(&mut rng); let mut leaves = Vec::new(); node.collect_leaves(&mut leaves); @@ -458,7 +458,7 @@ mod tests { 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); + let mut rng = SmallRng::seed_from_u64(12345); node.split(&mut rng); node.create_room(&mut rng); let corridors = node.connect_children(&mut rng); @@ -468,7 +468,8 @@ mod tests { #[test] fn test_generate() { let seed = 12345u64; - let floor = generate(seed); + let rng = SmallRng::seed_from_u64(seed); + let floor = generate(seed, rng); // Check that tiles contain some Room tiles let room_count = floor.tiles().iter().filter(|&&t| t == Tile::Room).count(); assert!(room_count > 0); diff --git a/dungeon/src/entity.rs b/dungeon/src/entity.rs index 82042b5..de21c28 100644 --- a/dungeon/src/entity.rs +++ b/dungeon/src/entity.rs @@ -93,8 +93,8 @@ impl EnemyMoveState { pub fn new_roam(starting_pos: Pos, floor: &mut Floor) -> Self { let mut loop_index = 0; loop { - let dir = Direction::random(floor.rand()); - let dist = floor.rand().random_range(MIN_ROAM_DIST..=MAX_ROAM_DIST); + let dir = floor.rng().random(); + let dist = floor.rng().random_range(MIN_ROAM_DIST..=MAX_ROAM_DIST); if let Some(p) = starting_pos.step_by(dir, dist) { if !floor.get(p).is_wall() { diff --git a/dungeon/src/lib.rs b/dungeon/src/lib.rs index 4767942..7bc9c05 100644 --- a/dungeon/src/lib.rs +++ b/dungeon/src/lib.rs @@ -14,42 +14,35 @@ pub use map::*; pub use player_input::*; pub use pos::*; +use rand::{ + SeedableRng, TryRngCore, + rngs::{OsRng, SmallRng}, +}; + /// The `Dungeon` type represents the game state of the /// dungeon crawler. -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, Clone)] pub struct Dungeon { pub floor: Floor, pub player: Player, pub enemies: Vec, } impl Dungeon { - /// Creates a new `Dungeon`. - /// - /// # Examples - /// - /// ```no_run - /// use dungeon::Dungeon; - /// - /// let dungeon = Dungeon::new(); - /// ``` - #[must_use] - pub fn new() -> Self { - Self::from(Floor::generate()) - } - /// Creates a new `Dungeon` with a provided seed. /// /// # Examples /// /// ```no_run /// use dungeon::Dungeon; + /// use rand::{SeedableRng, rngs::SmallRng}; /// /// let seed = 234690523482u64; - /// let dungeon = Dungeon::new_seeded(seed); + /// let rng = SmallRng::seed_from_u64(seed); + /// let dungeon = Dungeon::with_rng(seed, rng); /// ``` #[must_use] - pub fn new_seeded(seed: u64) -> Self { - Self::from(Floor::generate_seeded(seed)) + pub fn with_rng(seed: u64, rng: SmallRng) -> Self { + Self::from(bsp::generate(seed, rng)) } /// Returns the current position of the camera (viewer) @@ -110,8 +103,20 @@ impl Dungeon { } } impl Default for Dungeon { + /// Creates a new `Dungeon` with a default rng. + /// + /// # Examples + /// + /// ```no_run + /// use dungeon::Dungeon; + /// + /// let dungeon = Dungeon::default(); + /// ``` fn default() -> Self { - Self::from(Floor::default()) + let seed = OsRng.try_next_u64().unwrap_or(0); + let rng = SmallRng::seed_from_u64(seed); + let floor = bsp::generate(seed, rng); + Self::from(floor) } } impl From for Dungeon { diff --git a/dungeon/src/map.rs b/dungeon/src/map.rs index 7a6cc45..b1c5f16 100644 --- a/dungeon/src/map.rs +++ b/dungeon/src/map.rs @@ -1,7 +1,7 @@ //! The `map` module contains structures of the dungeon game map //! including the current `Floor`, and map `Tile`. -use rand::{Rng, SeedableRng, TryRngCore, rngs::StdRng}; +use rand::{Rng, rngs::SmallRng}; use strum::IntoEnumIterator; use strum_macros::EnumIter; @@ -11,8 +11,7 @@ use std::{ hash::{DefaultHasher, Hash, Hasher}, }; -use crate::bsp; -use crate::{const_pos, pos::Pos}; +use crate::pos::Pos; /// `MAP_SIZE` is the size of the size of the dungeon grid. pub const MAP_SIZE: u16 = 48; @@ -71,7 +70,7 @@ impl Display for Tile { /// The `Floor` type represents the current playing /// grid of the dungeon. It contains the tiles of the /// grid, and the starting position of the player. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Debug, Clone)] pub struct Floor { /// The dungeon grid tiles: Box<[Tile; TILE_COUNT]>, @@ -80,7 +79,7 @@ pub struct Floor { /// The seed used when generating the dungeon grid seed: u64, /// Seeded rng by `seed` - rng: StdRng, + rng: SmallRng, /// The computed hash of the tile map hash: RefCell, /// If the tiles are dirty (hash needs to be recomputed) @@ -92,7 +91,7 @@ impl Floor { tiles: Box<[Tile; TILE_COUNT]>, player_start: Pos, seed: u64, - rng: StdRng, + rng: SmallRng, ) -> Self { Self { tiles, @@ -104,43 +103,6 @@ impl Floor { } } - /// Generates a dungeon `Floor` using binary space partitioning. - /// - /// # Examples - /// - /// ```no_run - /// use dungeon::Floor; - /// - /// let floor = Floor::generate(); - /// ``` - #[must_use] - pub fn generate() -> Self { - let mut rng = rand::rngs::OsRng; - let seed = rng.try_next_u64().unwrap_or(0); - Self::generate_seeded(seed) - } - - /// Generates a dungeon `Floor` using binary space partitioning provided with a seed. - /// - /// The provided seed is used for randomness in the binary space partitioning - /// algorithm. - /// - /// # Examples - /// - /// ```no_run - /// use dungeon::Floor; - /// - /// /// here is our very seedy seed - /// let seed = 2893249402u64; - /// let floor_1 = Floor::generate_seeded(seed); - /// let floor_2 = Floor::generate_seeded(seed); - /// assert_eq!(floor_1, floor_2); // both floors will be identical - /// ``` - #[must_use] - pub fn generate_seeded(seed: u64) -> Self { - bsp::generate(seed) - } - /// Returns the start position of the player #[must_use] pub const fn player_start(&self) -> Pos { @@ -214,7 +176,7 @@ impl Floor { #[must_use] pub fn random_walkable_pos(&mut self) -> Pos { loop { - let pos = self.rand().random(); + let pos = self.rng().random(); if !self.get(pos).is_walkable() { continue; } @@ -224,35 +186,18 @@ impl Floor { /// Returns the random number gen for the `Floor` #[must_use] - pub fn rand(&mut self) -> &mut impl Rng { + pub fn rng(&mut self) -> &mut SmallRng { &mut self.rng } } -impl Default for Floor { - /// Returns a floor with a single set of walls on the map border - fn default() -> Self { - let player_start = const_pos!(1, 1); - let mut tiles = Box::new([Tile::Room; TILE_COUNT]); - let seed = 0u64; - - for pos in Pos::values() { - if pos.is_border() { - tiles[pos.idx()] = Tile::Wall; - } - } - - let rng = rand::rngs::StdRng::seed_from_u64(seed); - - Self::from_parts(tiles, player_start, seed, rng) - } -} impl Display for Floor { /// Display the floor as a string for debugging /// /// # Examples /// ```no_run - /// use dungeon::Floor; - /// let floor = Floor::generate(); + /// use dungeon::Dungeon; + /// let dungeon = Dungeon::default(); + /// let floor = &dungeon.floor; /// println!("{floor}"); /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -278,12 +223,13 @@ impl Display for Floor { /// Tests #[cfg(test)] mod tests { - use super::*; + use crate::Dungeon; // Test floor printing #[test] fn test_floor_display() { - let floor = Floor::generate(); + let dungeon = Dungeon::default(); + let floor = &dungeon.floor; // Print the display for visual inspection println!("{floor}"); } diff --git a/dungeon/tests/bsp.rs b/dungeon/tests/bsp.rs new file mode 100644 index 0000000..60198a5 --- /dev/null +++ b/dungeon/tests/bsp.rs @@ -0,0 +1,135 @@ +//! Integration Tests for BSP dungeon generation +#[cfg(test)] +mod tests { + use dungeon::*; + use pos::Pos; + use rand::{Rng, SeedableRng, rngs::SmallRng}; + + /// Generate a set of test seeds for reproducibility with a seeded RNG + fn generate_test_seeds(seed: u64) -> Vec { + let mut rng = SmallRng::seed_from_u64(seed); + // Generate 100 random u64 seeds + (0..100).map(|_| rng.random_range(0..u64::MAX)).collect() + } + + /// Basic integration test for BSP generation + #[test] + fn test_bsp_integration() { + let test_seeds = generate_test_seeds(123456); + for seed in test_seeds { + let rng = SmallRng::seed_from_u64(seed); + let floor = bsp::generate(seed, rng); + // Basic integration test: ensure we get valid data + assert!(!floor.tiles().is_empty()); + } + } + + /// Test that BSP-generated floors have a valid player start + #[test] + fn test_bsp_player_start() { + let test_seeds = generate_test_seeds(654321); + for seed in test_seeds { + let rng = SmallRng::seed_from_u64(seed); + let floor = bsp::generate(seed, rng); + // Ensure player start is a room tile + let start = floor.player_start(); + assert_eq!(floor.get(start), map::Tile::Room); + } + } + + /// Test that BSP-generated floors have at least one room tile + #[test] + fn test_bsp_2_or_more_rooms() { + let test_seeds = generate_test_seeds(111222); + for seed in test_seeds { + let rng = SmallRng::seed_from_u64(seed); + let floor = bsp::generate(seed, rng); + // Ensure we have at least one room tile + let room_count = floor + .tiles() + .iter() + .filter(|&&tile| tile == map::Tile::Room) + .count(); + assert!( + room_count >= 1, + "Expected at least one room tile, found {room_count}" + ); + } + } + + /// Test that BSP-generated floors have walls on the borders + #[test] + fn test_bsp_walls_on_borders() { + let test_seeds = generate_test_seeds(777888); + for seed in test_seeds { + let rng = SmallRng::seed_from_u64(seed); + let floor = bsp::generate(seed, rng); + // Go through all tiles, and ensure border tiles are walls + for pos in Pos::values() { + if pos.is_border() { + assert_eq!( + floor.get(pos), + map::Tile::Wall, + "Expected wall at border position {pos:?}" + ); + } + } + } + } + + // Test that BSP-generated floors are reproducible with the same seed + #[test] + fn test_bsp_reproducibility() { + let test_seeds = generate_test_seeds(111111); + for seed in test_seeds { + let rng1 = SmallRng::seed_from_u64(seed); + let rng2 = SmallRng::seed_from_u64(seed); + let floor1 = bsp::generate(seed, rng1); + let floor2 = bsp::generate(seed, rng2); + assert_eq!( + floor1.tiles(), + floor2.tiles(), + "Tiles differ for same seed {seed}" + ); + assert_eq!( + floor1.player_start(), + floor2.player_start(), + "Player starts differ for same seed {seed}" + ); + } + } + + // Test that all `air` tiles (`Tile::Room` and `Tile::Hallway`) are reachable from the player start + #[test] + fn test_bsp_all_air_tiles_reachable() { + let test_seeds = generate_test_seeds(333444); + for seed in test_seeds { + check_air_tiles_reachable(seed); + } + } + + // Helper function to check that all air tiles are reachable from player start + fn check_air_tiles_reachable(seed: u64) { + let rng = SmallRng::seed_from_u64(seed); + let floor = bsp::generate(seed, rng); + + // BFS to find all reachable air tiles + let mut visited = vec![false; TILE_COUNT]; + let mut queue = vec![floor.player_start()]; + visited[floor.player_start().idx()] = true; + while let Some(pos) = queue.pop() { + for neighbor in pos.neighbors() { + let idx = neighbor.idx(); + if !visited[idx] && floor.get(neighbor) != map::Tile::Wall { + visited[idx] = true; + queue.push(neighbor); + } + } + } + for (i, &tile) in floor.tiles().iter().enumerate() { + if tile == map::Tile::Room || tile == map::Tile::Hallway { + assert!(visited[i], "Unreachable air tile at index {i}"); + } + } + } +} diff --git a/dungeon/tests/bsp_tests.rs b/dungeon/tests/bsp_tests.rs deleted file mode 100644 index 09d2d3a..0000000 --- a/dungeon/tests/bsp_tests.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Integration Tests for BSP dungeon generation -#[cfg(test)] -mod tests { - use dungeon::*; - use pos::Pos; - use rand::{Rng, SeedableRng}; - - /// Generate a set of test seeds for reproducibility with a seeded RNG - fn generate_test_seeds(seed: u64) -> Vec { - let mut rng = rand::rngs::StdRng::seed_from_u64(seed); - // Generate 100 random u64 seeds - (0..100).map(|_| rng.random_range(0..u64::MAX)).collect() - } - - /// Basic integration test for BSP generation - #[test] - fn test_bsp_integration() { - let test_seeds = generate_test_seeds(123456); - for seed in test_seeds { - let floor = bsp::generate(seed); - // Basic integration test: ensure we get valid data - assert!(!floor.tiles().is_empty()); - } - } - - /// Test that BSP-generated floors have a valid player start - #[test] - fn test_bsp_player_start() { - let test_seeds = generate_test_seeds(654321); - for seed in test_seeds { - let floor = bsp::generate(seed); - // Ensure player start is a room tile - let start = floor.player_start(); - assert_eq!(floor.get(start), map::Tile::Room); - } - } - - /// Test that BSP-generated floors have at least one room tile - #[test] - fn test_bsp_2_or_more_rooms() { - let test_seeds = generate_test_seeds(111222); - for seed in test_seeds { - let floor = bsp::generate(seed); - // Ensure we have at least one room tile - let room_count = floor - .tiles() - .iter() - .filter(|&&tile| tile == map::Tile::Room) - .count(); - assert!( - room_count >= 1, - "Expected at least one room tile, found {room_count}" - ); - } - } - - /// Test that BSP-generated floors have walls on the borders - #[test] - fn test_bsp_walls_on_borders() { - let test_seeds = generate_test_seeds(777888); - for seed in test_seeds { - let floor = bsp::generate(seed); - // Go through all tiles, and ensure border tiles are walls - for pos in Pos::values() { - if pos.is_border() { - assert_eq!( - floor.get(pos), - map::Tile::Wall, - "Expected wall at border position {pos:?}" - ); - } - } - } - } - - // Test that BSP-generated floors are reproducible with the same seed - #[test] - fn test_bsp_reproducibility() { - let test_seeds = generate_test_seeds(111111); - for seed in test_seeds { - let floor1 = bsp::generate(seed); - let floor2 = bsp::generate(seed); - assert_eq!( - floor1.tiles(), - floor2.tiles(), - "Tiles differ for same seed {seed}" - ); - assert_eq!( - floor1.player_start(), - floor2.player_start(), - "Player starts differ for same seed {seed}" - ); - } - } - - // Test that all `air` tiles (`Tile::Room` and `Tile::Hallway`) are reachable from the player start - #[test] - fn test_bsp_all_air_tiles_reachable() { - let test_seeds = generate_test_seeds(333444); - for seed in test_seeds { - check_air_tiles_reachable(seed); - } - } - - // Helper function to check that all air tiles are reachable from player start - fn check_air_tiles_reachable(seed: u64) { - let floor = bsp::generate(seed); - - // BFS to find all reachable air tiles - let mut visited = vec![false; TILE_COUNT]; - let mut queue = vec![floor.player_start()]; - visited[floor.player_start().idx()] = true; - while let Some(pos) = queue.pop() { - for neighbor in pos.neighbors() { - let idx = neighbor.idx(); - if !visited[idx] && floor.get(neighbor) != map::Tile::Wall { - visited[idx] = true; - queue.push(neighbor); - } - } - } - for (i, &tile) in floor.tiles().iter().enumerate() { - if tile == map::Tile::Room || tile == map::Tile::Hallway { - assert!(visited[i], "Unreachable air tile at index {i}"); - } - } - } -} -- cgit v1.2.3-freya