diff --git a/Cargo.lock b/Cargo.lock index 1f3a04c..e5dc3b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 21be02e..1137472 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 } diff --git a/src/builder.rs b/src/builder.rs index 2670165..169c7ea 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -80,7 +80,7 @@ pub enum MazeBuilderError { #[allow(clippy::module_name_repetitions)] #[derive(Default)] pub struct MazeBuilder { - radius: Option, + radius: Option, seed: Option, generator_type: GeneratorType, start_position: Option, @@ -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); } } } diff --git a/src/generator.rs b/src/generator.rs index 579a69b..082787a 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -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, diff --git a/src/hex_maze.rs b/src/hex_maze.rs index 8186e69..e41957a 100644 --- a/src/hex_maze.rs +++ b/src/hex_maze.rs @@ -21,7 +21,7 @@ pub struct HexMaze(HashMap); 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() diff --git a/src/hex_tile.rs b/src/hex_tile.rs index 8ccef70..863ce91 100644 --- a/src/hex_tile.rs +++ b/src/hex_tile.rs @@ -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); diff --git a/src/lib.rs b/src/lib.rs index 7dead81..2fb8c48 100644 --- a/src/lib.rs +++ b/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; diff --git a/src/walls.rs b/src/walls.rs index 102735b..54af979 100644 --- a/src/walls.rs +++ b/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(&mut self, direction: T) where T: Into + 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(&mut self, direction: T) -> bool where T: Into + 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(&self, other: T) -> bool where T: Into + 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(&mut self, other: T) where T: Into, diff --git a/tests/builder.rs b/tests/builder.rs new file mode 100644 index 0000000..832781c --- /dev/null +++ b/tests/builder.rs @@ -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); +}