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" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "claims"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bba18ee93d577a8428902687bcc2b6b45a56b1981a1f6d779731c86cc4c5db18"
[[package]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.8.1" version = "1.8.1"
@ -2008,6 +2014,21 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" 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]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.31" version = "0.3.31"
@ -2015,6 +2036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink",
] ]
[[package]] [[package]]
@ -2023,6 +2045,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 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]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.31" version = "0.3.31"
@ -2042,6 +2075,53 @@ dependencies = [
"pin-project-lite", "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]] [[package]]
name = "gethostname" name = "gethostname"
version = "0.4.3" version = "0.4.3"
@ -2298,8 +2378,10 @@ name = "hexlab"
version = "0.3.1" version = "0.3.1"
dependencies = [ dependencies = [
"bevy", "bevy",
"claims",
"hexx", "hexx",
"rand", "rand",
"rstest",
"serde", "serde",
"thiserror 2.0.3", "thiserror 2.0.3",
] ]
@ -3162,6 +3244,12 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "piper" name = "piper"
version = "0.2.4" version = "0.2.4"
@ -3436,6 +3524,12 @@ version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]] [[package]]
name = "renderdoc-sys" name = "renderdoc-sys"
version = "1.1.0" version = "1.1.0"
@ -3471,12 +3565,51 @@ version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" 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]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" 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]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.42" version = "0.38.42"
@ -3543,6 +3676,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe" checksum = "c2fdfc24bc566f839a2da4c4295b82db7d25a24253867d5c64355abb5799bdbe"
[[package]]
name = "semver"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba"
[[package]] [[package]]
name = "send_wrapper" name = "send_wrapper"
version = "0.6.0" version = "0.6.0"

View File

@ -25,8 +25,9 @@ rand = "0.8"
serde = { version = "1.0", features = ["derive"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true }
thiserror = "2.0" thiserror = "2.0"
[dev-dependencies] [dev-dependencies]
claims = "0.8"
rstest = "0.23"
[features] [features]
default = [] default = []
@ -53,3 +54,6 @@ pedantic = "warn"
nursery = "warn" nursery = "warn"
unwrap_used = "warn" unwrap_used = "warn"
expect_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)] #[allow(clippy::module_name_repetitions)]
#[derive(Default)] #[derive(Default)]
pub struct MazeBuilder { pub struct MazeBuilder {
radius: Option<u32>, radius: Option<u16>,
seed: Option<u64>, seed: Option<u64>,
generator_type: GeneratorType, generator_type: GeneratorType,
start_position: Option<Hex>, start_position: Option<Hex>,
@ -88,7 +88,7 @@ pub struct MazeBuilder {
impl MazeBuilder { impl MazeBuilder {
/// Creates a new [`MazeBuilder`] instance with default settings. /// Creates a new [`MazeBuilder`] instance with default settings.
#[inline] #[cfg_attr(not(debug_assertions), inline)]
#[must_use] #[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
@ -104,9 +104,9 @@ impl MazeBuilder {
/// # Arguments /// # Arguments
/// ///
/// - `radius` - The number of tiles from the center to the edge of the hexagon. /// - `radius` - The number of tiles from the center to the edge of the hexagon.
#[inline] #[cfg_attr(not(debug_assertions), inline)]
#[must_use] #[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.radius = Some(radius);
self self
} }
@ -118,7 +118,7 @@ impl MazeBuilder {
/// # Arguments /// # Arguments
/// ///
/// - `seed` - The random seed value. /// - `seed` - The random seed value.
#[inline] #[cfg_attr(not(debug_assertions), inline)]
#[must_use] #[must_use]
pub const fn with_seed(mut self, seed: u64) -> Self { pub const fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed); self.seed = Some(seed);
@ -132,7 +132,7 @@ impl MazeBuilder {
/// # Arguments /// # Arguments
/// ///
/// - `generator_type` - The maze generation algorithm to use. /// - `generator_type` - The maze generation algorithm to use.
#[inline] #[cfg_attr(not(debug_assertions), inline)]
#[must_use] #[must_use]
pub const fn with_generator(mut self, generator_type: GeneratorType) -> Self { pub const fn with_generator(mut self, generator_type: GeneratorType) -> Self {
self.generator_type = generator_type; self.generator_type = generator_type;
@ -143,7 +143,7 @@ impl MazeBuilder {
/// # Arguments /// # Arguments
/// ///
/// - `pos` - The hexagonal coordinates for the starting position. /// - `pos` - The hexagonal coordinates for the starting position.
#[inline] #[cfg_attr(not(debug_assertions), inline)]
#[must_use] #[must_use]
pub const fn with_start_position(mut self, pos: Hex) -> Self { pub const fn with_start_position(mut self, pos: Hex) -> Self {
self.start_position = Some(pos); 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 mut maze = HexMaze::new();
let radius = i32::try_from(radius).unwrap_or(5); let radius = i32::from(radius);
for q in -radius..=radius { for q in -radius..=radius {
let r1 = (-radius).max(-q - radius); let r1 = (-radius).max(-q - radius);
@ -218,194 +218,68 @@ fn create_hex_maze(radius: u32) -> HexMaze {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use hexx::EdgeDirection;
use super::*; use super::*;
use claims::assert_gt;
/// Helper function to count the number of tiles for a given radius use rstest::rstest;
fn calculate_hex_tiles(radius: u32) -> usize {
let r = radius as i32;
(3 * r * r + 3 * r + 1) as usize
}
#[test] #[test]
fn new_builder() { fn maze_builder_new() {
let builder = MazeBuilder::new(); let builder = MazeBuilder::new();
assert!(builder.radius.is_none()); assert_eq!(builder.radius, None);
assert!(builder.seed.is_none()); assert_eq!(builder.seed, None);
assert!(builder.start_position.is_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] #[test]
fn builder_with_radius() { fn create_hex_maze_large_radius() {
let radius = 5; let large_radius = 1000;
let maze = MazeBuilder::new().with_radius(radius).build().unwrap(); let maze = create_hex_maze(large_radius);
assert_gt!(maze.len(), 0);
assert_eq!(maze.len(), calculate_hex_tiles(radius)); // Calculate expected size for this radius
assert!(maze.get_tile(&Hex::ZERO).is_some()); let expected_size = 3 * (large_radius as usize).pow(2) + 3 * large_radius as usize + 1;
assert_eq!(maze.len(), expected_size);
} }
#[test] #[test]
fn builder_without_radius() { fn create_hex_maze_tile_positions() {
let maze = MazeBuilder::new().build(); let maze = create_hex_maze(2);
assert!(matches!(maze, Err(MazeBuilderError::NoRadius))); let expected_positions = [
} Hex::new(0, 0),
Hex::new(1, -1),
#[test] Hex::new(1, 0),
fn builder_with_seed() { Hex::new(0, 1),
let radius = 3; Hex::new(-1, 1),
let seed = 12345; Hex::new(-1, 0),
Hex::new(0, -1),
let maze1 = MazeBuilder::new() Hex::new(2, -2),
.with_radius(radius) Hex::new(2, -1),
.with_seed(seed) Hex::new(2, 0),
.build() Hex::new(1, 1),
.unwrap(); Hex::new(0, 2),
Hex::new(-1, 2),
let maze2 = MazeBuilder::new() Hex::new(-2, 2),
.with_radius(radius) Hex::new(-2, 1),
.with_seed(seed) Hex::new(-2, 0),
.build() Hex::new(-1, -1),
.unwrap(); Hex::new(0, -2),
Hex::new(1, -2),
// Same seed should produce identical mazes ];
assert_eq!(maze1, maze2); for pos in expected_positions.iter() {
} assert!(maze.get_tile(pos).is_some(), "Expected tile at {:?}", pos);
#[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
);
}
}
} }
} }
} }

View File

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

View File

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

View File

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

View File

@ -16,23 +16,39 @@
//!``` //!```
//! use hexlab::prelude::*; //! use hexlab::prelude::*;
//! //!
//! // Create a new maze
//! let maze = MazeBuilder::new() //! let maze = MazeBuilder::new()
//! .with_radius(5) //! .with_radius(3)
//! .build() //! .build()
//! .expect("Failed to create maze"); //! .expect("Failed to create maze");
//! //!
//! // Get a specific tile //! assert_eq!(maze.len(), 37); // A radius of 3 should create 37 tiles
//! let tile = maze.get_tile(&Hex::new(1, -1)).unwrap();
//!
//! // Check if a wall exists
//! let has_wall = tile.walls().contains(EdgeDirection::FLAT_NORTH);
//!``` //!```
//! //!
//! # 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 builder;
mod generator; mod generator;
mod hex_maze; mod hex_maze;

View File

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