mirror of
https://github.com/kristoferssolo/hexlab.git
synced 2025-10-21 19:40:34 +00:00
test(builder): 100% builder tests
This commit is contained in:
parent
389c8ee1fd
commit
7cacf92014
139
Cargo.lock
generated
139
Cargo.lock
generated
@ -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"
|
||||
|
||||
@ -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 }
|
||||
|
||||
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>,
|
||||
|
||||
139
tests/builder.rs
Normal file
139
tests/builder.rs
Normal 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);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user