mirror of
https://github.com/kristoferssolo/hexlab.git
synced 2025-10-21 19:40:34 +00:00
feat(builder): add maze builder
This commit is contained in:
parent
b5922f4b83
commit
03eaad6a21
214
src/builder.rs
Normal file
214
src/builder.rs
Normal 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(¤t, direction);
|
||||||
|
maze.remove_tile_wall(&neighbor, direction.const_neg());
|
||||||
|
self.recursive_backtrack(maze, neighbor, visited, rng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
102
src/generator.rs
102
src/generator.rs
@ -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(¤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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
10
src/walls.rs
10
src/walls.rs
@ -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)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user