test(builder): 100% builder tests

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

139
Cargo.lock generated
View File

@ -1441,6 +1441,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "claims"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18"
[[package]]
name = "clang-sys"
version = "1.8.1"
@ -2008,6 +2014,21 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -2015,6 +2036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -2023,6 +2045,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@ -2042,6 +2075,53 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "gethostname"
version = "0.4.3"
@ -2298,8 +2378,10 @@ name = "hexlab"
version = "0.3.1"
dependencies = [
"bevy",
"claims",
"hexx",
"rand",
"rstest",
"serde",
"thiserror 2.0.3",
]
@ -3162,6 +3244,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "piper"
version = "0.2.4"
@ -3436,6 +3524,12 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
@ -3471,12 +3565,51 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
[[package]]
name = "rstest"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.42"
@ -3543,6 +3676,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]]
name = "semver"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]]
name = "send_wrapper"
version = "0.6.0"

View File

@ -25,8 +25,9 @@ rand = "0.8"
serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "2.0"
[dev-dependencies]
claims = "0.8"
rstest = "0.23"
[features]
default = []
@ -53,3 +54,6 @@ pedantic = "warn"
nursery = "warn"
unwrap_used = "warn"
expect_used = "warn"
[package.metadata.nextest]
slow-timeout = { period = "120s", terminate-after = 3 }

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>,

139
tests/builder.rs Normal file
View File

@ -0,0 +1,139 @@
use claims::{assert_err, assert_gt, assert_matches, assert_ok, assert_some};
use hexlab::prelude::*;
use rstest::rstest;
#[rstest]
#[case(1, 7)]
#[case(2, 19)]
#[case(3, 37)]
#[case(4, 61)]
#[case(5, 91)]
fn maze_size(#[case] radius: u16, #[case] expected_size: usize) {
let maze = assert_ok!(MazeBuilder::new().with_radius(radius).build());
assert_eq!(maze.len(), expected_size);
}
#[test]
fn builder_without_radius() {
let result = MazeBuilder::new().build();
assert_err!(&result);
assert_matches!(result, Err(MazeBuilderError::NoRadius));
}
#[rstest]
#[case(Hex::ZERO)]
#[case(Hex::new(1,-1))]
#[case(Hex::new(-2,1))]
fn valid_start_position(#[case] start_pos: Hex) {
let maze = assert_ok!(MazeBuilder::new()
.with_radius(3)
.with_start_position(start_pos)
.build());
assert_some!(maze.get_tile(&start_pos));
}
#[test]
fn invalid_start_position() {
let maze = MazeBuilder::new()
.with_radius(3)
.with_start_position(Hex::new(10, 10))
.build();
assert_err!(&maze);
assert_matches!(maze, Err(MazeBuilderError::InvalidStartPosition(_)));
}
#[test]
fn maze_with_seed() {
let maze1 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(12345).build());
let maze2 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(12345).build());
assert_eq!(maze1, maze2, "Mazes with the same seed should be identical");
}
#[test]
fn different_seeds_produce_different_mazes() {
let maze1 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(12345).build());
let maze2 = assert_ok!(MazeBuilder::new().with_radius(3).with_seed(54321).build());
assert_ne!(
maze1, maze2,
"Mazes with different seeds should be different"
);
}
#[test]
fn maze_connectivity() {
let maze = assert_ok!(MazeBuilder::new().with_radius(3).build());
// Helper function to count accessible neighbors
fn count_accessible_neighbors(maze: &HexMaze, pos: Hex) -> usize {
hexx::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);
claims::assert_gt!(
accessible_neighbors,
0,
"Tile at {:?} has no accessible neighbors",
pos
);
}
}
#[test]
fn generator_type() {
let maze = assert_ok!(MazeBuilder::new()
.with_radius(3)
.with_generator(GeneratorType::RecursiveBacktracking)
.build());
claims::assert_gt!(maze.len(), 0);
}
#[test]
fn maze_boundaries() {
let radius = 3;
let maze = MazeBuilder::new()
.with_radius(radius as u16)
.build()
.unwrap();
// Test that tiles exist within the radius
for q in -radius..=radius {
for r in -radius..=radius {
let pos = Hex::new(q, r);
if q.abs() + r.abs() <= radius {
assert!(
maze.get_tile(&pos).is_some(),
"Expected tile at {:?} to exist",
pos
);
}
}
}
}
#[rstest]
#[case(GeneratorType::RecursiveBacktracking)]
fn generate_maze_with_different_types(#[case] generator: GeneratorType) {
// TODO: Add more generator types when they become available
let maze = assert_ok!(MazeBuilder::new()
.with_radius(3)
.with_generator(generator)
.build());
assert_gt!(maze.len(), 0);
}