test(builder): 100% builder tests

This commit is contained in:
2024-12-25 20:18:12 +02:00
parent 389c8ee1fd
commit 7cacf92014
9 changed files with 390 additions and 218 deletions

View File

@@ -80,7 +80,7 @@ pub enum MazeBuilderError {
#[allow(clippy::module_name_repetitions)]
#[derive(Default)]
pub struct MazeBuilder {
radius: Option<u32>,
radius: Option<u16>,
seed: Option<u64>,
generator_type: GeneratorType,
start_position: Option<Hex>,
@@ -88,7 +88,7 @@ pub struct MazeBuilder {
impl MazeBuilder {
/// Creates a new [`MazeBuilder`] instance with default settings.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn new() -> Self {
Self::default()
@@ -104,9 +104,9 @@ impl MazeBuilder {
/// # Arguments
///
/// - `radius` - The number of tiles from the center to the edge of the hexagon.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn with_radius(mut self, radius: u32) -> Self {
pub const fn with_radius(mut self, radius: u16) -> Self {
self.radius = Some(radius);
self
}
@@ -118,7 +118,7 @@ impl MazeBuilder {
/// # Arguments
///
/// - `seed` - The random seed value.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
@@ -132,7 +132,7 @@ impl MazeBuilder {
/// # Arguments
///
/// - `generator_type` - The maze generation algorithm to use.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn with_generator(mut self, generator_type: GeneratorType) -> Self {
self.generator_type = generator_type;
@@ -143,7 +143,7 @@ impl MazeBuilder {
/// # Arguments
///
/// - `pos` - The hexagonal coordinates for the starting position.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn with_start_position(mut self, pos: Hex) -> Self {
self.start_position = Some(pos);
@@ -200,9 +200,9 @@ impl MazeBuilder {
}
}
}
fn create_hex_maze(radius: u32) -> HexMaze {
fn create_hex_maze(radius: u16) -> HexMaze {
let mut maze = HexMaze::new();
let radius = i32::try_from(radius).unwrap_or(5);
let radius = i32::from(radius);
for q in -radius..=radius {
let r1 = (-radius).max(-q - radius);
@@ -218,194 +218,68 @@ fn create_hex_maze(radius: u32) -> HexMaze {
#[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
}
use claims::assert_gt;
use rstest::rstest;
#[test]
fn new_builder() {
fn maze_builder_new() {
let builder = MazeBuilder::new();
assert!(builder.radius.is_none());
assert!(builder.seed.is_none());
assert!(builder.start_position.is_none());
assert_eq!(builder.radius, None);
assert_eq!(builder.seed, None);
assert_eq!(builder.generator_type, GeneratorType::default());
assert_eq!(builder.start_position, None);
}
#[rstest]
#[case(0, 1)] // Minimum size is 1 tile
#[case(1, 7)]
#[case(2, 19)]
#[case(3, 37)]
#[case(10, 331)]
#[case(100, 30301)]
fn create_hex_maze_various_radii(#[case] radius: u16, #[case] expected_size: usize) {
let maze = create_hex_maze(radius);
assert_eq!(maze.len(), expected_size);
}
#[test]
fn builder_with_radius() {
let radius = 5;
let maze = MazeBuilder::new().with_radius(radius).build().unwrap();
fn create_hex_maze_large_radius() {
let large_radius = 1000;
let maze = create_hex_maze(large_radius);
assert_gt!(maze.len(), 0);
assert_eq!(maze.len(), calculate_hex_tiles(radius));
assert!(maze.get_tile(&Hex::ZERO).is_some());
// Calculate expected size for this radius
let expected_size = 3 * (large_radius as usize).pow(2) + 3 * large_radius as usize + 1;
assert_eq!(maze.len(), expected_size);
}
#[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
);
}
}
fn create_hex_maze_tile_positions() {
let maze = create_hex_maze(2);
let expected_positions = [
Hex::new(0, 0),
Hex::new(1, -1),
Hex::new(1, 0),
Hex::new(0, 1),
Hex::new(-1, 1),
Hex::new(-1, 0),
Hex::new(0, -1),
Hex::new(2, -2),
Hex::new(2, -1),
Hex::new(2, 0),
Hex::new(1, 1),
Hex::new(0, 2),
Hex::new(-1, 2),
Hex::new(-2, 2),
Hex::new(-2, 1),
Hex::new(-2, 0),
Hex::new(-1, -1),
Hex::new(0, -2),
Hex::new(1, -2),
];
for pos in expected_positions.iter() {
assert!(maze.get_tile(pos).is_some(), "Expected tile at {:?}", pos);
}
}
}

View File

@@ -11,7 +11,7 @@ use crate::HexMaze;
#[cfg_attr(feature = "bevy_reflect", derive(Reflect))]
#[cfg_attr(feature = "bevy", derive(Component))]
#[cfg_attr(feature = "bevy", reflect(Component))]
#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum GeneratorType {
#[default]
RecursiveBacktracking,

View File

@@ -21,7 +21,7 @@ pub struct HexMaze(HashMap<Hex, HexTile>);
impl HexMaze {
/// Creates a new empty maze
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn new() -> Self {
Self::default()
@@ -54,7 +54,7 @@ impl HexMaze {
/// # Arguments
///
/// - `coord` - The hexagonal coordinates of the tile to retrieve.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn get_tile(&self, coord: &Hex) -> Option<&HexTile> {
self.0.get(coord)
@@ -70,14 +70,14 @@ impl HexMaze {
}
/// Returns the number of tiles in the maze.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
/// Returns `true` if the maze contains no tiles.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()

View File

@@ -34,14 +34,14 @@ impl HexTile {
}
/// Returns a reference to the tile's walls
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn walls(&self) -> &Walls {
&self.walls
}
/// Returns position of the tile
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn pos(&self) -> Hex {
self.pos
@@ -52,7 +52,7 @@ impl HexTile {
///
/// - `layout` - The hexagonal layout used for conversion.
#[cfg(feature = "bevy_reflect")]
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn to_vec2(&self, layout: &HexLayout) -> Vec2 {
layout.hex_to_world_pos(self.pos)
@@ -64,7 +64,7 @@ impl HexTile {
///
/// - `layout` - The hexagonal layout used for conversion.
#[cfg(feature = "bevy_reflect")]
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn to_vec3(&self, layout: &HexLayout) -> Vec3 {
let pos = self.to_vec2(layout);

View File

@@ -16,23 +16,39 @@
//!```
//! use hexlab::prelude::*;
//!
//! // Create a new maze
//! let maze = MazeBuilder::new()
//! .with_radius(5)
//! .with_radius(3)
//! .build()
//! .expect("Failed to create maze");
//!
//! // Get a specific tile
//! let tile = maze.get_tile(&Hex::new(1, -1)).unwrap();
//!
//! // Check if a wall exists
//! let has_wall = tile.walls().contains(EdgeDirection::FLAT_NORTH);
//! assert_eq!(maze.len(), 37); // A radius of 3 should create 37 tiles
//!```
//!
//! # Acknowledgements
//! Customizing maze generation:
//!
//! Hexlab relies on the excellent [hexx](https://github.com/ManevilleF/hexx) library for handling
//! hexagonal grid mathematics, coordinates, and related operations.
//!```
//! use hexlab::prelude::*;
//!
//! let maze = MazeBuilder::new()
//! .with_radius(2)
//! .with_seed(12345)
//! .with_start_position(Hex::new(1, -1))
//! .build()
//! .expect("Failed to create maze");
//!
//! assert!(maze.get_tile(&Hex::new(1, -1)).is_some());
//!```
//!
//! Manipulating walls:
//!
//!```
//! use hexlab::prelude::*;
//!
//! let mut walls = Walls::empty();
//! walls.add(EdgeDirection::FLAT_NORTH);
//! assert!(walls.contains(EdgeDirection::FLAT_NORTH));
//! assert!(!walls.contains(EdgeDirection::FLAT_SOUTH));
//!```
mod builder;
mod generator;
mod hex_maze;

View File

@@ -38,21 +38,21 @@ impl Walls {
/// Creates a new set of walls with all edges closed.
///
/// This is the default state where all six edges of the hexagon have walls.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Creates a new set of walls with no edges (completely open).
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn empty() -> Self {
Self(0)
}
/// Checks if the walls are currently empty (no walls present).
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn is_empty(&self) -> bool {
self.0 == 0
@@ -63,7 +63,7 @@ impl Walls {
/// # Arguments
///
/// 0 `direction` - The direction in which to add the wall.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
pub fn add<T>(&mut self, direction: T)
where
T: Into<Self> + Copy,
@@ -76,7 +76,7 @@ impl Walls {
/// # Arguments
///
/// - `direction` - The direction from which to remove the wall.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
pub fn remove<T>(&mut self, direction: T) -> bool
where
T: Into<Self> + Copy,
@@ -93,7 +93,7 @@ impl Walls {
/// # Arguments
///
/// - `other` - The direction to check for a wall.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
pub fn contains<T>(&self, other: T) -> bool
where
T: Into<Self> + Copy,
@@ -102,21 +102,21 @@ impl Walls {
}
/// Returns the raw bit representation of the walls
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn as_bits(&self) -> u8 {
self.0
}
/// Returns the total number of walls present
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn count(&self) -> u8 {
u8::try_from(self.0.count_ones()).unwrap_or_default()
}
/// Returns a `Walls` value representing all possible directions.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub const fn all_directions() -> Self {
Self(0b11_1111)
@@ -156,7 +156,7 @@ impl Walls {
/// # Deprecated
///
/// This method is deprecated since version 0.3.1. Use `is_enclosed()` instead.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
#[deprecated(since = "0.3.1", note = "use `walls::Walls::is_enclosed()`")]
pub fn is_closed(&self) -> bool {
@@ -168,7 +168,7 @@ impl Walls {
/// # Returns
///
/// `true` if the hexagon has all possible walls, making it completely enclosed.
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
#[must_use]
pub fn is_enclosed(&self) -> bool {
self.count() == 6
@@ -196,7 +196,7 @@ impl Walls {
/// assert!(walls.contains(EdgeDirection::FLAT_SOUTH));
/// assert_eq!(walls.count(), 3);
/// ```
#[inline]
#[cfg_attr(not(debug_assertions), inline)]
pub fn fill<T>(&mut self, other: T)
where
T: Into<Self>,