diff options
Diffstat (limited to 'dungeon')
| -rw-r--r-- | dungeon/src/bsp.rs | 33 | ||||
| -rw-r--r-- | dungeon/src/entity.rs | 7 | ||||
| -rw-r--r-- | dungeon/src/lib.rs | 17 | ||||
| -rw-r--r-- | dungeon/src/map.rs | 6 | ||||
| -rw-r--r-- | dungeon/src/rng.rs | 74 | ||||
| -rw-r--r-- | dungeon/tests/bsp.rs | 19 |
6 files changed, 117 insertions, 39 deletions
diff --git a/dungeon/src/bsp.rs b/dungeon/src/bsp.rs index 971f5fe..6fb2e51 100644 --- a/dungeon/src/bsp.rs +++ b/dungeon/src/bsp.rs @@ -2,13 +2,16 @@ //! Produces a tile array and a player start position for a given seed. use core::panic; +use rand::Rng; use rand::prelude::IndexedRandom; -use rand::{Rng, rngs::SmallRng}; use std::cmp::{self, Ordering}; // for min/max -use crate::Floor; -use crate::map::{MAP_SIZE, TILE_COUNT, Tile}; -use crate::{const_pos, pos::Pos}; +use crate::{ + const_pos, + map::{Floor, MAP_SIZE, TILE_COUNT, Tile}, + pos::Pos, + rng::DungeonRng, +}; /// `MIN_LEAF_SIZE` is the minimum width/height for a leaf to be considered "splittable". const MIN_LEAF_SIZE: u16 = 8; @@ -40,7 +43,7 @@ impl Rect { } /// Returns a random point in this rectangle. - fn random_point(self, rng: &mut SmallRng) -> Pos { + fn random_point(self, rng: &mut DungeonRng) -> 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 +73,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 SmallRng) -> bool { + fn split(&mut self, rng: &mut DungeonRng) -> bool { // Already split if self.left.is_some() || self.right.is_some() { return false; @@ -148,7 +151,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 SmallRng) { + fn create_room(&mut self, rng: &mut DungeonRng) { if self.left.is_some() || self.right.is_some() { // This is not a leaf if let Some(left) = &mut self.left { @@ -211,7 +214,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 SmallRng) -> Pos { + fn random_point_in_room(&self, rng: &mut DungeonRng) -> 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); @@ -228,7 +231,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 SmallRng) -> Vec<(Pos, Pos)> { + fn connect_children(&self, rng: &mut DungeonRng) -> Vec<(Pos, Pos)> { let mut corridors = Vec::new(); if let (Some(left), Some(right)) = (&self.left, &self.right) { @@ -286,7 +289,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(rng: &mut SmallRng) -> Floor { +pub fn generate(rng: &mut DungeonRng) -> Floor { // Initialize all tiles to walls let mut tiles_box: Box<[Tile; TILE_COUNT]> = Box::new([Tile::Wall; TILE_COUNT]); @@ -413,7 +416,7 @@ mod tests { fn test_node_split() { let rect = Rect::new(0, 0, 20, 20); let mut node = Node::new(rect); - let mut rng = SmallRng::seed_from_u64(12345); + let mut rng = DungeonRng::seed_from_u64(12345); let splitted = node.split(&mut rng); assert!(splitted); assert!(node.left.is_some()); @@ -424,7 +427,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 = SmallRng::seed_from_u64(12345); + let mut rng = DungeonRng::seed_from_u64(12345); node.create_room(&mut rng); assert!(node.room.is_some()); match &node.room { @@ -442,7 +445,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 = SmallRng::seed_from_u64(12345); + let mut rng = DungeonRng::seed_from_u64(12345); node.split(&mut rng); let mut leaves = Vec::new(); node.collect_leaves(&mut leaves); @@ -453,7 +456,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 = SmallRng::seed_from_u64(12345); + let mut rng = DungeonRng::seed_from_u64(12345); node.split(&mut rng); node.create_room(&mut rng); let corridors = node.connect_children(&mut rng); @@ -463,7 +466,7 @@ mod tests { #[test] fn test_generate() { let seed = 12345u64; - let mut rng = SmallRng::seed_from_u64(seed); + let mut rng = DungeonRng::seed_from_u64(seed); let floor = generate(&mut rng); // Check that tiles contain some Room tiles let room_count = floor.tiles().iter().filter(|&&t| t == Tile::Room).count(); diff --git a/dungeon/src/entity.rs b/dungeon/src/entity.rs index b290a2f..3a8f131 100644 --- a/dungeon/src/entity.rs +++ b/dungeon/src/entity.rs @@ -2,13 +2,14 @@ use std::mem::take; -use rand::{Rng, rngs::SmallRng}; +use rand::Rng; use crate::{ Dungeon, astar, const_pos, map::Floor, player_input::PlayerInput, pos::{Direction, FPos, Pos}, + rng::DungeonRng, }; /// `PLAYER_FULL_HEALTH` is the starting health of the player entity @@ -97,7 +98,7 @@ impl EnemyMoveState { /// /// Will randomly pick a direction and number of tiles within roaming range to move. /// If an invalid tile is selected 50 times in a row, `EnemyMoveState::Idle` is returned instead. - pub fn roam(starting_pos: Pos, floor: &Floor, rng: &mut SmallRng) -> Self { + pub fn roam(starting_pos: Pos, floor: &Floor, rng: &mut DungeonRng) -> Self { let mut loop_index = 0; loop { let dir = rng.random(); @@ -274,7 +275,7 @@ impl Default for Player { struct Updater<'a> { floor: &'a Floor, - rng: &'a mut SmallRng, + rng: &'a mut DungeonRng, player_pos: Pos, input: PlayerInput, delta_time: f32, diff --git a/dungeon/src/lib.rs b/dungeon/src/lib.rs index 76a2bda..4e48b68 100644 --- a/dungeon/src/lib.rs +++ b/dungeon/src/lib.rs @@ -8,11 +8,9 @@ pub mod map; pub mod msg; pub mod player_input; pub mod pos; +pub mod rng; -use rand::{ - Rng, SeedableRng, TryRngCore, - rngs::{OsRng, SmallRng}, -}; +use rand::{Rng, SeedableRng, TryRngCore, rngs::OsRng}; use crate::{ entity::{Entity, Player}, @@ -20,6 +18,7 @@ use crate::{ msg::Message, player_input::PlayerInput, pos::FPos, + rng::DungeonRng, }; /// Lets the caller know what has @@ -45,8 +44,8 @@ pub struct Dungeon { pub enemies: Vec<Entity>, pub msg: Message, seed: u64, - level_rng: SmallRng, - game_rng: SmallRng, + level_rng: DungeonRng, + game_rng: DungeonRng, } impl Dungeon { /// Creates a new `Dungeon` with a provided seed. @@ -61,8 +60,8 @@ impl Dungeon { /// ``` #[must_use] pub fn new(seed: u64) -> Self { - let mut game_rng = SmallRng::seed_from_u64(seed); - let mut level_rng = SmallRng::seed_from_u64(game_rng.random()); + let mut game_rng = DungeonRng::seed_from_u64(seed); + let mut level_rng = DungeonRng::seed_from_u64(game_rng.random()); let floor = bsp::generate(&mut level_rng); let player = Player::new(floor.player_start()); let enemies = vec![]; @@ -110,7 +109,7 @@ impl Dungeon { /// Returns the runtime random number gen for the `Floor` #[must_use] - pub const fn rng(&mut self) -> &mut SmallRng { + pub const fn rng(&mut self) -> &mut DungeonRng { &mut self.game_rng } diff --git a/dungeon/src/map.rs b/dungeon/src/map.rs index 4928a29..0d05b73 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, rngs::SmallRng}; +use rand::Rng; use strum::IntoEnumIterator; use strum_macros::EnumIter; @@ -11,7 +11,7 @@ use std::{ hash::{DefaultHasher, Hash, Hasher}, }; -use crate::pos::Pos; +use crate::{pos::Pos, rng::DungeonRng}; /// `MAP_SIZE` is the size of the size of the dungeon grid. pub const MAP_SIZE: u16 = 48; @@ -155,7 +155,7 @@ impl Floor { /// Returns a random open (no wall) position #[must_use] - pub fn random_walkable_pos(&self, rng: &mut SmallRng) -> Pos { + pub fn random_walkable_pos(&self, rng: &mut DungeonRng) -> Pos { loop { let pos = rng.random(); if !self.get(pos).is_walkable() { diff --git a/dungeon/src/rng.rs b/dungeon/src/rng.rs new file mode 100644 index 0000000..bb9151b --- /dev/null +++ b/dungeon/src/rng.rs @@ -0,0 +1,74 @@ +//! Implements rng using the xoshiro256++ algorithm + +use rand::{ + SeedableRng, + rand_core::{RngCore, le}, +}; + +/// Deterministic pseudo random number generator. +/// +/// Is is a copy of `SmallRng` from `rand`, but is gurenteed +/// to be reproducible across versions and platforms. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct DungeonRng { + s: [u64; 4], +} +impl SeedableRng for DungeonRng { + // Fix to 256 bits. Changing this is a breaking change! + type Seed = [u8; 32]; + + #[inline] + fn from_seed(seed: Self::Seed) -> Self { + let mut state = [0; 4]; + le::read_u64_into(&seed, &mut state); + Self { s: state } + } + + #[inline] + fn seed_from_u64(mut state: u64) -> Self { + const PHI: u64 = 0x9e3779b97f4a7c15; + let mut s = [0; 4]; + for i in &mut s { + state = state.wrapping_add(PHI); + let mut z = state; + z = (z ^ (z >> 30)).wrapping_mul(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27)).wrapping_mul(0x94d049bb133111eb); + z = z ^ (z >> 31); + *i = z; + } + Self { s } + } +} +impl RngCore for DungeonRng { + #[inline] + fn next_u32(&mut self) -> u32 { + let val = self.next_u64(); + (val >> 32) as u32 + } + + #[inline] + fn next_u64(&mut self) -> u64 { + let res = self.s[0] + .wrapping_add(self.s[3]) + .rotate_left(23) + .wrapping_add(self.s[0]); + + let t = self.s[1] << 17; + + self.s[2] ^= self.s[0]; + self.s[3] ^= self.s[1]; + self.s[1] ^= self.s[2]; + self.s[0] ^= self.s[3]; + + self.s[2] ^= t; + + self.s[3] = self.s[3].rotate_left(45); + + res + } + + #[inline] + fn fill_bytes(&mut self, dst: &mut [u8]) { + le::fill_bytes_via_next(self, dst); + } +} diff --git a/dungeon/tests/bsp.rs b/dungeon/tests/bsp.rs index f7919c5..8ec7b3a 100644 --- a/dungeon/tests/bsp.rs +++ b/dungeon/tests/bsp.rs @@ -5,12 +5,13 @@ mod tests { bsp, map::{self, TILE_COUNT}, pos::Pos, + rng::DungeonRng, }; - use rand::{Rng, SeedableRng, rngs::SmallRng}; + use rand::{Rng, SeedableRng}; /// Generate a set of test seeds for reproducibility with a seeded RNG fn generate_test_seeds(seed: u64) -> Vec<u64> { - let mut rng = SmallRng::seed_from_u64(seed); + let mut rng = DungeonRng::seed_from_u64(seed); // Generate 100 random u64 seeds (0..100).map(|_| rng.random_range(0..u64::MAX)).collect() } @@ -20,7 +21,7 @@ mod tests { fn test_bsp_integration() { let test_seeds = generate_test_seeds(123456); for seed in test_seeds { - let mut rng = SmallRng::seed_from_u64(seed); + let mut rng = DungeonRng::seed_from_u64(seed); let floor = bsp::generate(&mut rng); // Basic integration test: ensure we get valid data assert!(!floor.tiles().is_empty()); @@ -32,7 +33,7 @@ mod tests { fn test_bsp_player_start() { let test_seeds = generate_test_seeds(654321); for seed in test_seeds { - let mut rng = SmallRng::seed_from_u64(seed); + let mut rng = DungeonRng::seed_from_u64(seed); let floor = bsp::generate(&mut rng); // Ensure player start is a room tile let start = floor.player_start(); @@ -45,7 +46,7 @@ mod tests { fn test_bsp_2_or_more_rooms() { let test_seeds = generate_test_seeds(111222); for seed in test_seeds { - let mut rng = SmallRng::seed_from_u64(seed); + let mut rng = DungeonRng::seed_from_u64(seed); let floor = bsp::generate(&mut rng); // Ensure we have at least one room tile let room_count = floor @@ -65,7 +66,7 @@ mod tests { fn test_bsp_walls_on_borders() { let test_seeds = generate_test_seeds(777888); for seed in test_seeds { - let mut rng = SmallRng::seed_from_u64(seed); + let mut rng = DungeonRng::seed_from_u64(seed); let floor = bsp::generate(&mut rng); // Go through all tiles, and ensure border tiles are walls for pos in Pos::values() { @@ -85,8 +86,8 @@ mod tests { fn test_bsp_reproducibility() { let test_seeds = generate_test_seeds(111111); for seed in test_seeds { - let mut rng1 = SmallRng::seed_from_u64(seed); - let mut rng2 = SmallRng::seed_from_u64(seed); + let mut rng1 = DungeonRng::seed_from_u64(seed); + let mut rng2 = DungeonRng::seed_from_u64(seed); let floor1 = bsp::generate(&mut rng1); let floor2 = bsp::generate(&mut rng2); assert_eq!( @@ -113,7 +114,7 @@ mod tests { // Helper function to check that all air tiles are reachable from player start fn check_air_tiles_reachable(seed: u64) { - let mut rng = SmallRng::seed_from_u64(seed); + let mut rng = DungeonRng::seed_from_u64(seed); let floor = bsp::generate(&mut rng); // BFS to find all reachable air tiles |