From 6cd755008651c8a64bd45e9ab0a0d4583639d0e0 Mon Sep 17 00:00:00 2001 From: Kristofers Solo Date: Tue, 14 Jan 2025 11:35:20 +0200 Subject: [PATCH] feat(pathfinding): add A* --- Cargo.lock | 6 +- Cargo.toml | 5 +- src/lib.rs | 2 + src/pathfinding/maze.rs | 137 ++++++++++++++++++++++++++++++++++++++++ src/pathfinding/mod.rs | 2 + src/pathfinding/node.rs | 20 ++++++ 6 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 src/pathfinding/maze.rs create mode 100644 src/pathfinding/mod.rs create mode 100644 src/pathfinding/node.rs diff --git a/Cargo.lock b/Cargo.lock index c4f52a8..a2b05bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2338,7 +2338,7 @@ checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" [[package]] name = "hexlab" -version = "0.5.3" +version = "0.6.0" dependencies = [ "bevy", "bevy_reflect", @@ -2354,9 +2354,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", diff --git a/Cargo.toml b/Cargo.toml index 0673bff..9b2ad83 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" @@ -48,7 +48,8 @@ bevy_reflect = [ "hexx/bevy_reflect", "dep:glam", ] -full = ["serde", "bevy"] +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/maze.rs b/src/pathfinding/maze.rs new file mode 100644 index 0000000..feb18f3 --- /dev/null +++ b/src/pathfinding/maze.rs @@ -0,0 +1,137 @@ +use crate::Maze; +#[cfg(feature = "bevy_reflect")] +use bevy_utils::{HashMap, HashSet}; +use hexx::{EdgeDirection, Hex}; +#[cfg(not(feature = "bevy_reflect"))] +use std::collections::{HashMap, HashSet}; +use std::{collections::BinaryHeap, i32}; + +use super::node::Node; + +impl Maze { + pub fn find_path(&self, from: Hex, to: Hex) -> Option> { + let mut open_set = BinaryHeap::new(); + let mut came_from = HashMap::new(); + let mut g_score = HashMap::new(); + let mut closed_set = HashSet::new(); + + initialize_search(&mut open_set, &mut g_score, from, to); + while let Some(current) = open_set.pop() { + if current.position == to { + return Some(self.reconstruct_path(came_from, current.position)); + } + if !self.process_current_node( + current, + to, + &mut open_set, + &mut came_from, + &mut g_score, + &mut closed_set, + ) { + continue; + } + } + None + } + + fn is_valid_move(&self, from: &Hex, to: &Hex, direction: EdgeDirection) -> bool { + if let Some(current_tile) = self.get(from) { + if let Some(_) = self.get(to) { + return !current_tile.walls.contains(direction); + } + } + false + } + + fn process_current_node( + &self, + current: Node, + goal: Hex, + open_set: &mut BinaryHeap, + came_from: &mut HashMap, + g_score: &mut HashMap, + closed_set: &mut HashSet, + ) -> bool { + if closed_set.contains(¤t.position) { + return false; + } + closed_set.insert(current.position); + + self.process_neighbors(current, goal, open_set, came_from, g_score, closed_set); + + true + } + + fn process_neighbors( + &self, + current: Node, + goal: Hex, + open_set: &mut BinaryHeap, + came_from: &mut HashMap, + g_score: &mut HashMap, + closed_set: &HashSet, + ) { + for direction in EdgeDirection::ALL_DIRECTIONS { + let neighbor_pos = current.position.neighbor(direction); + if closed_set.contains(&neighbor_pos) { + continue; + } + if self.is_valid_move(¤t.position, &neighbor_pos, direction) { + self.update_neighbor(current, neighbor_pos, goal, open_set, came_from, g_score); + } + } + } + + fn update_neighbor( + &self, + current: Node, + neighbor_pos: Hex, + goal: Hex, + open_set: &mut BinaryHeap, + came_from: &mut HashMap, + g_score: &mut HashMap, + ) { + let tentative_g_score = g_score.get(¤t.position).unwrap() + 1; + + if tentative_g_score < *g_score.get(&neighbor_pos).unwrap_or(&i32::MAX) { + came_from.insert(neighbor_pos, current.position); + g_score.insert(neighbor_pos, tentative_g_score); + open_set.push(Node { + position: neighbor_pos, + f_score: tentative_g_score + heuristic(neighbor_pos, goal), + g_score: tentative_g_score, + }); + } + } + + fn reconstruct_path(&self, came_from: HashMap, current: Hex) -> Vec { + let mut path = vec![current]; + let mut current_pos = current; + while let Some(&prev) = came_from.get(¤t_pos) { + path.push(prev); + current_pos = prev; + } + path.reverse(); + path + } +} + +fn initialize_search( + open_set: &mut BinaryHeap, + g_score: &mut HashMap, + start: Hex, + goal: Hex, +) { + g_score.insert(start, 0); + open_set.push(Node { + position: start, + f_score: heuristic(start, goal), + g_score: 0, + }) +} + +fn heuristic(from: Hex, to: Hex) -> i32 { + // Manhatan distance for hex grid + let pos = from - to; + pos.x.abs() + pos.y.abs() +} diff --git a/src/pathfinding/mod.rs b/src/pathfinding/mod.rs new file mode 100644 index 0000000..2864822 --- /dev/null +++ b/src/pathfinding/mod.rs @@ -0,0 +1,2 @@ +mod maze; +mod node; diff --git a/src/pathfinding/node.rs b/src/pathfinding/node.rs new file mode 100644 index 0000000..a27f7e1 --- /dev/null +++ b/src/pathfinding/node.rs @@ -0,0 +1,20 @@ +use hexx::Hex; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Node { + pub position: Hex, + pub f_score: i32, + pub g_score: i32, +} + +impl Ord for Node { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.f_score.cmp(&other.f_score) + } +} + +impl PartialOrd for Node { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +}