From 03eaad6a2198f951cbd7bf8238005fe3af96b0c5 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 13 Nov 2024 15:58:55 +0200 Subject: [PATCH 1/3] feat(builder): add maze builder --- src/builder.rs | 214 +++++++++++++++++++++++++++++++++++++++++++++++ src/generator.rs | 102 +--------------------- src/lib.rs | 5 +- src/maze.rs | 2 +- src/walls.rs | 10 +++ 5 files changed, 232 insertions(+), 101 deletions(-) create mode 100644 src/builder.rs diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..9e9ab81 --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,214 @@ +use std::collections::HashSet; + +use hexx::{EdgeDirection, Hex}; +use rand::{seq::SliceRandom, thread_rng, Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +use crate::{generator::GeneratorType, HexMaze}; + +/// A builder pattern for creating hexagonal mazes. +/// +/// This struct provides a fluent interface for configuring and building hexagonal mazes. +/// It offers flexibility in specifying the maze size, random seed, and generation algorithm. +/// +/// # Examples +/// +/// Basic usage: +/// ```rust +/// use hexlab::prelude::*; +/// +/// let maze = MazeBuilder::new() +/// .with_radius(5) +/// .build() +/// .expect("Failed to create maze"); +/// +/// // A radius of 5 creates 61 hexagonal tiles +/// assert!(!maze.is_empty()); +/// assert_eq!(maze.len(), 91); +/// ``` +/// +/// Using a seed for reproducible results: +/// ```rust +/// use hexlab::prelude::*; +/// +/// let maze1 = MazeBuilder::new() +/// .with_radius(3) +/// .with_seed(12345) +/// .build() +/// .expect("Failed to create maze"); +/// +/// let maze2 = MazeBuilder::new() +/// .with_radius(3) +/// .with_seed(12345) +/// .build() +/// .expect("Failed to create maze"); +/// +/// // Same seed should produce identical mazes +/// assert_eq!(maze1.len(), maze2.len()); +/// assert_eq!(maze1, maze2); +/// ``` +/// +/// Specifying a custom generator: +/// ```rust +/// use hexlab::prelude::*; +/// +/// let maze = MazeBuilder::new() +/// .with_radius(7) +/// .with_generator(GeneratorType::RecursiveBacktracking) +/// .build() +/// .expect("Failed to create maze"); +/// ``` +#[derive(Default)] +pub struct MazeBuilder { + radius: Option, + seed: Option, + generator_type: GeneratorType, + start_position: Option, +} + +impl MazeBuilder { + /// Creates a new [`MazeBuilder`] instance. + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Sets the radius for the hexagonal maze. + /// + /// # Arguments + /// + /// * `radius` - The size of the maze (number of tiles along one edge). + #[inline] + pub fn with_radius(mut self, radius: u32) -> Self { + self.radius = Some(radius); + self + } + + /// Sets the random seed for maze generation. + /// + /// # Arguments + /// + /// * `seed` - The random seed value. + #[inline] + pub fn with_seed(mut self, seed: u64) -> Self { + self.seed = Some(seed); + self + } + + /// Sets the generator algorithm for maze creation. + /// + /// Different generators may produce different maze patterns and characteristics. + /// + /// # Arguments + /// + /// * `generator_type` - The maze generation algorithm to use. + #[inline] + pub fn with_generator(mut self, generator_type: GeneratorType) -> Self { + self.generator_type = generator_type; + self + } + + #[inline] + pub fn with_start_position(mut self, pos: Hex) -> Self { + self.start_position = Some(pos); + self + } + + /// Builds the hexagonal maze based on the configured parameters. + /// + /// # Errors + /// + /// Returns an error if no radius is specified. + /// + /// # Examples + /// + /// ```rust + /// use hexlab::prelude::*; + /// + /// // Should fail without radius + /// let result = MazeBuilder::new().build(); + /// assert!(result.is_err()); + /// + /// // Should succeed with radius + /// let result = MazeBuilder::new() + /// .with_radius(3) + /// .build(); + /// assert!(result.is_ok()); + /// + /// let maze = result.unwrap(); + /// assert!(!maze.is_empty()); + /// ``` + pub fn build(self) -> Result { + let radius = self.radius.ok_or("Radius must be specified")?; + let mut maze = self.create_hex_maze(radius); + + if !maze.is_empty() { + self.generate_maze(&mut maze); + } + + Ok(maze) + } + + fn create_hex_maze(&self, radius: u32) -> HexMaze { + let mut maze = HexMaze::new(); + let radius = radius as i32; + for q in -radius..=radius { + let r1 = (-radius).max(-q - radius); + let r2 = radius.min(-q + radius); + for r in r1..=r2 { + let pos = Hex::new(q, r); + maze.add_tile(pos); + } + } + + maze + } + + fn generate_maze(&self, maze: &mut HexMaze) { + match self.seed { + Some(seed) => self.generate_from_seed(maze, seed), + None => self.generate_backtracking(maze), + } + } + + fn generate_from_seed(&self, maze: &mut HexMaze, seed: u64) { + if maze.is_empty() { + return; + } + let start = Hex::ZERO; + let mut visited = HashSet::new(); + let mut rng = ChaCha8Rng::seed_from_u64(seed); + self.recursive_backtrack(maze, start, &mut visited, &mut rng); + } + + fn generate_backtracking(&self, maze: &mut HexMaze) { + if maze.is_empty() { + return; + } + let start = *maze.keys().next().unwrap(); + let mut visited = HashSet::new(); + let mut rng = thread_rng(); + self.recursive_backtrack(maze, start, &mut visited, &mut rng); + } + + fn recursive_backtrack( + &self, + maze: &mut HexMaze, + current: Hex, + visited: &mut HashSet, + rng: &mut R, + ) { + visited.insert(current); + let mut directions = EdgeDirection::ALL_DIRECTIONS; + directions.shuffle(rng); + + for direction in directions { + let neighbor = current + direction; + if maze.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) { + maze.remove_tile_wall(¤t, direction); + maze.remove_tile_wall(&neighbor, direction.const_neg()); + self.recursive_backtrack(maze, neighbor, visited, rng); + } + } + } +} diff --git a/src/generator.rs b/src/generator.rs index 0df0e12..efab22c 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,101 +1,5 @@ -use std::collections::HashSet; - -use hexx::{EdgeDirection, Hex}; -use rand::{seq::SliceRandom, thread_rng, Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; - -use crate::HexMaze; - -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub enum GeneratorType { - BackTracking, -} - -impl HexMaze { - pub fn generate(&mut self, generator_type: GeneratorType) { - match generator_type { - GeneratorType::BackTracking => self.generate_backtracking(), - } - } - - pub fn generate_from_seed(&mut self, generator_type: GeneratorType, seed: u64) { - match generator_type { - GeneratorType::BackTracking => self.generate_backtracking_from_seed(seed), - } - } - - pub fn generate_backtracking(&mut self) { - if self.is_empty() { - return; - } - let start = *self.keys().next().unwrap(); - - let mut visited = HashSet::new(); - let mut rng = thread_rng(); - self.recursive_backtrack(start, &mut visited, &mut rng); - } - - pub fn generate_backtracking_from_seed(&mut self, seed: u64) { - if self.is_empty() { - return; - } - // let start = *self.keys().next().unwrap(); - let start = Hex::ZERO; - - let mut visited = HashSet::new(); - let mut rng = ChaCha8Rng::seed_from_u64(seed); - self.recursive_backtrack(start, &mut visited, &mut rng); - } - - fn recursive_backtrack( - &mut self, - current: Hex, - visited: &mut HashSet, - rng: &mut R, - ) { - visited.insert(current); - - let mut directions = EdgeDirection::ALL_DIRECTIONS; - directions.shuffle(rng); - - for direction in directions { - let neighbor = current + direction; - - if self.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) { - self.remove_tile_wall(¤t, direction); - self.remove_tile_wall(&neighbor, direction.const_neg()); - - self.recursive_backtrack(neighbor, visited, rng); - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn backtracking_generation() { - let mut maze = HexMaze::with_radius(2); - - // Before generation - for tile in maze.values() { - assert_eq!(tile.walls.as_bits(), 0b111111); - } - - // Generate using backtracking - maze.generate(GeneratorType::BackTracking); - - // After generation - let all_walls = maze.values().all(|tile| tile.walls.as_bits() == 0b111111); - assert!(!all_walls, "Some walls should be removed"); - } - - #[test] - fn empty_maze() { - let mut maze = HexMaze::default(); - maze.generate(GeneratorType::BackTracking); - assert!(maze.is_empty(), "Empty maze should remain empty"); - } + #[default] + RecursiveBacktracking, } diff --git a/src/lib.rs b/src/lib.rs index 267a1f5..43d23b3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,13 +1,16 @@ +mod builder; mod generator; mod maze; mod tile; mod walls; +pub use builder::MazeBuilder; +pub use generator::GeneratorType; pub use maze::HexMaze; pub use tile::HexTile; pub use walls::Walls; pub mod prelude { - pub use super::{HexMaze, HexTile, Walls}; + pub use super::{GeneratorType, HexMaze, HexTile, MazeBuilder, Walls}; pub use hexx::{EdgeDirection, Hex, HexLayout}; } diff --git a/src/maze.rs b/src/maze.rs index 45b98b4..65e293b 100644 --- a/src/maze.rs +++ b/src/maze.rs @@ -9,7 +9,7 @@ use super::{HexTile, Walls}; /// Represents a hexagonal maze with tiles and walls #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct HexMaze(HashMap); impl HexMaze { diff --git a/src/walls.rs b/src/walls.rs index 01a8fa7..1e00b5a 100644 --- a/src/walls.rs +++ b/src/walls.rs @@ -49,6 +49,16 @@ impl From for Walls { } } +impl From<[EdgeDirection; 6]> for Walls { + fn from(value: [EdgeDirection; 6]) -> Self { + let mut walls = 0u8; + for direction in value { + walls |= 1 << direction.index(); + } + Self(walls) + } +} + impl From for Walls { fn from(value: u8) -> Self { Self(1 << value) From 3700e0991e70230fb356763a39993577611bd518 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 13 Nov 2024 16:29:42 +0200 Subject: [PATCH 2/3] refactor(builder): make generator functions --- Cargo.lock | 87 +++++++++------ Cargo.toml | 3 +- src/builder.rs | 282 ++++++++++++++++++++++++++++++++++++++--------- src/generator.rs | 44 ++++++++ src/lib.rs | 4 +- 5 files changed, 333 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4030cea..deda24e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,7 +148,7 @@ dependencies = [ "ndk-context", "ndk-sys 0.6.0+11769913", "num_enum", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -341,7 +341,7 @@ dependencies = [ "petgraph", "ron", "serde", - "thiserror", + "thiserror 1.0.68", "thread_local", "uuid", ] @@ -359,7 +359,7 @@ dependencies = [ "bevy_utils", "console_error_panic_hook", "downcast-rs", - "thiserror", + "thiserror 1.0.68", "wasm-bindgen", "web-sys", ] @@ -389,7 +389,7 @@ dependencies = [ "parking_lot", "ron", "serde", - "thiserror", + "thiserror 1.0.68", "uuid", "wasm-bindgen", "wasm-bindgen-futures", @@ -438,7 +438,7 @@ dependencies = [ "bytemuck", "encase", "serde", - "thiserror", + "thiserror 1.0.68", "wgpu-types", ] @@ -478,7 +478,7 @@ dependencies = [ "radsort", "serde", "smallvec", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -526,7 +526,7 @@ dependencies = [ "nonmax", "petgraph", "serde", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -563,7 +563,7 @@ dependencies = [ "bevy_time", "bevy_utils", "gilrs", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -629,7 +629,7 @@ dependencies = [ "serde", "serde_json", "smallvec", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -658,7 +658,7 @@ dependencies = [ "bevy_reflect", "bevy_utils", "smol_str", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -740,7 +740,7 @@ dependencies = [ "rand", "serde", "smallvec", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -801,7 +801,7 @@ dependencies = [ "serde", "smallvec", "smol_str", - "thiserror", + "thiserror 1.0.68", "uuid", ] @@ -860,7 +860,7 @@ dependencies = [ "send_wrapper", "serde", "smallvec", - "thiserror", + "thiserror 1.0.68", "wasm-bindgen", "web-sys", "wgpu", @@ -894,7 +894,7 @@ dependencies = [ "bevy_transform", "bevy_utils", "serde", - "thiserror", + "thiserror 1.0.68", "uuid", ] @@ -921,7 +921,7 @@ dependencies = [ "guillotiere", "radsort", "rectangle-pack", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -983,7 +983,7 @@ dependencies = [ "bevy_window", "glyph_brush_layout", "serde", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -997,7 +997,7 @@ dependencies = [ "bevy_reflect", "bevy_utils", "crossbeam-channel", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -1011,7 +1011,7 @@ dependencies = [ "bevy_hierarchy", "bevy_math", "bevy_reflect", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -1041,7 +1041,7 @@ dependencies = [ "nonmax", "smallvec", "taffy", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -1258,7 +1258,7 @@ dependencies = [ "polling", "rustix", "slab", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -1606,7 +1606,7 @@ dependencies = [ "const_panic", "encase_derive", "glam", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -1966,7 +1966,7 @@ checksum = "6f56f6318968d03c18e1bcf4857ff88c61157e9da8e47c5f29055d60e1228884" dependencies = [ "log", "presser", - "thiserror", + "thiserror 1.0.68", "winapi", "windows 0.52.0", ] @@ -2034,7 +2034,7 @@ dependencies = [ "com", "libc", "libloading 0.8.5", - "thiserror", + "thiserror 1.0.68", "widestring", "winapi", ] @@ -2063,13 +2063,14 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hexlab" -version = "0.1.0" +version = "0.1.1" dependencies = [ "bevy", "hexx", "rand", "rand_chacha", "serde", + "thiserror 2.0.3", ] [[package]] @@ -2176,7 +2177,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.68", "walkdir", "windows-sys 0.45.0", ] @@ -2405,7 +2406,7 @@ dependencies = [ "rustc-hash", "spirv", "termcolor", - "thiserror", + "thiserror 1.0.68", "unicode-xid", ] @@ -2424,7 +2425,7 @@ dependencies = [ "regex", "regex-syntax 0.8.5", "rustc-hash", - "thiserror", + "thiserror 1.0.68", "tracing", "unicode-ident", ] @@ -2440,7 +2441,7 @@ dependencies = [ "log", "ndk-sys 0.5.0+25.2.9519653", "num_enum", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -2455,7 +2456,7 @@ dependencies = [ "ndk-sys 0.6.0+11769913", "num_enum", "raw-window-handle", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -3156,7 +3157,7 @@ checksum = "d1fceb9d127d515af1586d8d0cc601e1245bdb0af38e75c865a156290184f5b3" dependencies = [ "cpal", "lewton", - "thiserror", + "thiserror 1.0.68", ] [[package]] @@ -3397,7 +3398,16 @@ version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02dd99dc800bbb97186339685293e1cc5d9df1f8fae2d0aecd9ff1c77efea892" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.68", +] + +[[package]] +name = "thiserror" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +dependencies = [ + "thiserror-impl 2.0.3", ] [[package]] @@ -3411,6 +3421,17 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "thiserror-impl" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "thread_local" version = "1.1.8" @@ -3749,7 +3770,7 @@ dependencies = [ "raw-window-handle", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.68", "web-sys", "wgpu-hal", "wgpu-types", @@ -3793,7 +3814,7 @@ dependencies = [ "renderdoc-sys", "rustc-hash", "smallvec", - "thiserror", + "thiserror 1.0.68", "wasm-bindgen", "web-sys", "wgpu-types", diff --git a/Cargo.toml b/Cargo.toml index 44c1b25..de1c3ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hexlab" authors = ["Kristofers Solo "] -version = "0.1.0" +version = "0.1.1" edition = "2021" description = "A hexagonal maze library" repository = "https://github.com/kristoferssolo/hexlab" @@ -14,6 +14,7 @@ hexx = { version = "0.18" } rand = "0.8" rand_chacha = "0.3" serde = { version = "1.0", features = ["derive"], optional = true } +thiserror = "2.0" [features] default = [] diff --git a/src/builder.rs b/src/builder.rs index 9e9ab81..b61b600 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,10 +1,29 @@ -use std::collections::HashSet; +use hexx::Hex; +use thiserror::Error; -use hexx::{EdgeDirection, Hex}; -use rand::{seq::SliceRandom, thread_rng, Rng, SeedableRng}; -use rand_chacha::ChaCha8Rng; +use crate::{ + generator::{generate_backtracking, GeneratorType}, + HexMaze, +}; -use crate::{generator::GeneratorType, HexMaze}; +#[derive(Debug, Error)] +pub enum MazeBuilderError { + /// Occurs when attempting to build a maze without specifying a radius. + #[error("Radius must be specified to build a maze")] + NoRadius, + + /// Occurs when the specified radius is too large. + #[error("Radius {0} is too large. Maximum allowed radius is {1}")] + RadiusTooLarge(u32, u32), + + /// Occurs when the specified start position is outside the maze bounds. + #[error("Start position {0:?} is outside maze bounds")] + InvalidStartPosition(Hex), + + /// Occurs when maze generation fails. + #[error("Failed to generate maze: {0}")] + GenerationError(String), +} /// A builder pattern for creating hexagonal mazes. /// @@ -118,7 +137,8 @@ impl MazeBuilder { /// /// # Errors /// - /// Returns an error if no radius is specified. + /// Returns [`MazeBuilderError::NoRadius`] if no radius is specified. + /// Returns [`MazeBuilderError::InvalidStartPosition`] if the start position is outside maze bounds. /// /// # Examples /// @@ -138,10 +158,16 @@ impl MazeBuilder { /// let maze = result.unwrap(); /// assert!(!maze.is_empty()); /// ``` - pub fn build(self) -> Result { - let radius = self.radius.ok_or("Radius must be specified")?; + pub fn build(self) -> Result { + let radius = self.radius.ok_or(MazeBuilderError::NoRadius)?; let mut maze = self.create_hex_maze(radius); + if let Some(start_pos) = self.start_position { + if maze.get_tile(&start_pos).is_none() { + return Err(MazeBuilderError::InvalidStartPosition(start_pos)); + } + } + if !maze.is_empty() { self.generate_maze(&mut maze); } @@ -165,49 +191,203 @@ impl MazeBuilder { } fn generate_maze(&self, maze: &mut HexMaze) { - match self.seed { - Some(seed) => self.generate_from_seed(maze, seed), - None => self.generate_backtracking(maze), - } - } - - fn generate_from_seed(&self, maze: &mut HexMaze, seed: u64) { - if maze.is_empty() { - return; - } - let start = Hex::ZERO; - let mut visited = HashSet::new(); - let mut rng = ChaCha8Rng::seed_from_u64(seed); - self.recursive_backtrack(maze, start, &mut visited, &mut rng); - } - - fn generate_backtracking(&self, maze: &mut HexMaze) { - if maze.is_empty() { - return; - } - let start = *maze.keys().next().unwrap(); - let mut visited = HashSet::new(); - let mut rng = thread_rng(); - self.recursive_backtrack(maze, start, &mut visited, &mut rng); - } - - fn recursive_backtrack( - &self, - maze: &mut HexMaze, - current: Hex, - visited: &mut HashSet, - rng: &mut R, - ) { - visited.insert(current); - let mut directions = EdgeDirection::ALL_DIRECTIONS; - directions.shuffle(rng); - - for direction in directions { - let neighbor = current + direction; - if maze.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) { - maze.remove_tile_wall(¤t, direction); - maze.remove_tile_wall(&neighbor, direction.const_neg()); - self.recursive_backtrack(maze, neighbor, visited, rng); + match self.generator_type { + GeneratorType::RecursiveBacktracking => { + generate_backtracking(maze, self.start_position, self.seed) + } + } + } +} + +#[cfg(test)] +mod test { + use hexx::EdgeDirection; + + use super::*; + + /// Helper function to count the number of tiles for a given radius + fn calculate_hex_tiles(radius: u32) -> usize { + let r = radius as i32; + (3 * r * r + 3 * r + 1) as usize + } + + #[test] + fn new_builder() { + let builder = MazeBuilder::new(); + assert!(builder.radius.is_none()); + assert!(builder.seed.is_none()); + assert!(builder.start_position.is_none()); + } + + #[test] + fn builder_with_radius() { + let radius = 5; + let maze = MazeBuilder::new().with_radius(radius).build().unwrap(); + + assert_eq!(maze.len(), calculate_hex_tiles(radius)); + assert!(maze.get_tile(&Hex::ZERO).is_some()); + } + + #[test] + fn builder_without_radius() { + let maze = MazeBuilder::new().build(); + assert!(matches!(maze, Err(MazeBuilderError::NoRadius))); + } + + #[test] + fn builder_with_seed() { + let radius = 3; + let seed = 12345; + + let maze1 = MazeBuilder::new() + .with_radius(radius) + .with_seed(seed) + .build() + .unwrap(); + + let maze2 = MazeBuilder::new() + .with_radius(radius) + .with_seed(seed) + .build() + .unwrap(); + + // Same seed should produce identical mazes + assert_eq!(maze1, maze2); + } + + #[test] + fn different_seeds_produce_different_mazes() { + let radius = 3; + + let maze1 = MazeBuilder::new() + .with_radius(radius) + .with_seed(12345) + .build() + .unwrap(); + + let maze2 = MazeBuilder::new() + .with_radius(radius) + .with_seed(54321) + .build() + .unwrap(); + + // Different seeds should produce different mazes + assert_ne!(maze1, maze2); + } + + #[test] + fn maze_connectivity() { + let radius = 3; + let maze = MazeBuilder::new().with_radius(radius).build().unwrap(); + + // Helper function to count accessible neighbors + fn count_accessible_neighbors(maze: &HexMaze, pos: Hex) -> usize { + EdgeDirection::ALL_DIRECTIONS + .iter() + .filter(|&&dir| { + let neighbor = pos + dir; + if let Some(walls) = maze.get_walls(&pos) { + !walls.contains(dir) && maze.get_tile(&neighbor).is_some() + } else { + false + } + }) + .count() + } + + // Check that each tile has at least one connection + for &pos in maze.keys() { + let accessible_neighbors = count_accessible_neighbors(&maze, pos); + assert!( + accessible_neighbors > 0, + "Tile at {:?} has no accessible neighbors", + pos + ); + } + } + + #[test] + fn start_position() { + let radius = 3; + let start_pos = Hex::new(1, 1); + + let maze = MazeBuilder::new() + .with_radius(radius) + .with_start_position(start_pos) + .build() + .unwrap(); + + assert!(maze.get_tile(&start_pos).is_some()); + } + + #[test] + fn invalid_start_position() { + let maze = MazeBuilder::new() + .with_radius(3) + .with_start_position(Hex::new(10, 10)) + .build(); + + assert!(matches!( + maze, + Err(MazeBuilderError::InvalidStartPosition(_)) + )); + } + + #[test] + fn maze_boundaries() { + let radius = 3; + let maze = MazeBuilder::new().with_radius(radius).build().unwrap(); + + // Test that tiles exist within the radius + for q in -(radius as i32)..=(radius as i32) { + for r in -(radius as i32)..=(radius as i32) { + let pos = Hex::new(q, r); + if q.abs() + r.abs() <= radius as i32 { + assert!( + maze.get_tile(&pos).is_some(), + "Expected tile at {:?} to exist", + pos + ); + } + } + } + } + + #[test] + fn different_radii() { + for radius in 1..=5 { + let maze = MazeBuilder::new().with_radius(radius).build().unwrap(); + + assert_eq!( + maze.len(), + calculate_hex_tiles(radius), + "Incorrect number of tiles for radius {}", + radius + ); + } + } + + #[test] + fn wall_consistency() { + let radius = 3; + let maze = MazeBuilder::new().with_radius(radius).build().unwrap(); + + // Check that if tile A has no wall to tile B, + // then tile B has no wall to tile A + for &pos in maze.keys() { + for &dir in &EdgeDirection::ALL_DIRECTIONS { + let neighbor = pos + dir; + if let (Some(walls), Some(neighbor_walls)) = + (maze.get_walls(&pos), maze.get_walls(&neighbor)) + { + assert_eq!( + walls.contains(dir), + neighbor_walls.contains(dir.const_neg()), + "Wall inconsistency between {:?} and {:?}", + pos, + neighbor + ); + } } } } diff --git a/src/generator.rs b/src/generator.rs index efab22c..6b7b1a8 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -1,5 +1,49 @@ +use std::collections::HashSet; + +use hexx::{EdgeDirection, Hex}; +use rand::{seq::SliceRandom, thread_rng, Rng, RngCore, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +use crate::HexMaze; + #[derive(Debug, Clone, Copy, Default)] pub enum GeneratorType { #[default] RecursiveBacktracking, } + +pub(crate) fn generate_backtracking(maze: &mut HexMaze, start_pos: Option, seed: Option) { + if maze.is_empty() { + return; + } + + let start = start_pos.unwrap_or(Hex::ZERO); + + let mut visited = HashSet::new(); + + let mut rng: Box = match seed { + Some(seed) => Box::new(ChaCha8Rng::seed_from_u64(seed)), + None => Box::new(thread_rng()), + }; + recursive_backtrack(maze, start, &mut visited, &mut rng); +} + +fn recursive_backtrack( + maze: &mut HexMaze, + current: Hex, + visited: &mut HashSet, + rng: &mut R, +) { + visited.insert(current); + let mut directions = EdgeDirection::ALL_DIRECTIONS; + directions.shuffle(rng); + + for direction in directions { + let neighbor = current + direction; + if maze.get_tile(&neighbor).is_some() && !visited.contains(&neighbor) { + maze.remove_tile_wall(¤t, direction); + maze.remove_tile_wall(&neighbor, direction.const_neg()); + recursive_backtrack(maze, neighbor, visited, rng); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 43d23b3..0e8e955 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,13 +4,13 @@ mod maze; mod tile; mod walls; -pub use builder::MazeBuilder; +pub use builder::{MazeBuilder, MazeBuilderError}; pub use generator::GeneratorType; pub use maze::HexMaze; pub use tile::HexTile; pub use walls::Walls; pub mod prelude { - pub use super::{GeneratorType, HexMaze, HexTile, MazeBuilder, Walls}; + pub use super::{GeneratorType, HexMaze, HexTile, MazeBuilder, MazeBuilderError, Walls}; pub use hexx::{EdgeDirection, Hex, HexLayout}; } From 90570d0eca2334564e47cf4777333935e2d52ae6 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 13 Nov 2024 16:36:05 +0200 Subject: [PATCH 3/3] chore: update cargo.toml --- Cargo.toml | 30 +++++++++++++++++++++++++++--- src/builder.rs | 5 ++--- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index de1c3ea..16f6725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,19 @@ name = "hexlab" authors = ["Kristofers Solo "] version = "0.1.1" edition = "2021" -description = "A hexagonal maze library" +description = "A hexagonal maze generation and manipulation library" repository = "https://github.com/kristoferssolo/hexlab" +documentation = "https://docs.rs/hexlab" +homepage = "https://github.com/kristoferssolo/hexlab" license = "MIT OR Apache-2.0" -keywords = ["maze", "hex", "hexagons"] +keywords = ["maze", "hex", "hexagons", "generation", "game"] +categories = [ + "algorithms", + "game-development", + "mathematics", + "data-structures", +] +exclude = ["/.github", "/.gitignore", "/tests", "*.png", "*.md"] [dependencies] bevy = { version = "0.14", optional = true } @@ -16,9 +25,24 @@ rand_chacha = "0.3" serde = { version = "1.0", features = ["derive"], optional = true } thiserror = "2.0" + +[dev-dependencies] + [features] default = [] serde = ["dep:serde", "hexx/serde", "rand_chacha/serde"] bevy = ["dep:bevy", "hexx/bevy_reflect"] +full = ["serde", "bevy"] -[dev-dependencies] +[profile.dev] +opt-level = 1 # Better compile times with some optimization + +[profile.release] +opt-level = 3 +lto = "thin" +strip = true # Smaller binary size +panic = "abort" # Smaller binary size + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/src/builder.rs b/src/builder.rs index b61b600..11917ae 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,10 +1,9 @@ -use hexx::Hex; -use thiserror::Error; - use crate::{ generator::{generate_backtracking, GeneratorType}, HexMaze, }; +use hexx::Hex; +use thiserror::Error; #[derive(Debug, Error)] pub enum MazeBuilderError {