feat(builder): add maze builder

This commit is contained in:
Kristofers Solo 2024-11-13 15:58:55 +02:00
parent b5922f4b83
commit 03eaad6a21
5 changed files with 232 additions and 101 deletions

214
src/builder.rs Normal file
View File

@ -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<u32>,
seed: Option<u64>,
generator_type: GeneratorType,
start_position: Option<Hex>,
}
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<HexMaze, String> {
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<R: Rng>(
&self,
maze: &mut HexMaze,
current: Hex,
visited: &mut HashSet<Hex>,
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(&current, direction);
maze.remove_tile_wall(&neighbor, direction.const_neg());
self.recursive_backtrack(maze, neighbor, visited, rng);
}
}
}
}

View File

@ -1,101 +1,5 @@
use std::collections::HashSet; #[derive(Debug, Clone, Copy, Default)]
use hexx::{EdgeDirection, Hex};
use rand::{seq::SliceRandom, thread_rng, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::HexMaze;
#[derive(Debug, Clone, Copy)]
pub enum GeneratorType { pub enum GeneratorType {
BackTracking, #[default]
} RecursiveBacktracking,
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<R: Rng>(
&mut self,
current: Hex,
visited: &mut HashSet<Hex>,
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(&current, 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");
}
} }

View File

@ -1,13 +1,16 @@
mod builder;
mod generator; mod generator;
mod maze; mod maze;
mod tile; mod tile;
mod walls; mod walls;
pub use builder::MazeBuilder;
pub use generator::GeneratorType;
pub use maze::HexMaze; pub use maze::HexMaze;
pub use tile::HexTile; pub use tile::HexTile;
pub use walls::Walls; pub use walls::Walls;
pub mod prelude { pub mod prelude {
pub use super::{HexMaze, HexTile, Walls}; pub use super::{GeneratorType, HexMaze, HexTile, MazeBuilder, Walls};
pub use hexx::{EdgeDirection, Hex, HexLayout}; pub use hexx::{EdgeDirection, Hex, HexLayout};
} }

View File

@ -9,7 +9,7 @@ use super::{HexTile, Walls};
/// Represents a hexagonal maze with tiles and walls /// Represents a hexagonal maze with tiles and walls
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct HexMaze(HashMap<Hex, HexTile>); pub struct HexMaze(HashMap<Hex, HexTile>);
impl HexMaze { impl HexMaze {

View File

@ -49,6 +49,16 @@ impl From<EdgeDirection> 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<u8> for Walls { impl From<u8> for Walls {
fn from(value: u8) -> Self { fn from(value: u8) -> Self {
Self(1 << value) Self(1 << value)