diff --git a/Cargo.lock b/Cargo.lock index c4f52a8..185f51d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1248,7 +1248,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", "syn", ] @@ -1631,7 +1631,7 @@ dependencies = [ "log", "rangemap", "rayon", - "rustc-hash", + "rustc-hash 1.1.0", "rustybuzz", "self_cell", "swash", @@ -1743,6 +1743,18 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +[[package]] +name = "deprecate-until" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a3767f826efbbe5a5ae093920b58b43b01734202be697e1354914e862e8e704" +dependencies = [ + "proc-macro2", + "quote", + "semver", + "syn", +] + [[package]] name = "derive_more" version = "1.0.0" @@ -2338,7 +2350,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hexlab" -version = "0.5.3" +version = "0.6.0" dependencies = [ "bevy", "bevy_reflect", @@ -2346,6 +2358,7 @@ dependencies = [ "claims", "glam", "hexx", + "pathfinding", "rand", "rstest", "serde", @@ -2354,9 +2367,9 @@ dependencies = [ [[package]] name = "hexx" -version = "0.19.0" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b47b6f7ee46bba869534a92306b7e7f549bec38114a4ba0288b9321617db22" +checksum = "4b450e02a24a4a981c895be4cd2752e2401996c545971309730c4e812b984691" dependencies = [ "bevy_reflect", "glam", @@ -2420,6 +2433,15 @@ dependencies = [ "libc", ] +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + [[package]] name = "io-kit-sys" version = "0.4.1" @@ -2687,7 +2709,7 @@ dependencies = [ "indexmap", "log", "pp-rs", - "rustc-hash", + "rustc-hash 1.1.0", "spirv", "termcolor", "thiserror 1.0.69", @@ -2708,7 +2730,7 @@ dependencies = [ "once_cell", "regex", "regex-syntax 0.8.5", - "rustc-hash", + "rustc-hash 1.1.0", "thiserror 1.0.69", "tracing", "unicode-ident", @@ -3166,6 +3188,20 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathfinding" +version = "4.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301ad6aa19104eeb9af172b3d6a4ab8a5ea26234890baf2fcb1cbbc3f05f674b" +dependencies = [ + "deprecate-until", + "indexmap", + "integer-sqrt", + "num-traits", + "rustc-hash 2.1.0", + "thiserror 2.0.3", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -3567,6 +3603,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc-hash" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4283,7 +4325,7 @@ dependencies = [ "parking_lot", "profiling", "raw-window-handle", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 1.0.69", "wgpu-hal", @@ -4325,7 +4367,7 @@ dependencies = [ "range-alloc", "raw-window-handle", "renderdoc-sys", - "rustc-hash", + "rustc-hash 1.1.0", "smallvec", "thiserror 1.0.69", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 0673bff..603250a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "hexlab" authors = ["Kristofers Solo "] -version = "0.5.3" +version = "0.6.0" edition = "2021" description = "A hexagonal maze generation and manipulation library" repository = "https://github.com/kristoferssolo/hexlab" @@ -26,6 +26,7 @@ thiserror = "2.0" bevy = { version = "0.15", optional = true } bevy_utils = { version = "0.15", optional = true } glam = { version = "0.29", optional = true } +pathfinding = { version = "4.13", optional = true } [dependencies.bevy_reflect] @@ -48,7 +49,8 @@ bevy_reflect = [ "hexx/bevy_reflect", "dep:glam", ] -full = ["serde", "bevy"] +pathfinding = ["dep:pathfinding"] +full = ["serde", "bevy", "pathfinding"] [profile.dev] opt-level = 1 # Better compile times with some optimization diff --git a/src/lib.rs b/src/lib.rs index 25348f7..06e22eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,6 +52,8 @@ mod builder; pub mod errors; mod generator; mod maze; +#[cfg(feature = "pathfinding")] +mod pathfinding; mod tile; pub mod traits; mod walls; diff --git a/src/pathfinding.rs b/src/pathfinding.rs new file mode 100644 index 0000000..5c66c15 --- /dev/null +++ b/src/pathfinding.rs @@ -0,0 +1,33 @@ +use hexx::{EdgeDirection, Hex}; +use pathfinding::prelude::*; + +use crate::Maze; + +impl Maze { + pub fn find_path(&self, from: Hex, to: Hex) -> Option> { + let successors = |pos: &Hex| { + { + EdgeDirection::ALL_DIRECTIONS.iter().filter_map(|&dir| { + let neighbor = pos.neighbor(dir); + if let Some(current_tile) = self.get(pos) { + if let Some(_) = self.get(&neighbor) { + if !current_tile.walls.contains(dir) { + return Some((neighbor, 1)); // Cost of 1 for each step + } + } + } + None + }) + } + .collect::>() + }; + + let heuristic = |pos: &Hex| { + // Manhatan distance + let diff = *pos - to; + (diff.x.abs() + diff.y.abs() + diff.z().abs()) as u32 / 2 + }; + + astar(&from, successors, heuristic, |pos| *pos == to).map(|(path, _)| path) + } +} diff --git a/tests/pathfinding.rs b/tests/pathfinding.rs new file mode 100644 index 0000000..dad0742 --- /dev/null +++ b/tests/pathfinding.rs @@ -0,0 +1,79 @@ +use claims::*; +use hexlab::MazeBuilder; +use hexx::{hex, EdgeDirection, Hex}; + +#[test] +fn basic_path() { + let maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build()); + + let start = Hex::new(0, 0); + let goal = Hex::new(2, 0); + + assert_some_eq!( + maze.find_path(start, goal), + vec![start, hex(1, 0), hex(1, 1), hex(2, 1), goal] + ); +} + +#[test] +fn path_with_walls() { + let mut maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build()); + let start = Hex::new(0, 0); + let goal = Hex::new(2, 0); + + // Block direct path with wall + assert_ok!(maze.add_tile_wall(&start, EdgeDirection::FLAT_SOUTH)); + + // Should find alternative path or no path + let path = maze.find_path(start, goal); + if let Some(path) = path { + // If path exists, verify it's valid + assert!(path.len() > 3); // Should be longer than direct path + assert_eq!(path.first(), Some(&start)); + assert_eq!(path.last(), Some(&goal)); + } +} + +#[test] +fn path_to_self() { + let maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build()); + let pos = Hex::new(0, 0); + + assert_some_eq!(maze.find_path(pos, pos), vec![pos]); +} + +#[test] +fn no_path_exists() { + let mut maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(5).build()); + let start = Hex::new(0, 0); + let goal = Hex::new(2, 0); + + // Surround start with walls + for dir in EdgeDirection::ALL_DIRECTIONS { + assert_ok!(maze.add_tile_wall(&start, dir)); + } + + assert_none!(maze.find_path(start, goal)); +} + +#[test] +fn path_in_larger_maze() { + let maze = assert_ok!(MazeBuilder::new().with_seed(12345).with_radius(10).build()); + let start = Hex::new(-5, -5); + let goal = Hex::new(5, 5); + + let path = assert_some!(maze.find_path(start, goal)); + + // Basic path properties + assert_eq!(path.first(), Some(&start)); + assert_eq!(path.last(), Some(&goal)); + + // Path should be continuous + for window in path.windows(2) { + let current = window[0]; + let next = window[1]; + assert!(EdgeDirection::ALL_DIRECTIONS + .iter() + .any(|&dir| current.neighbor(dir) == next)); + } +}