From e9f02e362a9da400f3b52c7de60e59d480633c27 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Wed, 8 Jan 2025 18:27:07 +0200 Subject: [PATCH] feat(maze): add Coordinates trait --- Cargo.lock | 7 ++ Cargo.toml | 1 + src/maze/components.rs | 110 ++++++++++++++++++------------- src/maze/coordinates.rs | 139 ++++++++++++++++++++++++++++++++++++++++ src/maze/errors.rs | 6 +- src/maze/mod.rs | 1 + 6 files changed, 219 insertions(+), 45 deletions(-) create mode 100644 src/maze/coordinates.rs diff --git a/Cargo.lock b/Cargo.lock index 9644b97..8243877 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1614,6 +1614,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" @@ -3135,6 +3141,7 @@ dependencies = [ "bevy", "bevy-inspector-egui", "bevy_egui", + "claims", "hexlab", "hexx", "log", diff --git a/Cargo.toml b/Cargo.toml index 9d09bdc..200ba1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ anyhow = "1" strum = { version = "0.26", features = ["derive"] } [dev-dependencies] +claims = "0.8.0" rstest = "0.24" rstest_reuse = "0.7" test-log = { version = "0.2.16", default-features = false, features = [ diff --git a/src/maze/components.rs b/src/maze/components.rs index 537394e..130365e 100644 --- a/src/maze/components.rs +++ b/src/maze/components.rs @@ -3,7 +3,7 @@ //! Module defines the core components and configuration structures used //! for maze generation and rendering, including hexagonal maze layouts, //! tiles, walls, and maze configuration. -use super::GlobalMazeConfig; +use super::{coordinates::is_within_radius, GlobalMazeConfig}; use crate::floor::components::Floor; use bevy::prelude::*; @@ -110,15 +110,16 @@ impl Default for MazeConfig { /// A valid Hex coordinate within the specified radius fn generate_pos(radius: u16, rng: &mut R) -> Hex { let radius = radius as i32; - loop { - let q = rng.gen_range(-radius..=radius); - let r = rng.gen_range(-radius..=radius); - let s = -q - r; // Calculate third coordinate (axial coordinates: q + r + s = 0) - // Check if the position is within the hexagonal radius - // Using the formula: max(abs(q), abs(r), abs(s)) <= radius - if q.abs().max(r.abs()).max(s.abs()) <= radius { - return Hex::new(q, r); + loop { + // Generate coordinates using cube coordinate bounds + let q = rng.gen_range(-radius..=radius); + let r = rng.gen_range((-radius).max(-q - radius)..=radius.min(-q + radius)); + + if let Ok(is_valid) = is_within_radius(radius, &(q, r)) { + if is_valid { + return Hex::new(q, r); + } } } } @@ -126,20 +127,9 @@ fn generate_pos(radius: u16, rng: &mut R) -> Hex { #[cfg(test)] mod tests { use super::*; + use claims::*; use rstest::*; - fn is_within_radius(hex: Hex, radius: u16) -> bool { - let q = hex.x; - let r = hex.y; - let s = -q - r; - q.abs().max(r.abs()).max(s.abs()) <= radius as i32 - } - - #[fixture] - fn test_radius() -> Vec { - vec![1, 2, 5, 8] - } - #[rstest] #[case(1)] #[case(2)] @@ -156,18 +146,8 @@ mod tests { assert_eq!(config.seed, 12345); assert_eq!(config.layout.orientation, orientation); - assert!( - is_within_radius(config.start_pos, radius), - "Start pos {:?} outside radius {}", - config.start_pos, - radius - ); - assert!( - is_within_radius(config.end_pos, radius), - "End pos {:?} outside radius {}", - config.end_pos, - radius - ); + assert_ok!(is_within_radius(radius, &config.start_pos),); + assert_ok!(is_within_radius(radius, &config.end_pos)); assert_ne!(config.start_pos, config.end_pos); } @@ -178,13 +158,13 @@ mod tests { let config = MazeConfig::default(); let radius = config.radius; - assert!(is_within_radius(config.start_pos, radius)); - assert!(is_within_radius(config.end_pos, radius)); + assert_ok!(is_within_radius(radius, &config.start_pos)); + assert_ok!(is_within_radius(radius, &config.end_pos)); assert_ne!(config.start_pos, config.end_pos); } } - #[rstest] + #[test] fn maze_config_default_with_seeds() { let test_seeds = [ None, @@ -206,8 +186,8 @@ mod tests { assert_eq!(config.radius, 8); assert_eq!(config.layout.orientation, HexOrientation::Flat); - assert!(is_within_radius(config.start_pos, 8)); - assert!(is_within_radius(config.end_pos, 8)); + assert_ok!(is_within_radius(8, &config.start_pos)); + assert_ok!(is_within_radius(8, &config.end_pos)); assert_ne!(config.start_pos, config.end_pos); } } @@ -238,12 +218,7 @@ mod tests { for _ in 0..10 { let pos = generate_pos(radius, &mut rng); - assert!( - is_within_radius(pos, radius), - "Position {:?} outside radius {}", - pos, - radius - ); + assert_ok!(is_within_radius(radius, &pos),); } } @@ -317,4 +292,51 @@ mod tests { assert_eq!(config.layout.hex_size.x, 0.0); assert_eq!(config.layout.hex_size.y, 0.0); } + + #[test] + fn basic_generation() { + let mut rng = thread_rng(); + let radius = 2; + let hex = generate_pos(radius, &mut rng); + + // Test that generated position is within radius + assert_ok!(is_within_radius(radius as i32, &(hex.x, hex.y))); + } + + #[rstest] + #[case(1)] + #[case(2)] + #[case(3)] + #[case(6)] + fn multiple_radii(#[case] radius: u16) { + let mut rng = thread_rng(); + + // Generate multiple points for each radius + for _ in 0..100 { + let hex = generate_pos(radius, &mut rng); + assert_ok!(is_within_radius(radius, &hex)); + } + } + + #[test] + fn zero_radius() { + let mut rng = thread_rng(); + let hex = generate_pos(0, &mut rng); + + // With radius 0, only (0,0) should be possible + assert_eq!(hex.x, 0); + assert_eq!(hex.y, 0); + } + + #[test] + fn large_radius() { + let mut rng = thread_rng(); + let radius = 100; + let iterations = 100; + + for _ in 0..iterations { + let hex = generate_pos(radius, &mut rng); + assert_ok!(is_within_radius(radius, &hex)); + } + } } diff --git a/src/maze/coordinates.rs b/src/maze/coordinates.rs new file mode 100644 index 0000000..14447d4 --- /dev/null +++ b/src/maze/coordinates.rs @@ -0,0 +1,139 @@ +use hexx::Hex; + +use super::errors::RadiusError; + +pub trait Coordinates { + fn get_coords(&self) -> (i32, i32); +} + +impl Coordinates for (i32, i32) { + fn get_coords(&self) -> (i32, i32) { + *self + } +} + +impl Coordinates for Hex { + fn get_coords(&self) -> (i32, i32) { + (self.x, self.y) + } +} + +pub fn is_within_radius(radius: R, coords: &C) -> Result +where + R: Into, + C: Coordinates, +{ + let radius = radius.into(); + + if radius < 0 { + return Err(RadiusError::NegativeRadius(radius)); + } + + let (q, r) = coords.get_coords(); + let s = -q - r; // Calculate third axial coordinate (q + r + s = 0) + + Ok(q.abs().max(r.abs()).max(s.abs()) <= radius) +} + +#[cfg(test)] +mod tests { + use super::*; + use claims::*; + use rstest::*; + + #[rstest] + // Original test cases + #[case(0, (0, 0), true)] // Center point + #[case(1, (1, 0), true)] // Point at radius 1 + #[case(1, (2, 0), false)] // Point outside radius 1 + #[case(2, (2, 0), true)] // East + #[case(2, (0, 2), true)] // Southeast + #[case(2, (-2, 2), true)] // Southwest + #[case(2, (-2, 0), true)] // West + #[case(2, (0, -2), true)] // Northwest + #[case(2, (2, -2), true)] // Northeast + #[case(2, (3, 0), false)] // Just outside radius 2 + // Large radius test cases + #[case(6, (6, 0), true)] // East at radius 6 + #[case(6, (0, 6), true)] // Southeast at radius 6 + #[case(6, (-6, 6), true)] // Southwest at radius 6 + #[case(6, (-6, 0), true)] // West at radius 6 + #[case(6, (0, -6), true)] // Northwest at radius 6 + #[case(6, (6, -6), true)] // Northeast at radius 6 + #[case(6, (7, 0), false)] // Just outside radius 6 east + #[case(6, (4, 4), false)] // Outside radius 6 diagonal + #[case(6, (5, 5), false)] // Outside radius 6 diagonal + // Edge cases with large radius + #[case(6, (6, -3), true)] // Complex position within radius 6 + #[case(6, (-3, 6), true)] // Complex position within radius 6 + #[case(6, (3, -6), true)] // Complex position within radius 6 + #[case(6, (7, -7), false)] // Outside radius 6 corner + fn valid_radius_tuple(#[case] radius: i32, #[case] pos: (i32, i32), #[case] expected: bool) { + let result = is_within_radius(radius, &pos); + assert_ok_eq!(result, expected); + } + + #[rstest] + // Large radius test cases for Hex struct + #[case(6, (6, 0), true)] // East at radius 6 + #[case(6, (0, 6), true)] // Southeast at radius 6 + #[case(6, (-6, 6), true)] // Southwest at radius 6 + #[case(6, (-6, 0), true)] // West at radius 6 + #[case(6, (0, -6), true)] // Northwest at radius 6 + #[case(6, (6, -6), true)] // Northeast at radius 6 + #[case(6, (4, 4), false)] // Outside radius 6 diagonal + #[case(6, (5, 5), false)] // Outside radius 6 diagonal + fn valid_radius_hex(#[case] radius: i32, #[case] pos: (i32, i32), #[case] expected: bool) { + let hex = Hex::from(pos); + let result = is_within_radius(radius, &hex); + assert_ok_eq!(result, expected); + } + + #[rstest] + #[case(-1)] + #[case(-2)] + #[case(-5)] + fn negative_radius(#[case] radius: i32) { + let result = is_within_radius(radius, &(0, 0)); + assert_err!(&result); + } + + #[test] + fn boundary_points() { + let radius = 3; + // Test points exactly on the boundary of radius 3 + assert_ok_eq!(is_within_radius(radius, &(3, 0)), true); // East boundary + assert_ok_eq!(is_within_radius(radius, &(0, 3)), true); // Southeast boundary + assert_ok_eq!(is_within_radius(radius, &(-3, 3)), true); // Southwest boundary + assert_ok_eq!(is_within_radius(radius, &(-3, 0)), true); // West boundary + assert_ok_eq!(is_within_radius(radius, &(0, -3)), true); // Northwest boundary + assert_ok_eq!(is_within_radius(radius, &(3, -3)), true); // Northeast boundary + } + + #[test] + fn large_boundary_points() { + let radius = 6; + // Test points exactly on the boundary of radius 6 + assert_ok_eq!(is_within_radius(radius, &(6, 0)), true); // East boundary + assert_ok_eq!(is_within_radius(radius, &(0, 6)), true); // Southeast boundary + assert_ok_eq!(is_within_radius(radius, &(-6, 6)), true); // Southwest boundary + assert_ok_eq!(is_within_radius(radius, &(-6, 0)), true); // West boundary + assert_ok_eq!(is_within_radius(radius, &(0, -6)), true); // Northwest boundary + assert_ok_eq!(is_within_radius(radius, &(6, -6)), true); // Northeast boundary + + // Test points just outside the boundary + assert_ok_eq!(is_within_radius(radius, &(7, 0)), false); // Just outside east + assert_ok_eq!(is_within_radius(radius, &(0, 7)), false); // Just outside southeast + assert_ok_eq!(is_within_radius(radius, &(-7, 7)), false); // Just outside southwest + } + + #[test] + fn different_coordinate_types() { + // Test with tuple coordinates + assert_ok_eq!(is_within_radius(2, &(1, 1)), true); + + // Test with Hex struct + let hex = Hex { x: 1, y: 1 }; + assert_ok_eq!(is_within_radius(2, &hex), true); + } +} diff --git a/src/maze/errors.rs b/src/maze/errors.rs index f287bdc..2a186da 100644 --- a/src/maze/errors.rs +++ b/src/maze/errors.rs @@ -25,7 +25,11 @@ pub enum MazeError { Other(#[from] anyhow::Error), } -pub type MazeResult = Result; +#[derive(Debug, Error)] +pub enum RadiusError { + #[error("Radius cannot be negative: {0}")] + NegativeRadius(i32), +} impl MazeError { pub fn config_error(msg: impl Into) -> Self { diff --git a/src/maze/mod.rs b/src/maze/mod.rs index 648f171..485961e 100644 --- a/src/maze/mod.rs +++ b/src/maze/mod.rs @@ -1,6 +1,7 @@ mod assets; pub mod commands; pub mod components; +pub mod coordinates; pub mod errors; pub mod resources; mod systems;