From 03eaad6a2198f951cbd7bf8238005fe3af96b0c5 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 13 Nov 2024 15:58:55 +0200 Subject: [PATCH] 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)