mirror of
https://github.com/kristoferssolo/hexlab.git
synced 2026-03-22 00:26:26 +00:00
test(builder): 100% builder tests
This commit is contained in:
246
src/builder.rs
246
src/builder.rs
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
36
src/lib.rs
36
src/lib.rs
@@ -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;
|
||||
|
||||
24
src/walls.rs
24
src/walls.rs
@@ -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>,
|
||||
|
||||
Reference in New Issue
Block a user