From 7ab951ac052d5092c01fac27779462acd48788d8 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 8 Nov 2025 16:41:57 +0100 Subject: [PATCH 01/41] Made S ('swamp') on maps passable --- grid_pathfinding_benchmark/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/grid_pathfinding_benchmark/src/lib.rs b/grid_pathfinding_benchmark/src/lib.rs index a1e318a..5b5ade5 100644 --- a/grid_pathfinding_benchmark/src/lib.rs +++ b/grid_pathfinding_benchmark/src/lib.rs @@ -73,7 +73,7 @@ fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point)>) { for x in 0..bool_grid.width() as i32 { // Not sure why x, y have to be swapped here... let tile_val = lines[offset + x as usize].as_bytes()[y as usize]; - let val = ![b'.', b'G'].contains(&tile_val); + let val = ![b'.', b'G', b'S'].contains(&tile_val); bool_grid.set(x, y, val); } } From 8eb6b3c6f39a156ea594cc6aaa09a5c6a2647715 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 8 Nov 2025 17:04:35 +0100 Subject: [PATCH 02/41] Made load_benchmark also pass target distance for each point pair as step towards verification --- benches/comparison_bench.rs | 2 +- benches/single_bench.rs | 2 +- examples/benchmark_runner.rs | 4 ++-- grid_pathfinding_benchmark/src/lib.rs | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index df9dd35..1d628d0 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -25,7 +25,7 @@ fn dao_bench(c: &mut Criterion) { c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { b.iter(|| { - for (start, end) in &scenarios { + for (start, end, _) in &scenarios { black_box(pathing_grid.get_path_single_goal(*start, *end, false)); } }) diff --git a/benches/single_bench.rs b/benches/single_bench.rs index 60c27b3..afe3e0c 100644 --- a/benches/single_bench.rs +++ b/benches/single_bench.rs @@ -21,7 +21,7 @@ fn dao_bench_single(c: &mut Criterion) { c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { b.iter(|| { - for (start, end) in &scenarios { + for (start, end, _) in &scenarios { black_box(pathing_grid.get_path_single_goal(*start, *end, false)); } }) diff --git a/examples/benchmark_runner.rs b/examples/benchmark_runner.rs index 2f22aa2..33d72cc 100644 --- a/examples/benchmark_runner.rs +++ b/examples/benchmark_runner.rs @@ -35,8 +35,8 @@ fn main() { println!("\tTotal benchmark time: {:.2?}", total_time); } -pub fn run_scenarios(pathing_grid: &PathingGrid, scenarios: &Vec<(Point, Point)>) { - for (start, goal) in scenarios { +pub fn run_scenarios(pathing_grid: &PathingGrid, scenarios: &Vec<(Point, Point, f64)>) { + for (start, goal, _) in scenarios { let path: Option> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); assert!(path.is_some()); } diff --git a/grid_pathfinding_benchmark/src/lib.rs b/grid_pathfinding_benchmark/src/lib.rs index 5b5ade5..7ddd7a2 100644 --- a/grid_pathfinding_benchmark/src/lib.rs +++ b/grid_pathfinding_benchmark/src/lib.rs @@ -22,7 +22,7 @@ pub struct Scenario { distance: f64, } -fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point)>) { +fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point, f64)>) { let map_str = fs::read_to_string(Path::new(&format!("./maps/{}.map", name))) .expect("Could not read scenario file"); @@ -45,14 +45,14 @@ fn load_benchmark(name: &str) -> (BoolGrid, Vec<(Point, Point)>) { // .flexible(true) .from_reader(remaining_data.as_bytes()); // Initialize an empty vector to store the parsed data - let mut data_array: Vec<(Point, Point)> = Vec::new(); + let mut data_array: Vec<(Point, Point, f64)> = Vec::new(); // Iterate over the records in the file for result in csv_reader.deserialize() { let record: Scenario = result.expect("Could not parse scenario record"); let start = Point::new(record.y1 as i32, record.x1 as i32); let goal = Point::new(record.y2 as i32, record.x2 as i32); - data_array.push((start, goal)); + data_array.push((start, goal, record.distance)); } let lines: Vec<&str> = map_str.lines().collect(); @@ -105,7 +105,7 @@ pub fn get_benchmark_names() -> Vec { names } -pub fn get_benchmark(name: String) -> (BoolGrid, Vec<(Point, Point)>) { +pub fn get_benchmark(name: String) -> (BoolGrid, Vec<(Point, Point, f64)>) { let benchmark_names = get_benchmark_names(); if benchmark_names.contains(&name) { load_benchmark(name.as_str()) From 0d6472ac42c382c9f6e67593f8b33f44486da274 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 8 Nov 2025 17:05:24 +0100 Subject: [PATCH 03/41] Added verify_solution_distance test, currently failing presumably due to corner cutting --- src/lib.rs | 5 +++++ tests/benchmark_distances.rs | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 tests/benchmark_distances.rs diff --git a/src/lib.rs b/src/lib.rs index f21c5cf..3e5cdc5 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,11 @@ const D: i32 = if EQUAL_EDGE_COST { 1 } else { 99 }; const C: i32 = if EQUAL_EDGE_COST { 1 } else { 70 }; const E: i32 = 2 * C - D; +/// Converts the integer cost to an approximate floating point equivalent where cardinal directions have cost 1.0. +pub fn convert_cost_to_unit_cost_float(cost: i32) -> f64 { + (cost as f64) / (C as f64) +} + /// Helper function for debugging binary representations of neighborhoods. pub fn explain_bin_neighborhood(nn: u8) { for i in 0..8_i32 { diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs new file mode 100644 index 0000000..1d4588e --- /dev/null +++ b/tests/benchmark_distances.rs @@ -0,0 +1,39 @@ +use grid_pathfinding::*; +use grid_pathfinding_benchmark::get_benchmark; +use grid_util::*; + +#[test] +fn verify_solution_distance() { + for pruning in [false, true] { + let bench_set = ["dao/arena", "dao/arena2"]; + for name in bench_set { + let (bool_grid, scenarios) = get_benchmark(name.to_owned()); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.allow_diagonal_move = true; + pathing_grid.improved_pruning = pruning; + pathing_grid.initialize(); + pathing_grid.generate_components(); + for (start, end, distance) in &scenarios { + println!("Distance: {distance}"); + let path = pathing_grid + .get_path_single_goal(*start, *end, false) + .unwrap(); + let mut v = path[0]; + let n = path.len(); + let mut total_cost_int = 0; + for i in 1..n { + let v_old = v; + v = path[i]; + let cost = pathing_grid.heuristic(&v_old, &v); + total_cost_int += cost; + } + let float_cost = convert_cost_to_unit_cost_float(total_cost_int); + println!("My distance: {float_cost}"); + let delta_dist = (float_cost - distance).abs() / distance; + assert!(delta_dist < 0.01); + } + } + } +} From cb60d194507ce9740b9b31a4baaa7b0d15a3056d Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 8 Nov 2025 19:38:43 +0100 Subject: [PATCH 04/41] Added ALLOW_CORNER_CUTTING as compile time constant with updated can_move_to. Makes A* (GRAPH_PRUNING=False) pass benchmark_distances --- src/lib.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3e5cdc5..44a686f 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,6 +19,7 @@ use std::collections::VecDeque; use std::sync::{Arc, Mutex}; const EQUAL_EDGE_COST: bool = false; +const ALLOW_CORNER_CUTTING: bool = false; const GRAPH_PRUNING: bool = true; const N_SMALLVEC_SIZE: usize = 8; @@ -110,7 +111,7 @@ impl PathingGrid { ) -> SmallVec<[(Point, i32); N_SMALLVEC_SIZE]> { self.neighborhood_points(pos) .into_iter() - .filter(|p| self.can_move_to(*p)) + .filter(|p| self.can_move_to(*p, *pos)) // See comment in pruned_neighborhood about cost calculation .map(move |p| (p, (pos.dir_obj(&p).num() % 2) * (D - C) + C)) .collect::>() @@ -128,7 +129,17 @@ impl PathingGrid { p1.manhattan_distance(p2) * C } } - fn can_move_to(&self, pos: Point) -> bool { + fn can_move_to(&self, pos: Point, start: Point) -> bool { + if ALLOW_CORNER_CUTTING { + self.can_move_to_simple(pos) + } else { + debug_assert!((start.x - pos.x).abs() <= 1 && (start.y - pos.y).abs() <= 1); + self.can_move_to_simple(pos) + && (!self.grid.get_point(Point::new(start.x, pos.y)) + && !self.grid.get_point(Point::new(pos.x, start.y))) + } + } + fn can_move_to_simple(&self, pos: Point) -> bool { self.point_in_bounds(pos) && !self.grid.get_point(pos) } fn in_bounds(&self, x: i32, y: i32) -> bool { From 585d0461abb844b82257425156b8ae98dfcaee8b Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 8 Nov 2025 19:41:07 +0100 Subject: [PATCH 05/41] Added handling of distance 0 case in comparison and added saving of path coordinates to csv for visualization etc. --- tests/benchmark_distances.rs | 68 +++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 28 deletions(-) diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index 1d4588e..ad090a7 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -1,38 +1,50 @@ use grid_pathfinding::*; use grid_pathfinding_benchmark::get_benchmark; use grid_util::*; +use std::fs::File; +use std::io::{BufWriter, Write}; + +fn save_path(path: Vec, filename: &str) -> std::io::Result<()> { + let f = File::create(filename)?; + let mut w = BufWriter::new(f); + for p in path { + writeln!(w, "{},{}", p.x, p.y)?; + } + Ok(()) +} #[test] fn verify_solution_distance() { - for pruning in [false, true] { - let bench_set = ["dao/arena", "dao/arena2"]; - for name in bench_set { - let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); - pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = true; - pathing_grid.improved_pruning = pruning; - pathing_grid.initialize(); - pathing_grid.generate_components(); - for (start, end, distance) in &scenarios { - println!("Distance: {distance}"); - let path = pathing_grid - .get_path_single_goal(*start, *end, false) - .unwrap(); - let mut v = path[0]; - let n = path.len(); - let mut total_cost_int = 0; - for i in 1..n { - let v_old = v; - v = path[i]; - let cost = pathing_grid.heuristic(&v_old, &v); - total_cost_int += cost; - } - let float_cost = convert_cost_to_unit_cost_float(total_cost_int); - println!("My distance: {float_cost}"); + let bench_set = ["dao/arena", "dao/lak107d", "dao/den101d"]; + for name in bench_set { + let (bool_grid, scenarios) = get_benchmark(name.to_owned()); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.allow_diagonal_move = true; + pathing_grid.improved_pruning = false; + pathing_grid.initialize(); + pathing_grid.generate_components(); + for (start, end, distance) in &scenarios { + println!("Start: {start}; End: {end}; Distance: {distance}"); + let path = pathing_grid + .get_path_single_goal(*start, *end, false) + .unwrap(); + let mut v = path[0]; + let n = path.len(); + let mut total_cost_int = 0; + for i in 1..n { + let v_old = v; + v = path[i]; + let cost = pathing_grid.heuristic(&v_old, &v); + total_cost_int += cost; + } + save_path(path, "path.csv").unwrap(); + let float_cost = convert_cost_to_unit_cost_float(total_cost_int); + println!("My distance: {float_cost}"); + if *distance >=0.01{ let delta_dist = (float_cost - distance).abs() / distance; - assert!(delta_dist < 0.01); + assert!(delta_dist < 0.05); } } } From 42e90612eb799e0cbe557411a9f2f0f42343f010 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 8 Nov 2025 20:11:02 +0100 Subject: [PATCH 06/41] Integrated new move_to logic elsewhere, still passing all tests but benchmark_distances when allowing corner cutting --- src/lib.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 44a686f..2f151b8 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -226,7 +226,7 @@ impl PathingGrid { debug_assert!(!direction.diagonal()); loop { initial = initial + direction; - if !self.can_move_to(initial) { + if !self.can_move_to_simple(initial) { return None; } @@ -250,11 +250,13 @@ impl PathingGrid { where F: Fn(&Point) -> bool, { + let mut new_initial: Point; loop { - initial = initial + direction; - if !self.can_move_to(initial) { + new_initial = initial + direction; + if !self.can_move_to(new_initial, initial) { return None; } + initial = new_initial; if goal(&initial) || self.is_forced(direction, &initial) { return Some((initial, cost)); @@ -641,9 +643,9 @@ impl ValueGrid for PathingGrid { self.components_dirty = true; } else { let p_ix = self.grid.compute_ix(x, y); - for p in self.neighborhood_points(&p) { - if self.can_move_to(p) { - self.components.union(p_ix, self.grid.get_ix_point(&p)); + for n in self.neighborhood_points(&p) { + if self.can_move_to(n, p) { + self.components.union(p_ix, self.grid.get_ix_point(&n)); } } } From aebc8c846934ed9639c1e1bbe6d8f4b12666ac3e Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 21:48:23 +0100 Subject: [PATCH 07/41] Updated generate_components to respect new can_move_to rule, making fuzz test pass again --- src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 2f151b8..840f769 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -586,7 +586,9 @@ impl PathingGrid { ] } .into_iter() - .filter(|p| self.grid.point_in_bounds(*p) && !self.grid.get_point(*p)) + .filter(|p| self.can_move_to(*p,point)) + .collect::>() + .iter() .for_each(|p| { let ix = self.grid.get_ix_point(&p); self.components.union(parent_ix, ix); From c9f286f8ef0ab921000b27aebcfc748a7218244b Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 22:56:48 +0100 Subject: [PATCH 08/41] Renamed PathingGrid to Pathfinder --- benches/comparison_bench.rs | 6 ++--- benches/single_bench.rs | 6 ++--- examples/benchmark_runner.rs | 8 +++--- examples/heuristic_factor.rs | 4 +-- examples/multiple_goals.rs | 4 +-- examples/paths_and_waypoints.rs | 4 +-- examples/simple_4.rs | 4 +-- examples/simple_8.rs | 4 +-- src/astar_jps.rs | 1 + src/lib.rs | 43 +++++++++++++++++---------------- tests/benchmark_distances.rs | 3 +-- tests/fuzz_test.rs | 8 +++--- 12 files changed, 48 insertions(+), 47 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 1d628d0..970fefa 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use grid_pathfinding::PathingGrid; +use grid_pathfinding::Pathfinder; use grid_pathfinding_benchmark::*; use grid_util::grid::ValueGrid; use std::hint::black_box; @@ -13,8 +13,8 @@ fn dao_bench(c: &mut Criterion) { }; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); + let mut pathing_grid: Pathfinder = + Pathfinder::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; diff --git a/benches/single_bench.rs b/benches/single_bench.rs index afe3e0c..8bed392 100644 --- a/benches/single_bench.rs +++ b/benches/single_bench.rs @@ -1,5 +1,5 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use grid_pathfinding::PathingGrid; +use grid_pathfinding::Pathfinder; use grid_pathfinding_benchmark::*; use grid_util::grid::ValueGrid; use std::hint::black_box; @@ -9,8 +9,8 @@ fn dao_bench_single(c: &mut Criterion) { let bench_set = ["dao/arena"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); + let mut pathing_grid: Pathfinder = + Pathfinder::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; diff --git a/examples/benchmark_runner.rs b/examples/benchmark_runner.rs index 33d72cc..747c87d 100644 --- a/examples/benchmark_runner.rs +++ b/examples/benchmark_runner.rs @@ -1,4 +1,4 @@ -use grid_pathfinding::PathingGrid; +use grid_pathfinding::Pathfinder; use grid_pathfinding_benchmark::*; use grid_util::grid::ValueGrid; use grid_util::point::Point; @@ -13,8 +13,8 @@ fn main() { let (bool_grid, scenarios) = get_benchmark(name); // for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { for (allow_diag, pruning) in [(true, false)] { - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); + let mut pathing_grid: Pathfinder = + Pathfinder::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; @@ -35,7 +35,7 @@ fn main() { println!("\tTotal benchmark time: {:.2?}", total_time); } -pub fn run_scenarios(pathing_grid: &PathingGrid, scenarios: &Vec<(Point, Point, f64)>) { +pub fn run_scenarios(pathing_grid: &Pathfinder, scenarios: &Vec<(Point, Point, f64)>) { for (start, goal, _) in scenarios { let path: Option> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); assert!(path.is_some()); diff --git a/examples/heuristic_factor.rs b/examples/heuristic_factor.rs index a5f918f..825646d 100644 --- a/examples/heuristic_factor.rs +++ b/examples/heuristic_factor.rs @@ -1,4 +1,4 @@ -use grid_pathfinding::PathingGrid; +use grid_pathfinding::Pathfinder; use grid_util::grid::ValueGrid; use grid_util::point::Point; use grid_util::Rect; @@ -8,7 +8,7 @@ use grid_util::Rect; fn main() { const N: i32 = 30; - let mut pathing_grid: PathingGrid = PathingGrid::new(N as usize, N as usize, true); + let mut pathing_grid: Pathfinder = Pathfinder::new(N as usize, N as usize, true); pathing_grid.heuristic_factor = 1.3; pathing_grid.set_rect(Rect::new(1, 1, N - 2, N - 2), false); pathing_grid.set_rect(Rect::new(8, 8, 8, 8), true); diff --git a/examples/multiple_goals.rs b/examples/multiple_goals.rs index a467c14..524a29f 100644 --- a/examples/multiple_goals.rs +++ b/examples/multiple_goals.rs @@ -1,4 +1,4 @@ -use grid_pathfinding::PathingGrid; +use grid_pathfinding::Pathfinder; use grid_util::grid::ValueGrid; use grid_util::point::Point; @@ -15,7 +15,7 @@ use grid_util::point::Point; // The found path moves to the closest goal, which is the top one. fn main() { - let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); println!("{}", pathing_grid); diff --git a/examples/paths_and_waypoints.rs b/examples/paths_and_waypoints.rs index 21f5c72..b6df4ba 100644 --- a/examples/paths_and_waypoints.rs +++ b/examples/paths_and_waypoints.rs @@ -1,4 +1,4 @@ -use grid_pathfinding::{waypoints_to_path, PathingGrid}; +use grid_pathfinding::{waypoints_to_path, Pathfinder}; use grid_util::grid::ValueGrid; use grid_util::point::Point; @@ -20,7 +20,7 @@ use grid_util::point::Point; // path, as a shorthand for the two previous calls. fn main() { - let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(5, 5, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); println!("{}", pathing_grid); diff --git a/examples/simple_4.rs b/examples/simple_4.rs index e3da7c2..2882b8f 100644 --- a/examples/simple_4.rs +++ b/examples/simple_4.rs @@ -1,4 +1,4 @@ -use grid_pathfinding::PathingGrid; +use grid_pathfinding::Pathfinder; use grid_util::grid::ValueGrid; use grid_util::point::Point; @@ -16,7 +16,7 @@ use grid_util::point::Point; // Nodes have a 4-neighborhood fn main() { - let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.allow_diagonal_move = false; pathing_grid.set(1, 1, true); pathing_grid.generate_components(); diff --git a/examples/simple_8.rs b/examples/simple_8.rs index 0654deb..7ec57db 100644 --- a/examples/simple_8.rs +++ b/examples/simple_8.rs @@ -1,4 +1,4 @@ -use grid_pathfinding::PathingGrid; +use grid_pathfinding::Pathfinder; use grid_util::grid::ValueGrid; use grid_util::point::Point; @@ -16,7 +16,7 @@ use grid_util::point::Point; // Nodes have an 8-neighborhood fn main() { - let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); println!("{}", pathing_grid); diff --git a/src/astar_jps.rs b/src/astar_jps.rs index f2089c9..fd47d80 100644 --- a/src/astar_jps.rs +++ b/src/astar_jps.rs @@ -86,6 +86,7 @@ where parents: FxIndexMap::default(), } } + pub fn astar_jps( &mut self, start: &N, diff --git a/src/lib.rs b/src/lib.rs index 840f769..782ff6d 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ //! pathfinding. Note that this assumes a uniform-cost grid. Pre-computes //! [connected components](https://en.wikipedia.org/wiki/Component_(graph_theory)) //! to avoid flood-filling behaviour if no path exists. +pub mod astar; mod astar_jps; use astar_jps::AstarContext; use core::fmt; @@ -68,7 +69,7 @@ pub fn waypoints_to_path(waypoints: Vec) -> Vec { /// empty ([false]). It also records neighbours in [u8] format for fast lookups during search. /// Implements [Grid] by building on [BoolGrid]. #[derive(Clone, Debug)] -pub struct PathingGrid { +pub struct Pathfinder { pub grid: BoolGrid, pub neighbours: SimpleValueGrid, pub jump_point: SimpleValueGrid, @@ -80,9 +81,9 @@ pub struct PathingGrid { context: Arc>>, } -impl Default for PathingGrid { - fn default() -> PathingGrid { - let mut grid = PathingGrid { +impl Default for Pathfinder { + fn default() -> Pathfinder { + let mut grid = Pathfinder { grid: BoolGrid::default(), neighbours: SimpleValueGrid::default(), jump_point: SimpleValueGrid::default(), @@ -97,7 +98,7 @@ impl Default for PathingGrid { grid } } -impl PathingGrid { +impl Pathfinder { fn neighborhood_points(&self, point: &Point) -> SmallVec<[Point; 8]> { if self.allow_diagonal_move { point.moore_neighborhood_smallvec() @@ -586,7 +587,7 @@ impl PathingGrid { ] } .into_iter() - .filter(|p| self.can_move_to(*p,point)) + .filter(|p| self.can_move_to(*p, point)) .collect::>() .iter() .for_each(|p| { @@ -598,7 +599,7 @@ impl PathingGrid { } } } -impl fmt::Display for PathingGrid { +impl fmt::Display for Pathfinder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Grid:")?; for y in 0..self.grid.height as i32 { @@ -618,9 +619,9 @@ impl fmt::Display for PathingGrid { } } -impl ValueGrid for PathingGrid { +impl ValueGrid for Pathfinder { fn new(width: usize, height: usize, default_value: bool) -> Self { - let mut base_grid = PathingGrid { + let mut base_grid = Pathfinder { grid: BoolGrid::new(width, height, default_value), jump_point: SimpleValueGrid::new(width, height, 0b00000000), neighbours: SimpleValueGrid::new(width, height, 0b11111111), @@ -677,7 +678,7 @@ mod tests { // | # | // | # | // ___ - let mut path_graph = PathingGrid::new(3, 2, false); + let mut path_graph = Pathfinder::new(3, 2, false); path_graph.grid.set(1, 0, true); path_graph.grid.set(1, 1, true); let f_ix = |p| path_graph.get_ix_point(p); @@ -697,7 +698,7 @@ mod tests { #[test] fn reachable_with_diagonals() { - let mut path_graph = PathingGrid::new(3, 2, false); + let mut path_graph = Pathfinder::new(3, 2, false); path_graph.grid.set(1, 0, true); path_graph.grid.set(1, 1, true); let p1 = Point::new(0, 0); @@ -719,7 +720,7 @@ mod tests { // | # | // | G| // ___ - let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.improved_pruning = false; pathing_grid.allow_diagonal_move = false; pathing_grid.set(1, 1, true); @@ -733,7 +734,7 @@ mod tests { #[test] fn equal_start_goal() { for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { - let mut pathing_grid: PathingGrid = PathingGrid::new(1, 1, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(1, 1, false); pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; pathing_grid.generate_components(); @@ -751,7 +752,7 @@ mod tests { for (allow_diag, pruning, expected) in [(false, false, 5), (true, false, 4), (true, true, 4)] { - let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; pathing_grid.set(1, 1, true); @@ -770,7 +771,7 @@ mod tests { for (allow_diag, pruning, expected) in [(false, false, 7), (true, false, 5), (true, true, 5)] { - let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(5, 5, false); pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; pathing_grid.set(1, 1, true); @@ -790,7 +791,7 @@ mod tests { for (allow_diag, pruning, expected) in [(false, false, 15), (true, false, 10), (true, true, 10)] { - let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(10, 10, false); pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); @@ -812,7 +813,7 @@ mod tests { for (allow_diag, pruning, expected) in [(false, false, 11), (true, false, 7), (true, true, 5)] { - let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(10, 10, false); pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); @@ -837,9 +838,9 @@ mod tests { // | #| // |# | // __ - let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); + let mut pathing_grid: Pathfinder = Pathfinder::new(2, 2, true); pathing_grid.allow_diagonal_move = false; - let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); + let mut pathing_grid_diag: Pathfinder = Pathfinder::new(2, 2, true); for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { pathing_grid.set(0, 0, false); pathing_grid.set(1, 1, false); @@ -858,9 +859,9 @@ mod tests { // | #| // |# | // __ - let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); + let mut pathing_grid: Pathfinder = Pathfinder::new(2, 2, true); pathing_grid.allow_diagonal_move = false; - let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); + let mut pathing_grid_diag: Pathfinder = Pathfinder::new(2, 2, true); for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { pathing_grid.set(0, 0, false); pathing_grid.set(1, 1, false); diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index ad090a7..d8ea069 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -18,8 +18,7 @@ fn verify_solution_distance() { let bench_set = ["dao/arena", "dao/lak107d", "dao/den101d"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); + let mut pathing_grid: Pathfinder = Pathfinder::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = true; pathing_grid.improved_pruning = false; diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index af997db..929025d 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -4,8 +4,8 @@ use grid_pathfinding::*; use grid_util::*; use rand::prelude::*; -fn random_grid(n: usize, rng: &mut StdRng, diagonal: bool, improved_pruning: bool) -> PathingGrid { - let mut pathing_grid: PathingGrid = PathingGrid::new(n, n, false); +fn random_grid(n: usize, rng: &mut StdRng, diagonal: bool, improved_pruning: bool) -> Pathfinder { + let mut pathing_grid: Pathfinder = Pathfinder::new(n, n, false); pathing_grid.allow_diagonal_move = diagonal; pathing_grid.improved_pruning = improved_pruning; for x in 0..pathing_grid.width() as i32 { @@ -17,7 +17,7 @@ fn random_grid(n: usize, rng: &mut StdRng, diagonal: bool, improved_pruning: boo pathing_grid } -fn visualize_grid(grid: &PathingGrid, start: &Point, end: &Point) { +fn visualize_grid(grid: &Pathfinder, start: &Point, end: &Point) { let grid = &grid.grid; for y in (0..grid.height as i32).rev() { for x in 0..grid.width as i32 { @@ -42,7 +42,7 @@ fn fuzz() { const N_GRIDS: usize = 10000; let mut rng = StdRng::seed_from_u64(0); for (diagonal, improved_pruning) in [(false, false), (true, false), (true, true)] { - let mut random_grids: Vec = Vec::new(); + let mut random_grids: Vec = Vec::new(); for _ in 0..N_GRIDS { random_grids.push(random_grid(N, &mut rng, diagonal, improved_pruning)) } From f4cc66b50260f52230c38eb4768ec47564d7bbf9 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 23:01:27 +0100 Subject: [PATCH 09/41] Created new PathingGrid with just core functionalities like maintaining connected components and neighbour maps --- src/lib.rs | 3 +- src/pathing_grid.rs | 286 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 src/pathing_grid.rs diff --git a/src/lib.rs b/src/lib.rs index 782ff6d..1f5c966 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,8 +7,9 @@ //! pathfinding. Note that this assumes a uniform-cost grid. Pre-computes //! [connected components](https://en.wikipedia.org/wiki/Component_(graph_theory)) //! to avoid flood-filling behaviour if no path exists. -pub mod astar; mod astar_jps; +pub mod pathing_grid; +pub mod solver; use astar_jps::AstarContext; use core::fmt; use grid_util::direction::Direction; diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs new file mode 100644 index 0000000..256123a --- /dev/null +++ b/src/pathing_grid.rs @@ -0,0 +1,286 @@ +use super::*; +use core::fmt; +use grid_util::grid::{BoolGrid, SimpleValueGrid, ValueGrid}; +use grid_util::point::Point; +use petgraph::unionfind::UnionFind; +use smallvec::SmallVec; +use std::sync::{Arc, Mutex}; + +#[derive(Clone, Debug)] +pub struct PathingGrid { + pub grid: BoolGrid, + pub neighbours: SimpleValueGrid, + pub components: UnionFind, + pub components_dirty: bool, + pub allow_diagonal_move: bool, + pub(crate) context: Arc>>, +} + +impl Default for PathingGrid { + fn default() -> PathingGrid { + let mut grid = PathingGrid { + grid: BoolGrid::default(), + neighbours: SimpleValueGrid::default(), + components: UnionFind::new(0), + components_dirty: false, + allow_diagonal_move: true, + context: Arc::new(Mutex::new(AstarContext::new())), + }; + grid.initialize(); + grid + } +} +impl PathingGrid { + fn neighborhood_points(&self, point: &Point) -> SmallVec<[Point; 8]> { + if self.allow_diagonal_move { + point.moore_neighborhood_smallvec() + } else { + point.neumann_neighborhood_smallvec() + } + } + pub fn neighborhood_points_and_cost( + &self, + pos: &Point, + ) -> SmallVec<[(Point, i32); N_SMALLVEC_SIZE]> { + self.neighborhood_points(pos) + .into_iter() + .filter(|p| self.can_move_to(*p, *pos)) + // See comment in pruned_neighborhood about cost calculation + .map(move |p| (p, (pos.dir_obj(&p).num() % 2) * (D - C) + C)) + .collect::>() + } + fn can_move_to(&self, pos: Point, start: Point) -> bool { + if ALLOW_CORNER_CUTTING { + self.can_move_to_simple(pos) + } else { + debug_assert!((start.x - pos.x).abs() <= 1 && (start.y - pos.y).abs() <= 1); + self.can_move_to_simple(pos) + && (!self.grid.get_point(Point::new(start.x, pos.y)) + && !self.grid.get_point(Point::new(pos.x, start.y))) + } + } + fn can_move_to_simple(&self, pos: Point) -> bool { + self.point_in_bounds(pos) && !self.grid.get_point(pos) + } + fn in_bounds(&self, x: i32, y: i32) -> bool { + self.grid.index_in_bounds(x, y) + } + /// The neighbour indexing used here corresponds to that used in [grid_util::Direction]. + fn indexed_neighbor(&self, node: &Point, index: i32) -> bool { + (self.neighbours.get_point(*node) & 1 << (index.rem_euclid(8))) != 0 + } + + fn forced_mask(&self, node: &Point) -> u8 { + let mut forced_mask: u8 = 0; + for dir_num in 0..8 { + if dir_num % 2 == 1 { + if ALLOW_CORNER_CUTTING && !self.indexed_neighbor(node, 3 + dir_num) + || !self.indexed_neighbor(node, 5 + dir_num) + { + forced_mask |= 1 << dir_num; + } + } else { + if !self.indexed_neighbor(node, 2 + dir_num) + || !self.indexed_neighbor(node, 6 + dir_num) + { + forced_mask |= 1 << dir_num; + } + }; + } + forced_mask + } + + /// Updates the neighbours grid after changing the grid. + fn update_neighbours(&mut self, x: i32, y: i32, blocked: bool) { + let p = Point::new(x, y); + for i in 0..8 { + let neighbor = p.moore_neighbor(i); + if self.in_bounds(neighbor.x, neighbor.y) { + let ix = (i + 4) % 8; + let mut n_mask = self.neighbours.get_point(neighbor); + if blocked { + n_mask &= !(1 << ix); + } else { + n_mask |= 1 << ix; + } + self.neighbours.set_point(neighbor, n_mask); + } + } + } + /// Retrieves the component id a given [Point] belongs to. + pub fn get_component(&self, point: &Point) -> usize { + self.components.find(self.get_ix_point(point)) + } + /// Checks if start and goal are on the same component. + pub fn reachable(&self, start: &Point, goal: &Point) -> bool { + !self.unreachable(start, goal) + } + + /// Checks if start and goal are not on the same component. + pub fn unreachable(&self, start: &Point, goal: &Point) -> bool { + if self.in_bounds(start.x, start.y) && self.in_bounds(goal.x, goal.y) { + let start_ix = self.get_ix_point(start); + let goal_ix = self.get_ix_point(goal); + !self.components.equiv(start_ix, goal_ix) + } else { + true + } + } + + /// Checks if any neighbour of the goal is on the same component as the start. + pub fn neighbours_reachable(&self, start: &Point, goal: &Point) -> bool { + if self.in_bounds(start.x, start.y) && self.in_bounds(goal.x, goal.y) { + let start_ix = self.get_ix_point(start); + let neighborhood = self.neighborhood_points(goal); + neighborhood.iter().any(|p| { + self.in_bounds(p.x, p.y) && self.components.equiv(start_ix, self.get_ix_point(p)) + }) + } else { + true + } + } + + /// Checks if every neighbour of the goal is on a different component as the start. + pub fn neighbours_unreachable(&self, start: &Point, goal: &Point) -> bool { + if self.in_bounds(start.x, start.y) && self.in_bounds(goal.x, goal.y) { + let start_ix = self.get_ix_point(start); + let neighborhood = self.neighborhood_points(goal); + neighborhood.iter().all(|p| { + !self.in_bounds(p.x, p.y) || !self.components.equiv(start_ix, self.get_ix_point(p)) + }) + } else { + true + } + } + + /// Regenerates the components if they are marked as dirty. + pub fn update(&mut self) { + if self.components_dirty { + // The components are dirty, regenerate them + self.generate_components(); + } + } + + pub fn update_all_neighbours(&mut self) { + for x in 0..self.width() as i32 { + for y in 0..self.height() as i32 { + self.update_neighbours(x, y, self.get(x, y)); + } + } + } + + pub fn initialize(&mut self) { + // Emulates 'placing' of blocked tile around map border to correctly initialize neighbours + // and make behaviour of a map bordered by tiles the same as a borderless map. + for i in -1..=(self.width() as i32) { + self.update_neighbours(i, -1, true); + self.update_neighbours(i, self.height() as i32, true); + } + for j in -1..=(self.height() as i32) { + self.update_neighbours(-1, j, true); + self.update_neighbours(self.width() as i32, j, true); + } + self.update_all_neighbours(); + } + + /// Generates a new [UnionFind] structure and links up grid neighbours to the same components. + pub fn generate_components(&mut self) { + let w = self.grid.width; + let h = self.grid.height; + self.components = UnionFind::new(w * h); + self.components_dirty = false; + for x in 0..w as i32 { + for y in 0..h as i32 { + if !self.grid.get(x, y) { + let point = Point::new(x, y); + let parent_ix = self.grid.get_ix_point(&point); + + if self.allow_diagonal_move { + vec![ + Point::new(point.x, point.y + 1), + Point::new(point.x, point.y - 1), + Point::new(point.x + 1, point.y), + Point::new(point.x + 1, point.y - 1), + Point::new(point.x + 1, point.y), + Point::new(point.x + 1, point.y + 1), + ] + } else { + vec![ + Point::new(point.x, point.y + 1), + Point::new(point.x, point.y - 1), + Point::new(point.x + 1, point.y), + ] + } + .into_iter() + .filter(|p| self.can_move_to(*p, point)) + .collect::>() + .iter() + .for_each(|p| { + let ix = self.grid.get_ix_point(&p); + self.components.union(parent_ix, ix); + }); + } + } + } + } +} +impl fmt::Display for PathingGrid { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Grid:")?; + for y in 0..self.grid.height as i32 { + let values = (0..self.grid.width as i32) + .map(|x| self.grid.get(x, y) as i32) + .collect::>(); + writeln!(f, "{:?}", values)?; + } + writeln!(f, "\nNeighbours:")?; + for y in 0..self.neighbours.height as i32 { + let values = (0..self.neighbours.width as i32) + .map(|x| self.neighbours.get(x, y) as i32) + .collect::>(); + writeln!(f, "{:?}", values)?; + } + Ok(()) + } +} + +impl ValueGrid for PathingGrid { + fn new(width: usize, height: usize, default_value: bool) -> Self { + let mut base_grid = PathingGrid { + grid: BoolGrid::new(width, height, default_value), + neighbours: SimpleValueGrid::new(width, height, 0b11111111), + components: UnionFind::new(width * height), + components_dirty: false, + allow_diagonal_move: true, + context: Arc::new(Mutex::new(AstarContext::new())), + }; + base_grid.initialize(); + base_grid + } + fn get(&self, x: i32, y: i32) -> bool { + self.grid.get(x, y) + } + /// Updates a position on the grid. Joins newly connected components and flags the components + /// as dirty if components are (potentially) broken apart into multiple. + fn set(&mut self, x: i32, y: i32, blocked: bool) { + let p = Point::new(x, y); + if self.grid.get(x, y) != blocked && blocked { + self.components_dirty = true; + } else { + let p_ix = self.grid.compute_ix(x, y); + for n in self.neighborhood_points(&p) { + if self.can_move_to(n, p) { + self.components.union(p_ix, self.grid.get_ix_point(&n)); + } + } + } + self.update_neighbours(p.x, p.y, blocked); + self.grid.set(x, y, blocked); + } + fn width(&self) -> usize { + self.grid.width() + } + fn height(&self) -> usize { + self.grid.height() + } +} From fb02531161968972a237d441405b90aaa966f355 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 23:02:21 +0100 Subject: [PATCH 10/41] Introduced new GridSolver trait with basic AstarSolver implementor --- src/solver.rs | 145 +++++++++++++++++++++++++++++++++++ tests/benchmark_distances.rs | 42 +++++++++- 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/solver.rs diff --git a/src/solver.rs b/src/solver.rs new file mode 100644 index 0000000..4d773fc --- /dev/null +++ b/src/solver.rs @@ -0,0 +1,145 @@ +use super::pathing_grid::PathingGrid; +use super::*; +use core::fmt; +use grid_util::grid::{BoolGrid, SimpleValueGrid, ValueGrid}; +use grid_util::point::Point; +use petgraph::unionfind::UnionFind; +use smallvec::SmallVec; +use std::sync::{Arc, Mutex}; + +pub trait GridSolver { + /// Container type for successors; you can keep this fixed to SmallVec if you prefer. + type Successors: IntoIterator; + + fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; + fn successors( + &self, + grid: &PathingGrid, + _parent: Option<&Point>, + node: &Point, + ) -> Self::Successors; + + fn get_path_single_goal( + &self, + grid: &mut PathingGrid, + start: Point, + goal: Point, + approximate: bool, + ) -> Option> { + self.get_waypoints_single_goal(grid, start, goal, approximate) + .map(waypoints_to_path) + } + /// The raw waypoints (jump points) from which [get_path_single_goal](Self::get_path_single_goal) makes a path. + fn get_waypoints_single_goal( + &self, + grid: &mut PathingGrid, + start: Point, + goal: Point, + approximate: bool, + ) -> Option> { + if approximate { + // Check if start and one of the goal neighbours are on the same connected component. + if grid.neighbours_unreachable(&start, &goal) { + // No neigbhours of the goal are reachable from the start + return None; + } + // A neighbour of the goal can be reached, compute a path + let mut ct = grid.context.lock().unwrap(); + ct.astar_jps( + &start, + |parent, node| self.successors(grid, *parent, node), + |point| self.heuristic(grid, point, &goal), + |point| self.heuristic(grid, point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }, + ) + } else { + // Check if start and goal are on the same connected component. + if grid.unreachable(&start, &goal) { + return None; + } + // The goal is reachable from the start, compute a path + let mut ct = grid.context.lock().unwrap(); + ct.astar_jps( + &start, + |parent, node| self.successors(grid, *parent, node), + |point| self.heuristic(grid, point, &goal), + |point| *point == goal, + ) + } + .map(|(v, _c)| v) + } + /// Computes a path from the start to one of the given goals and returns the selected goal in addition to the found path. Otherwise behaves similar to [get_path_single_goal](Self::get_path_single_goal). + fn get_path_multiple_goals( + &self, + grid: &mut PathingGrid, + start: Point, + goals: Vec<&Point>, + ) -> Option<(Point, Vec)> { + self.get_waypoints_multiple_goals(grid, start, goals) + .map(|(x, y)| (x, waypoints_to_path(y))) + } + /// The raw waypoints (jump points) from which [get_path_multiple_goals](Self::get_path_multiple_goals) makes a path. + fn get_waypoints_multiple_goals( + &self, + grid: &mut PathingGrid, + start: Point, + goals: Vec<&Point>, + ) -> Option<(Point, Vec)> { + if goals.is_empty() { + return None; + } + let mut ct = grid.context.lock().unwrap(); + let result = ct.astar_jps( + &start, + |parent, node| self.successors(grid, *parent, node), + |point| { + goals + .iter() + .map(|x| self.heuristic(grid, point, x)) + .min() + .unwrap() + }, + |point| goals.contains(&point), + ); + result.map(|(v, _c)| (*v.last().unwrap(), v)) + } +} + +#[derive(Clone, Debug)] +pub struct AstarSolver { + pub heuristic_factor: f32, +} + +impl AstarSolver { + pub fn new() -> AstarSolver { + AstarSolver { + heuristic_factor: 1.0, + } + } +} + +impl GridSolver for AstarSolver { + type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; + + fn successors( + &self, + grid: &PathingGrid, + _parent: Option<&Point>, + node: &Point, + ) -> Self::Successors { + grid.neighborhood_points_and_cost(node) + } + /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. + fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + ((if grid.allow_diagonal_move { + let delta_x = (p1.x - p2.x).abs(); + let delta_y = (p1.y - p2.y).abs(); + // Formula from https://github.com/riscy/a_star_on_grids + // to efficiently compute the cost of a path taking the maximal amount + // of diagonal steps before going straight + (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 + } else { + p1.manhattan_distance(p2) * C + }) as f32 + * self.heuristic_factor) as i32 + } +} diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index d8ea069..488faf8 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -1,3 +1,5 @@ +use grid_pathfinding::pathing_grid::PathingGrid; +use grid_pathfinding::solver::{AstarSolver, GridSolver}; use grid_pathfinding::*; use grid_pathfinding_benchmark::get_benchmark; use grid_util::*; @@ -41,7 +43,45 @@ fn verify_solution_distance() { save_path(path, "path.csv").unwrap(); let float_cost = convert_cost_to_unit_cost_float(total_cost_int); println!("My distance: {float_cost}"); - if *distance >=0.01{ + if *distance >= 0.01 { + let delta_dist = (float_cost - distance).abs() / distance; + assert!(delta_dist < 0.05); + } + } + } +} + +#[test] +fn verify_solution_distance_astar() { + let bench_set = ["dao/arena", "dao/lak107d", "dao/den101d"]; + let solver = AstarSolver::new(); + + for name in bench_set { + let (bool_grid, scenarios) = get_benchmark(name.to_owned()); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.allow_diagonal_move = true; + pathing_grid.initialize(); + pathing_grid.generate_components(); + for (start, end, distance) in &scenarios { + println!("Start: {start}; End: {end}; Distance: {distance}"); + let path = solver + .get_path_single_goal(&mut pathing_grid, *start, *end, false) + .unwrap(); + let mut v = path[0]; + let n = path.len(); + let mut total_cost_int = 0; + for i in 1..n { + let v_old = v; + v = path[i]; + let cost = solver.heuristic(&pathing_grid, &v_old, &v); + total_cost_int += cost; + } + save_path(path, "path.csv").unwrap(); + let float_cost = convert_cost_to_unit_cost_float(total_cost_int); + println!("My distance: {float_cost}"); + if *distance >= 0.01 { let delta_dist = (float_cost - distance).abs() / distance; assert!(delta_dist < 0.05); } From dd0cda5b44bc9a133297a50ba36303f7d7d51136 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 23:25:23 +0100 Subject: [PATCH 11/41] Moved AstarSolver to new solver::astar submodule --- src/solver/astar.rs | 44 +++++++++++++++++++++++++++ src/{solver.rs => solver/mod.rs} | 52 +++----------------------------- tests/benchmark_distances.rs | 2 +- 3 files changed, 49 insertions(+), 49 deletions(-) create mode 100644 src/solver/astar.rs rename src/{solver.rs => solver/mod.rs} (71%) diff --git a/src/solver/astar.rs b/src/solver/astar.rs new file mode 100644 index 0000000..8491459 --- /dev/null +++ b/src/solver/astar.rs @@ -0,0 +1,44 @@ +use grid_util::Point; +use smallvec::SmallVec; + +use crate::{pathing_grid::PathingGrid, solver::GridSolver, C, D, E, N_SMALLVEC_SIZE}; + +#[derive(Clone, Debug)] +pub struct AstarSolver { + pub heuristic_factor: f32, +} + +impl AstarSolver { + pub fn new() -> AstarSolver { + AstarSolver { + heuristic_factor: 1.0, + } + } +} + +impl GridSolver for AstarSolver { + type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; + + fn successors( + &self, + grid: &PathingGrid, + _parent: Option<&Point>, + node: &Point, + ) -> Self::Successors { + grid.neighborhood_points_and_cost(node) + } + /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. + fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + ((if grid.allow_diagonal_move { + let delta_x = (p1.x - p2.x).abs(); + let delta_y = (p1.y - p2.y).abs(); + // Formula from https://github.com/riscy/a_star_on_grids + // to efficiently compute the cost of a path taking the maximal amount + // of diagonal steps before going straight + (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 + } else { + p1.manhattan_distance(p2) * C + }) as f32 + * self.heuristic_factor) as i32 + } +} diff --git a/src/solver.rs b/src/solver/mod.rs similarity index 71% rename from src/solver.rs rename to src/solver/mod.rs index 4d773fc..7d7c1a3 100644 --- a/src/solver.rs +++ b/src/solver/mod.rs @@ -1,12 +1,8 @@ -use super::pathing_grid::PathingGrid; -use super::*; -use core::fmt; -use grid_util::grid::{BoolGrid, SimpleValueGrid, ValueGrid}; -use grid_util::point::Point; -use petgraph::unionfind::UnionFind; -use smallvec::SmallVec; -use std::sync::{Arc, Mutex}; +use crate::{pathing_grid::PathingGrid, waypoints_to_path, EQUAL_EDGE_COST}; +use grid_util::Point; +pub mod astar; +pub mod jps; pub trait GridSolver { /// Container type for successors; you can keep this fixed to SmallVec if you prefer. type Successors: IntoIterator; @@ -103,43 +99,3 @@ pub trait GridSolver { result.map(|(v, _c)| (*v.last().unwrap(), v)) } } - -#[derive(Clone, Debug)] -pub struct AstarSolver { - pub heuristic_factor: f32, -} - -impl AstarSolver { - pub fn new() -> AstarSolver { - AstarSolver { - heuristic_factor: 1.0, - } - } -} - -impl GridSolver for AstarSolver { - type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; - - fn successors( - &self, - grid: &PathingGrid, - _parent: Option<&Point>, - node: &Point, - ) -> Self::Successors { - grid.neighborhood_points_and_cost(node) - } - /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. - fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { - ((if grid.allow_diagonal_move { - let delta_x = (p1.x - p2.x).abs(); - let delta_y = (p1.y - p2.y).abs(); - // Formula from https://github.com/riscy/a_star_on_grids - // to efficiently compute the cost of a path taking the maximal amount - // of diagonal steps before going straight - (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 - } else { - p1.manhattan_distance(p2) * C - }) as f32 - * self.heuristic_factor) as i32 - } -} diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index 488faf8..838372e 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -1,5 +1,5 @@ use grid_pathfinding::pathing_grid::PathingGrid; -use grid_pathfinding::solver::{AstarSolver, GridSolver}; +use grid_pathfinding::solver::{astar::AstarSolver, GridSolver}; use grid_pathfinding::*; use grid_pathfinding_benchmark::get_benchmark; use grid_util::*; From c8182512f20386c0fb4b5ba1bf7ae61a94954746 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 23:25:44 +0100 Subject: [PATCH 12/41] Made PathingGrid::neighborhood_points and PathingGrid::indexed_neighbor public --- src/pathing_grid.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs index 256123a..5c98fd0 100644 --- a/src/pathing_grid.rs +++ b/src/pathing_grid.rs @@ -31,7 +31,7 @@ impl Default for PathingGrid { } } impl PathingGrid { - fn neighborhood_points(&self, point: &Point) -> SmallVec<[Point; 8]> { + pub fn neighborhood_points(&self, point: &Point) -> SmallVec<[Point; 8]> { if self.allow_diagonal_move { point.moore_neighborhood_smallvec() } else { @@ -66,7 +66,7 @@ impl PathingGrid { self.grid.index_in_bounds(x, y) } /// The neighbour indexing used here corresponds to that used in [grid_util::Direction]. - fn indexed_neighbor(&self, node: &Point, index: i32) -> bool { + pub fn indexed_neighbor(&self, node: &Point, index: i32) -> bool { (self.neighbours.get_point(*node) & 1 << (index.rem_euclid(8))) != 0 } From c0a209f9597354ace16407c7890dec2e31cab5c5 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 23:56:29 +0100 Subject: [PATCH 13/41] Renamed AstarContext to SearchContext --- src/astar_jps.rs | 10 +++++----- src/lib.rs | 8 ++++---- src/pathing_grid.rs | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/astar_jps.rs b/src/astar_jps.rs index fd47d80..acd286a 100644 --- a/src/astar_jps.rs +++ b/src/astar_jps.rs @@ -70,18 +70,18 @@ where /// [AstarContext] represents the search fringe and node parent map, facilitating reuse of memory allocations. #[derive(Clone, Debug)] -pub struct AstarContext { +pub struct SearchContext { fringe: BinaryHeap>, parents: FxIndexMap, } -impl AstarContext +impl SearchContext where N: Eq + Hash + Clone, C: Zero + Ord + Copy, { - pub fn new() -> AstarContext { - AstarContext { + pub fn new() -> SearchContext { + SearchContext { fringe: BinaryHeap::new(), parents: FxIndexMap::default(), } @@ -174,6 +174,6 @@ where FH: FnMut(&N) -> C, FS: FnMut(&N) -> bool, { - let mut search = AstarContext::new(); + let mut search = SearchContext::new(); search.astar_jps(start, successors, heuristic, success) } diff --git a/src/lib.rs b/src/lib.rs index 1f5c966..94ecc70 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ mod astar_jps; pub mod pathing_grid; pub mod solver; -use astar_jps::AstarContext; +use astar_jps::SearchContext; use core::fmt; use grid_util::direction::Direction; use grid_util::grid::{BoolGrid, SimpleValueGrid, ValueGrid}; @@ -79,7 +79,7 @@ pub struct Pathfinder { pub heuristic_factor: f32, pub improved_pruning: bool, pub allow_diagonal_move: bool, - context: Arc>>, + context: Arc>>, } impl Default for Pathfinder { @@ -93,7 +93,7 @@ impl Default for Pathfinder { improved_pruning: true, heuristic_factor: 1.0, allow_diagonal_move: true, - context: Arc::new(Mutex::new(AstarContext::new())), + context: Arc::new(Mutex::new(SearchContext::new())), }; grid.initialize(); grid @@ -631,7 +631,7 @@ impl ValueGrid for Pathfinder { improved_pruning: true, heuristic_factor: 1.0, allow_diagonal_move: true, - context: Arc::new(Mutex::new(AstarContext::new())), + context: Arc::new(Mutex::new(SearchContext::new())), }; base_grid.initialize(); base_grid diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs index 5c98fd0..5460432 100644 --- a/src/pathing_grid.rs +++ b/src/pathing_grid.rs @@ -13,7 +13,7 @@ pub struct PathingGrid { pub components: UnionFind, pub components_dirty: bool, pub allow_diagonal_move: bool, - pub(crate) context: Arc>>, + pub(crate) context: Arc>>, } impl Default for PathingGrid { @@ -24,7 +24,7 @@ impl Default for PathingGrid { components: UnionFind::new(0), components_dirty: false, allow_diagonal_move: true, - context: Arc::new(Mutex::new(AstarContext::new())), + context: Arc::new(Mutex::new(SearchContext::new())), }; grid.initialize(); grid @@ -252,7 +252,7 @@ impl ValueGrid for PathingGrid { components: UnionFind::new(width * height), components_dirty: false, allow_diagonal_move: true, - context: Arc::new(Mutex::new(AstarContext::new())), + context: Arc::new(Mutex::new(SearchContext::new())), }; base_grid.initialize(); base_grid From 00c53afc20408a52ae9b68da1496b32118e16d23 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Fri, 14 Nov 2025 23:58:32 +0100 Subject: [PATCH 14/41] Changed GridSolver::successors to take generic goal closure, adapting AstarSolver to match it --- src/solver/astar.rs | 8 ++++++-- src/solver/mod.rs | 20 ++++++++++++++------ 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/solver/astar.rs b/src/solver/astar.rs index 8491459..638dff7 100644 --- a/src/solver/astar.rs +++ b/src/solver/astar.rs @@ -19,12 +19,16 @@ impl AstarSolver { impl GridSolver for AstarSolver { type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; - fn successors( + fn successors( &self, grid: &PathingGrid, _parent: Option<&Point>, node: &Point, - ) -> Self::Successors { + _goal: &F, + ) -> Self::Successors + where + F: Fn(&Point) -> bool, + { grid.neighborhood_points_and_cost(node) } /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 7d7c1a3..c8d33cc 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -8,12 +8,15 @@ pub trait GridSolver { type Successors: IntoIterator; fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; - fn successors( + fn successors( &self, grid: &PathingGrid, - _parent: Option<&Point>, + parent: Option<&Point>, node: &Point, - ) -> Self::Successors; + goal: &F, + ) -> Self::Successors + where + F: Fn(&Point) -> bool; fn get_path_single_goal( &self, @@ -43,7 +46,12 @@ pub trait GridSolver { let mut ct = grid.context.lock().unwrap(); ct.astar_jps( &start, - |parent, node| self.successors(grid, *parent, node), + |parent, node| { + self.successors(grid, *parent, node, &|node_pos| { + self.heuristic(grid, node_pos, &goal) + <= if EQUAL_EDGE_COST { 1 } else { 99 } + }) + }, |point| self.heuristic(grid, point, &goal), |point| self.heuristic(grid, point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }, ) @@ -56,7 +64,7 @@ pub trait GridSolver { let mut ct = grid.context.lock().unwrap(); ct.astar_jps( &start, - |parent, node| self.successors(grid, *parent, node), + |parent, node| self.successors(grid, *parent, node, &|node_pos| *node_pos == goal), |point| self.heuristic(grid, point, &goal), |point| *point == goal, ) @@ -86,7 +94,7 @@ pub trait GridSolver { let mut ct = grid.context.lock().unwrap(); let result = ct.astar_jps( &start, - |parent, node| self.successors(grid, *parent, node), + |parent, node| self.successors(grid, *parent, node, &|point| goals.contains(&point)), |point| { goals .iter() From 5b7aabbfd71c52f2f55f9df2abb6ed762ce0ebad Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 00:00:44 +0100 Subject: [PATCH 15/41] Removed forced_mask from PathingGrid and made can_move_to, can_move_to_simple public --- src/pathing_grid.rs | 24 ++---------------------- 1 file changed, 2 insertions(+), 22 deletions(-) diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs index 5460432..9fa53f8 100644 --- a/src/pathing_grid.rs +++ b/src/pathing_grid.rs @@ -49,7 +49,7 @@ impl PathingGrid { .map(move |p| (p, (pos.dir_obj(&p).num() % 2) * (D - C) + C)) .collect::>() } - fn can_move_to(&self, pos: Point, start: Point) -> bool { + pub fn can_move_to(&self, pos: Point, start: Point) -> bool { if ALLOW_CORNER_CUTTING { self.can_move_to_simple(pos) } else { @@ -59,7 +59,7 @@ impl PathingGrid { && !self.grid.get_point(Point::new(pos.x, start.y))) } } - fn can_move_to_simple(&self, pos: Point) -> bool { + pub fn can_move_to_simple(&self, pos: Point) -> bool { self.point_in_bounds(pos) && !self.grid.get_point(pos) } fn in_bounds(&self, x: i32, y: i32) -> bool { @@ -70,26 +70,6 @@ impl PathingGrid { (self.neighbours.get_point(*node) & 1 << (index.rem_euclid(8))) != 0 } - fn forced_mask(&self, node: &Point) -> u8 { - let mut forced_mask: u8 = 0; - for dir_num in 0..8 { - if dir_num % 2 == 1 { - if ALLOW_CORNER_CUTTING && !self.indexed_neighbor(node, 3 + dir_num) - || !self.indexed_neighbor(node, 5 + dir_num) - { - forced_mask |= 1 << dir_num; - } - } else { - if !self.indexed_neighbor(node, 2 + dir_num) - || !self.indexed_neighbor(node, 6 + dir_num) - { - forced_mask |= 1 << dir_num; - } - }; - } - forced_mask - } - /// Updates the neighbours grid after changing the grid. fn update_neighbours(&mut self, x: i32, y: i32, blocked: bool) { let p = Point::new(x, y); From 61c5525f051e154d86dcfcf6d6436a7592028471 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 00:03:30 +0100 Subject: [PATCH 16/41] Added new solver::jps module with new JPSSolver, implementing the GridSolver trait. Also updated distances benchmark to use this --- src/solver/jps.rs | 271 +++++++++++++++++++++++++++++++++++ src/solver/mod.rs | 1 - tests/benchmark_distances.rs | 18 ++- 3 files changed, 282 insertions(+), 8 deletions(-) create mode 100644 src/solver/jps.rs diff --git a/src/solver/jps.rs b/src/solver/jps.rs new file mode 100644 index 0000000..2ce3f99 --- /dev/null +++ b/src/solver/jps.rs @@ -0,0 +1,271 @@ +use grid_util::{Direction, Point, SimpleValueGrid, ValueGrid}; +use smallvec::SmallVec; + +use crate::{ + pathing_grid::PathingGrid, solver::GridSolver, ALLOW_CORNER_CUTTING, C, D, E, N_SMALLVEC_SIZE, +}; + +#[derive(Clone, Debug)] +pub struct JPSSolver { + pub jump_point: SimpleValueGrid, + pub heuristic_factor: f32, + pub improved_pruning: bool, +} + +impl GridSolver for JPSSolver { + type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; + + fn successors( + &self, + grid: &PathingGrid, + parent: Option<&Point>, + node: &Point, + goal: &F, + ) -> SmallVec<[(Point, i32); N_SMALLVEC_SIZE]> + where + F: Fn(&Point) -> bool, + { + match parent { + Some(parent_node) => { + let mut succ = SmallVec::new(); + let dir = parent_node.dir_obj(node); + let p: Vec<_> = self.pruned_neighborhood(dir, node, grid).collect(); + for (n, c) in p { + let dir = node.dir_obj(&n); + // Jumps the neighbor, skipping over unnecessary nodes. + if let Some((jumped_node, cost)) = self.jump(*node, c, dir, goal, grid) { + // If improved pruning is enabled, expand any diagonal unforced nodes + if self.improved_pruning + && dir.diagonal() + && !goal(&jumped_node) + && !self.is_forced(dir, &jumped_node) + { + // Recursively expand the unforced diagonal node + let jump_points = self.successors(grid, parent, &jumped_node, goal); + + // Extend the successors with the neighbours of the unforced node, correcting the + // cost to include the cost from parent_node to jumped_node + succ.extend(jump_points.into_iter().map(|(p, c)| (p, c + cost))); + } else { + succ.push((jumped_node, cost)); + } + } + } + succ + } + None => { + // For the starting node, just generate the full normal neighborhood without any pruning or jumping. + grid.neighborhood_points_and_cost(node) + } + } + } + + /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. + fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + if grid.allow_diagonal_move { + let delta_x = (p1.x - p2.x).abs(); + let delta_y = (p1.y - p2.y).abs(); + // Formula from https://github.com/riscy/a_star_on_grids + // to efficiently compute the cost of a path taking the maximal amount + // of diagonal steps before going straight + (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 + } else { + p1.manhattan_distance(p2) * C + } + } +} +impl JPSSolver { + pub fn new(grid: &PathingGrid) -> JPSSolver { + let mut solver = JPSSolver { + jump_point: SimpleValueGrid::new(grid.width(), grid.height(), 0), + improved_pruning: true, + heuristic_factor: 1.0, + }; + solver.initialize(grid); + solver + } + + fn is_forced(&self, dir: Direction, node: &Point) -> bool { + let dir_num = dir.num(); + self.jump_point.get_point(*node) & (1 << dir_num) != 0 + } + + fn forced_mask(&self, node: &Point, grid: &PathingGrid) -> u8 { + let mut forced_mask: u8 = 0; + for dir_num in 0..8 { + if dir_num % 2 == 1 { + if ALLOW_CORNER_CUTTING && !grid.indexed_neighbor(node, 3 + dir_num) + || !grid.indexed_neighbor(node, 5 + dir_num) + { + forced_mask |= 1 << dir_num; + } + } else { + if !grid.indexed_neighbor(node, 2 + dir_num) + || !grid.indexed_neighbor(node, 6 + dir_num) + { + forced_mask |= 1 << dir_num; + } + }; + } + forced_mask + } + + fn pruned_neighborhood<'a>( + &self, + dir: Direction, + node: &'a Point, + grid: &PathingGrid, + ) -> impl Iterator + 'a { + let dir_num = dir.num(); + let mut n_mask: u8; + let mut neighbours = grid.neighbours.get_point(*node); + if !grid.allow_diagonal_move { + neighbours &= 0b01010101; + n_mask = 0b01000101_u8.rotate_left(dir_num as u32); + } else if dir.diagonal() { + n_mask = 0b10000011_u8.rotate_left(dir_num as u32); + // if !self.indexed_neighbor(node, 3 + dir_num) { + // n_mask |= 1 << ((dir_num + 2) % 8); + // } + // if !self.indexed_neighbor(node, 5 + dir_num) { + // n_mask |= 1 << ((dir_num + 6) % 8); + // } + } else { + if ALLOW_CORNER_CUTTING { + n_mask = 0b00000001 << dir_num; + if !grid.indexed_neighbor(node, 2 + dir_num) { + n_mask |= 1 << ((dir_num + 1) % 8); + } + if !grid.indexed_neighbor(node, 6 + dir_num) { + n_mask |= 1 << ((dir_num + 7) % 8); + } + } else { + neighbours &= 0b01010101; + n_mask = 0b01000101_u8.rotate_left(dir_num as u32); + } + } + let comb_mask = neighbours & n_mask; + (0..8) + .step_by(if grid.allow_diagonal_move { 1 } else { 2 }) + .filter(move |x| comb_mask & (1 << *x) != 0) + // (dir_num % 2) * (D-C) + C) + // is an optimized version without a conditional of + // if dir.diagonal() {D} else {C} + .map(move |d| (node.moore_neighbor(d), (dir_num % 2) * (D - C) + C)) + } + + /// Straight jump in a cardinal direction. + fn jump_straight( + &self, + mut initial: Point, + mut cost: i32, + direction: Direction, + goal: &F, + grid: &PathingGrid, + ) -> Option<(Point, i32)> + where + F: Fn(&Point) -> bool, + { + debug_assert!(!direction.diagonal()); + loop { + initial = initial + direction; + if !grid.can_move_to_simple(initial) { + return None; + } + + if goal(&initial) || self.is_forced(direction, &initial) { + return Some((initial, cost)); + } + + // Straight jumps always take cardinal cost + cost += C; + } + } + + /// Performs the jumping of node neighbours, skipping over unnecessary nodes until a goal or a forced node is found. + fn jump( + &self, + mut initial: Point, + mut cost: i32, + direction: Direction, + goal: &F, + grid: &PathingGrid, + ) -> Option<(Point, i32)> + where + F: Fn(&Point) -> bool, + { + let mut new_initial: Point; + loop { + new_initial = initial + direction; + if !grid.can_move_to(new_initial, initial) { + return None; + } + initial = new_initial; + + if goal(&initial) || self.is_forced(direction, &initial) { + return Some((initial, cost)); + } + if direction.diagonal() + && (self + .jump_straight(initial, 1, direction.x_dir(), goal, grid) + .is_some() + || self + .jump_straight(initial, 1, direction.y_dir(), goal, grid) + .is_some()) + { + return Some((initial, cost)); + } + + // When using a 4-neighborhood (specified by setting allow_diagonal_move to false), + // jumps perpendicular to the direction are performed. This is necessary to not miss the + // goal when passing by. + if !grid.allow_diagonal_move || !ALLOW_CORNER_CUTTING && !direction.diagonal() { + if grid.allow_diagonal_move { + let diag_1 = direction.rotate_ccw(1); + let diag_2 = direction.rotate_cw(1); + if self.jump(initial, 1, diag_1, goal, grid).is_some() + || self.jump(initial, 1, diag_2, goal, grid).is_some() + { + return Some((initial, cost)); + } + } + let perp_1 = direction.rotate_ccw(2); + let perp_2 = direction.rotate_cw(2); + if self.jump_straight(initial, 1, perp_1, goal, grid).is_some() + || self.jump_straight(initial, 1, perp_2, goal, grid).is_some() + { + return Some((initial, cost)); + } + } + + // See comment in pruned_neighborhood about cost calculation + cost += (direction.num() % 2) * (D - C) + C; + } + } + + pub fn set_jumppoints(&mut self, point: Point, grid: &PathingGrid) { + let value = self.forced_mask(&point, grid); + self.jump_point.set_point(point, value); + } + pub fn fix_jumppoints(&mut self, point: Point, grid: &PathingGrid) { + self.set_jumppoints(point, grid); + for p in grid.neighborhood_points(&point) { + if grid.point_in_bounds(p) { + self.set_jumppoints(p, grid); + } + } + } + + /// Performs the full jump point precomputation + pub fn set_all_jumppoints(&mut self, grid: &PathingGrid) { + for x in 0..grid.width() { + for y in 0..grid.height() { + self.set_jumppoints(Point::new(x as i32, y as i32), grid); + } + } + } + + pub fn initialize(&mut self, grid: &PathingGrid) { + self.set_all_jumppoints(grid); + } +} diff --git a/src/solver/mod.rs b/src/solver/mod.rs index c8d33cc..1cffb0f 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -4,7 +4,6 @@ use grid_util::Point; pub mod astar; pub mod jps; pub trait GridSolver { - /// Container type for successors; you can keep this fixed to SmallVec if you prefer. type Successors: IntoIterator; fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index 838372e..606f40f 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -1,5 +1,5 @@ use grid_pathfinding::pathing_grid::PathingGrid; -use grid_pathfinding::solver::{astar::AstarSolver, GridSolver}; +use grid_pathfinding::solver::{astar::AstarSolver, jps::JPSSolver, GridSolver}; use grid_pathfinding::*; use grid_pathfinding_benchmark::get_benchmark; use grid_util::*; @@ -16,20 +16,24 @@ fn save_path(path: Vec, filename: &str) -> std::io::Result<()> { } #[test] -fn verify_solution_distance() { +fn verify_solution_distance_jps() { let bench_set = ["dao/arena", "dao/lak107d", "dao/den101d"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: Pathfinder = Pathfinder::new(bool_grid.width, bool_grid.height, true); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = true; - pathing_grid.improved_pruning = false; pathing_grid.initialize(); pathing_grid.generate_components(); + let mut solver = JPSSolver::new(&pathing_grid); + solver.set_all_jumppoints(&pathing_grid); + for (start, end, distance) in &scenarios { println!("Start: {start}; End: {end}; Distance: {distance}"); - let path = pathing_grid - .get_path_single_goal(*start, *end, false) + let path = solver + .get_path_single_goal(&mut pathing_grid, *start, *end, false) .unwrap(); let mut v = path[0]; let n = path.len(); @@ -37,7 +41,7 @@ fn verify_solution_distance() { for i in 1..n { let v_old = v; v = path[i]; - let cost = pathing_grid.heuristic(&v_old, &v); + let cost = solver.heuristic(&pathing_grid, &v_old, &v); total_cost_int += cost; } save_path(path, "path.csv").unwrap(); From 8b14f2e949e5e8a52e80bca923836b8dad38198f Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 09:42:06 +0100 Subject: [PATCH 17/41] Added cost and default get_path_cost implementation to GridSolver --- src/solver/astar.rs | 13 +++++++++---- src/solver/jps.rs | 11 +++++++---- src/solver/mod.rs | 16 ++++++++++++++++ tests/benchmark_distances.rs | 26 +++++--------------------- 4 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/solver/astar.rs b/src/solver/astar.rs index 638dff7..7f7c13f 100644 --- a/src/solver/astar.rs +++ b/src/solver/astar.rs @@ -31,9 +31,10 @@ impl GridSolver for AstarSolver { { grid.neighborhood_points_and_cost(node) } + /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. - fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { - ((if grid.allow_diagonal_move { + fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + if grid.allow_diagonal_move { let delta_x = (p1.x - p2.x).abs(); let delta_y = (p1.y - p2.y).abs(); // Formula from https://github.com/riscy/a_star_on_grids @@ -42,7 +43,11 @@ impl GridSolver for AstarSolver { (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 } else { p1.manhattan_distance(p2) * C - }) as f32 - * self.heuristic_factor) as i32 + } + } + + /// Just the cost times a heuristic factor. + fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + (self.cost(grid, p1, p2) as f32 * self.heuristic_factor) as i32 } } diff --git a/src/solver/jps.rs b/src/solver/jps.rs index 2ce3f99..2bbc2d1 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -8,7 +8,6 @@ use crate::{ #[derive(Clone, Debug)] pub struct JPSSolver { pub jump_point: SimpleValueGrid, - pub heuristic_factor: f32, pub improved_pruning: bool, } @@ -73,13 +72,17 @@ impl GridSolver for JPSSolver { p1.manhattan_distance(p2) * C } } + + /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. + fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + self.heuristic(grid, p1, p2) + } } impl JPSSolver { - pub fn new(grid: &PathingGrid) -> JPSSolver { + pub fn new(grid: &PathingGrid, improved_pruning: bool) -> JPSSolver { let mut solver = JPSSolver { jump_point: SimpleValueGrid::new(grid.width(), grid.height(), 0), - improved_pruning: true, - heuristic_factor: 1.0, + improved_pruning, }; solver.initialize(grid); solver diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 1cffb0f..4189b32 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -7,6 +7,9 @@ pub trait GridSolver { type Successors: IntoIterator; fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; + + fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; + fn successors( &self, grid: &PathingGrid, @@ -17,6 +20,19 @@ pub trait GridSolver { where F: Fn(&Point) -> bool; + fn get_path_cost(&self, path: Vec, pathing_grid: &PathingGrid) -> i32 { + let mut v = path[0]; + let n = path.len(); + let mut total_cost_int = 0; + for i in 1..n { + let v_old = v; + v = path[i]; + let cost = self.cost(&pathing_grid, &v_old, &v); + total_cost_int += cost; + } + total_cost_int + } + fn get_path_single_goal( &self, grid: &mut PathingGrid, diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index 606f40f..3ee4ecb 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -27,7 +27,7 @@ fn verify_solution_distance_jps() { pathing_grid.allow_diagonal_move = true; pathing_grid.initialize(); pathing_grid.generate_components(); - let mut solver = JPSSolver::new(&pathing_grid); + let mut solver = JPSSolver::new(&pathing_grid, false); solver.set_all_jumppoints(&pathing_grid); for (start, end, distance) in &scenarios { @@ -35,16 +35,8 @@ fn verify_solution_distance_jps() { let path = solver .get_path_single_goal(&mut pathing_grid, *start, *end, false) .unwrap(); - let mut v = path[0]; - let n = path.len(); - let mut total_cost_int = 0; - for i in 1..n { - let v_old = v; - v = path[i]; - let cost = solver.heuristic(&pathing_grid, &v_old, &v); - total_cost_int += cost; - } - save_path(path, "path.csv").unwrap(); + save_path(path.clone(), "path.csv").unwrap(); + let total_cost_int = solver.get_path_cost(path, &pathing_grid); let float_cost = convert_cost_to_unit_cost_float(total_cost_int); println!("My distance: {float_cost}"); if *distance >= 0.01 { @@ -73,16 +65,8 @@ fn verify_solution_distance_astar() { let path = solver .get_path_single_goal(&mut pathing_grid, *start, *end, false) .unwrap(); - let mut v = path[0]; - let n = path.len(); - let mut total_cost_int = 0; - for i in 1..n { - let v_old = v; - v = path[i]; - let cost = solver.heuristic(&pathing_grid, &v_old, &v); - total_cost_int += cost; - } - save_path(path, "path.csv").unwrap(); + save_path(path.clone(), "path.csv").unwrap(); + let total_cost_int = solver.get_path_cost(path, &pathing_grid); let float_cost = convert_cost_to_unit_cost_float(total_cost_int); println!("My distance: {float_cost}"); if *distance >= 0.01 { From 81c3daf00461930ed11281d02d2e95ca17ff86ed Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 10:00:13 +0100 Subject: [PATCH 18/41] Added get_path_cost_float to GridSolver --- src/lib.rs | 5 +---- src/solver/mod.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 94ecc70..e886f4f 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,10 +32,7 @@ const D: i32 = if EQUAL_EDGE_COST { 1 } else { 99 }; const C: i32 = if EQUAL_EDGE_COST { 1 } else { 70 }; const E: i32 = 2 * C - D; -/// Converts the integer cost to an approximate floating point equivalent where cardinal directions have cost 1.0. -pub fn convert_cost_to_unit_cost_float(cost: i32) -> f64 { - (cost as f64) / (C as f64) -} + /// Helper function for debugging binary representations of neighborhoods. pub fn explain_bin_neighborhood(nn: u8) { diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 4189b32..ff7fe76 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -1,8 +1,14 @@ -use crate::{pathing_grid::PathingGrid, waypoints_to_path, EQUAL_EDGE_COST}; +use crate::{C, EQUAL_EDGE_COST, pathing_grid::PathingGrid, waypoints_to_path}; use grid_util::Point; pub mod astar; pub mod jps; + +/// Converts the integer cost to an approximate floating point equivalent where cardinal directions have cost 1.0. +pub fn convert_cost_to_unit_cost_float(cost: i32) -> f64 { + (cost as f64) / (C as f64) +} + pub trait GridSolver { type Successors: IntoIterator; @@ -32,7 +38,9 @@ pub trait GridSolver { } total_cost_int } - + fn get_path_cost_float(&self, path: Vec, pathing_grid: &PathingGrid) -> f64 { + convert_cost_to_unit_cost_float(self.get_path_cost(path, pathing_grid)) + } fn get_path_single_goal( &self, grid: &mut PathingGrid, From c667ec5d0ddb861a556f4a69cd12ab253ffd26f3 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 10:14:56 +0100 Subject: [PATCH 19/41] Added fuzz test verifying Astar and JPS distances match --- src/solver/mod.rs | 6 ++-- tests/benchmark_distances.rs | 6 ++-- tests/fuzz_test.rs | 68 +++++++++++++++++++++++++++++++----- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/src/solver/mod.rs b/src/solver/mod.rs index ff7fe76..5dc67cd 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -1,4 +1,4 @@ -use crate::{C, EQUAL_EDGE_COST, pathing_grid::PathingGrid, waypoints_to_path}; +use crate::{pathing_grid::PathingGrid, waypoints_to_path, C, EQUAL_EDGE_COST}; use grid_util::Point; pub mod astar; @@ -26,7 +26,7 @@ pub trait GridSolver { where F: Fn(&Point) -> bool; - fn get_path_cost(&self, path: Vec, pathing_grid: &PathingGrid) -> i32 { + fn get_path_cost(&self, path: &Vec, pathing_grid: &PathingGrid) -> i32 { let mut v = path[0]; let n = path.len(); let mut total_cost_int = 0; @@ -38,7 +38,7 @@ pub trait GridSolver { } total_cost_int } - fn get_path_cost_float(&self, path: Vec, pathing_grid: &PathingGrid) -> f64 { + fn get_path_cost_float(&self, path: &Vec, pathing_grid: &PathingGrid) -> f64 { convert_cost_to_unit_cost_float(self.get_path_cost(path, pathing_grid)) } fn get_path_single_goal( diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index 3ee4ecb..efadb54 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -36,8 +36,7 @@ fn verify_solution_distance_jps() { .get_path_single_goal(&mut pathing_grid, *start, *end, false) .unwrap(); save_path(path.clone(), "path.csv").unwrap(); - let total_cost_int = solver.get_path_cost(path, &pathing_grid); - let float_cost = convert_cost_to_unit_cost_float(total_cost_int); + let float_cost = solver.get_path_cost_float(&path, &pathing_grid); println!("My distance: {float_cost}"); if *distance >= 0.01 { let delta_dist = (float_cost - distance).abs() / distance; @@ -66,8 +65,7 @@ fn verify_solution_distance_astar() { .get_path_single_goal(&mut pathing_grid, *start, *end, false) .unwrap(); save_path(path.clone(), "path.csv").unwrap(); - let total_cost_int = solver.get_path_cost(path, &pathing_grid); - let float_cost = convert_cost_to_unit_cost_float(total_cost_int); + let float_cost = solver.get_path_cost_float(&path, &pathing_grid); println!("My distance: {float_cost}"); if *distance >= 0.01 { let delta_dist = (float_cost - distance).abs() / distance; diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index 929025d..b2c077b 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -1,13 +1,15 @@ /// Fuzzes pathfinding system by checking for many random grids that a path is always found if the goal is reachable /// by being part of the same connected component. All system settings (diagonals, improved pruning) are tested. -use grid_pathfinding::*; +use grid_pathfinding::{ + pathing_grid::PathingGrid, + solver::{astar::AstarSolver, jps::JPSSolver, GridSolver}, +}; use grid_util::*; use rand::prelude::*; -fn random_grid(n: usize, rng: &mut StdRng, diagonal: bool, improved_pruning: bool) -> Pathfinder { - let mut pathing_grid: Pathfinder = Pathfinder::new(n, n, false); +fn random_grid(w: usize, h: usize, rng: &mut StdRng, diagonal: bool) -> PathingGrid { + let mut pathing_grid: PathingGrid = PathingGrid::new(w, h, false); pathing_grid.allow_diagonal_move = diagonal; - pathing_grid.improved_pruning = improved_pruning; for x in 0..pathing_grid.width() as i32 { for y in 0..pathing_grid.height() as i32 { pathing_grid.set(x, y, rng.gen_bool(0.4)) @@ -17,7 +19,7 @@ fn random_grid(n: usize, rng: &mut StdRng, diagonal: bool, improved_pruning: boo pathing_grid } -fn visualize_grid(grid: &Pathfinder, start: &Point, end: &Point) { +fn visualize_grid(grid: &PathingGrid, start: &Point, end: &Point) { let grid = &grid.grid; for y in (0..grid.height as i32).rev() { for x in 0..grid.width as i32 { @@ -42,18 +44,20 @@ fn fuzz() { const N_GRIDS: usize = 10000; let mut rng = StdRng::seed_from_u64(0); for (diagonal, improved_pruning) in [(false, false), (true, false), (true, true)] { - let mut random_grids: Vec = Vec::new(); + let mut random_grids: Vec = Vec::new(); for _ in 0..N_GRIDS { - random_grids.push(random_grid(N, &mut rng, diagonal, improved_pruning)) + random_grids.push(random_grid(N, N, &mut rng, diagonal)) } let start = Point::new(0, 0); let end = Point::new(N as i32 - 1, N as i32 - 1); for mut random_grid in random_grids { + let mut solver = JPSSolver::new(&random_grid, improved_pruning); random_grid.set_point(start, false); random_grid.set_point(end, false); + solver.set_all_jumppoints(&random_grid); let reachable = random_grid.reachable(&start, &end); - let path = random_grid.get_path_single_goal(start, end, false); + let path = solver.get_path_single_goal(&mut random_grid, start, end, false); // Show the grid if a path is not found if path.is_some() != reachable { visualize_grid(&random_grid, &start, &end); @@ -62,3 +66,51 @@ fn fuzz() { } } } + +#[test] +fn fuzz_distance() { + const N: usize = 5; + const N_GRIDS: usize = 10000; + let mut rng = StdRng::seed_from_u64(0); + let astar_solver = AstarSolver::new(); + + for (diagonal, improved_pruning) in [(false, false), (true, false)] { + let mut random_grids: Vec = Vec::new(); + for _ in 0..N_GRIDS { + random_grids.push(random_grid(N, N - 1, &mut rng, diagonal)) + } + + let start = Point::new(0, 0); + let end = Point::new(N as i32 - 1, N as i32 - 2); + for mut random_grid in random_grids { + let mut jps_solver = JPSSolver::new(&random_grid, improved_pruning); + random_grid.set_point(start, false); + random_grid.set_point(end, false); + jps_solver.set_all_jumppoints(&random_grid); + let reachable = random_grid.reachable(&start, &end); + if reachable { + let jps_path = jps_solver + .get_path_single_goal(&mut random_grid, start, end, false) + .unwrap(); + let astar_path = astar_solver + .get_path_single_goal(&mut random_grid, start, end, false) + .unwrap(); + + let astar_cost = astar_solver.get_path_cost_float(&astar_path, &random_grid); + let jps_cost = jps_solver.get_path_cost_float(&jps_path, &random_grid); + if astar_cost >= 0.01 { + let delta_dist = (jps_cost - astar_cost).abs() / astar_cost; + if delta_dist >= 0.01 { + println!("Astar distance: {astar_cost}; JPS distance: {jps_cost}"); + println!("Astar path: {astar_path:?}\n JPS path: {jps_path:?}\n"); + let grid_diag = random_grid.allow_diagonal_move; + println!("diagonal: {diagonal}; grid_diag: {grid_diag}; improved_pruning: {improved_pruning}"); + + visualize_grid(&random_grid, &start, &end); + } + assert!(delta_dist < 0.01); + } + } + } + } +} From b5c64c060681c172ddd931459cd7c0387999291b Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 10:29:43 +0100 Subject: [PATCH 20/41] Made JPSSolver complete and optimal by fixing pruned_neighborhood --- src/solver/jps.rs | 17 +++++++++-------- tests/fuzz_test.rs | 4 ++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/solver/jps.rs b/src/solver/jps.rs index 2bbc2d1..88421b1 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -127,12 +127,12 @@ impl JPSSolver { n_mask = 0b01000101_u8.rotate_left(dir_num as u32); } else if dir.diagonal() { n_mask = 0b10000011_u8.rotate_left(dir_num as u32); - // if !self.indexed_neighbor(node, 3 + dir_num) { - // n_mask |= 1 << ((dir_num + 2) % 8); - // } - // if !self.indexed_neighbor(node, 5 + dir_num) { - // n_mask |= 1 << ((dir_num + 6) % 8); - // } + if !grid.indexed_neighbor(node, 3 + dir_num) { + n_mask |= 1 << ((dir_num + 2) % 8); + } + if !grid.indexed_neighbor(node, 5 + dir_num) { + n_mask |= 1 << ((dir_num + 6) % 8); + } } else { if ALLOW_CORNER_CUTTING { n_mask = 0b00000001 << dir_num; @@ -143,8 +143,9 @@ impl JPSSolver { n_mask |= 1 << ((dir_num + 7) % 8); } } else { - neighbours &= 0b01010101; - n_mask = 0b01000101_u8.rotate_left(dir_num as u32); + // TODO: look into whether this is minimal, this at least makes the algorithm + // optimal and complete following the no corner cutting rule + n_mask = 0b11010111_u8.rotate_left(dir_num as u32); } } let comb_mask = neighbours & n_mask; diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index b2c077b..da01340 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -77,11 +77,11 @@ fn fuzz_distance() { for (diagonal, improved_pruning) in [(false, false), (true, false)] { let mut random_grids: Vec = Vec::new(); for _ in 0..N_GRIDS { - random_grids.push(random_grid(N, N - 1, &mut rng, diagonal)) + random_grids.push(random_grid(N, N, &mut rng, diagonal)) } let start = Point::new(0, 0); - let end = Point::new(N as i32 - 1, N as i32 - 2); + let end = Point::new(N as i32 - 1, N as i32 - 1); for mut random_grid in random_grids { let mut jps_solver = JPSSolver::new(&random_grid, improved_pruning); random_grid.set_point(start, false); From 4a5a01ddc2faa64883106b72f7b59e440aafe9d3 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 10:56:18 +0100 Subject: [PATCH 21/41] Ported old PathingGrid/Pathfinder units tests to new test suites for AstarSolver and the new PathingGrid --- src/lib.rs | 217 +------------------------------------------- src/pathing_grid.rs | 88 ++++++++++++++++++ src/solver/astar.rs | 129 ++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 213 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e886f4f..7474b2e 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -32,7 +32,10 @@ const D: i32 = if EQUAL_EDGE_COST { 1 } else { 99 }; const C: i32 = if EQUAL_EDGE_COST { 1 } else { 70 }; const E: i32 = 2 * C - D; - +/// Converts the integer cost to an approximate floating point equivalent where cardinal directions have cost 1.0. +pub fn convert_cost_to_unit_cost_float(cost: i32) -> f64 { + (cost as f64) / (C as f64) +} /// Helper function for debugging binary representations of neighborhoods. pub fn explain_bin_neighborhood(nn: u8) { @@ -661,215 +664,3 @@ impl ValueGrid for Pathfinder { self.grid.height() } } - -#[cfg(test)] -mod tests { - use grid_util::Rect; - - use super::*; - - /// Tests whether points are correctly mapped to different connected components - #[test] - fn test_component_generation() { - // Corresponds to the following 3x3 grid: - // ___ - // | # | - // | # | - // ___ - let mut path_graph = Pathfinder::new(3, 2, false); - path_graph.grid.set(1, 0, true); - path_graph.grid.set(1, 1, true); - let f_ix = |p| path_graph.get_ix_point(p); - let p1 = Point::new(0, 0); - let p2 = Point::new(1, 1); - let p3 = Point::new(0, 1); - let p4 = Point::new(2, 0); - let p1_ix = f_ix(&p1); - let p2_ix = f_ix(&p2); - let p3_ix = f_ix(&p3); - let p4_ix = f_ix(&p4); - path_graph.generate_components(); - assert!(!path_graph.components.equiv(p1_ix, p2_ix)); - assert!(path_graph.components.equiv(p1_ix, p3_ix)); - assert!(!path_graph.components.equiv(p1_ix, p4_ix)); - } - - #[test] - fn reachable_with_diagonals() { - let mut path_graph = Pathfinder::new(3, 2, false); - path_graph.grid.set(1, 0, true); - path_graph.grid.set(1, 1, true); - let p1 = Point::new(0, 0); - let p2 = Point::new(1, 0); - let p3 = Point::new(0, 1); - let p4 = Point::new(2, 0); - path_graph.generate_components(); - assert!(path_graph.unreachable(&p1, &p2)); - assert!(!path_graph.unreachable(&p1, &p3)); - assert!(path_graph.unreachable(&p1, &p4)); - assert!(!path_graph.neighbours_unreachable(&p1, &p2)); - assert!(path_graph.neighbours_unreachable(&p1, &p4)); - } - - /// Asserts that the two corners are connected on a 4-grid. - #[test] - fn reachable_without_diagonals() { - // |S | - // | # | - // | G| - // ___ - let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); - pathing_grid.improved_pruning = false; - pathing_grid.allow_diagonal_move = false; - pathing_grid.set(1, 1, true); - pathing_grid.generate_components(); - let start = Point::new(0, 0); - let end = Point::new(2, 2); - assert!(pathing_grid.reachable(&start, &end)); - } - - /// Asserts that the case in which start and goal are equal is handled correctly. - #[test] - fn equal_start_goal() { - for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { - let mut pathing_grid: Pathfinder = Pathfinder::new(1, 1, false); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.improved_pruning = pruning; - pathing_grid.generate_components(); - let start = Point::new(0, 0); - let path = pathing_grid - .get_path_single_goal(start, start, false) - .unwrap(); - assert!(path.len() == 1); - } - } - - /// Asserts that the optimal 4 step solution is found. - #[test] - fn solve_simple_problem() { - for (allow_diag, pruning, expected) in - [(false, false, 5), (true, false, 4), (true, true, 4)] - { - let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.improved_pruning = pruning; - pathing_grid.set(1, 1, true); - pathing_grid.generate_components(); - let start = Point::new(0, 0); - let end = Point::new(2, 2); - let path = pathing_grid - .get_path_single_goal(start, end, false) - .unwrap(); - assert!(path.len() == expected); - } - } - - #[test] - fn test_multiple_goals() { - for (allow_diag, pruning, expected) in - [(false, false, 7), (true, false, 5), (true, true, 5)] - { - let mut pathing_grid: Pathfinder = Pathfinder::new(5, 5, false); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.improved_pruning = pruning; - pathing_grid.set(1, 1, true); - pathing_grid.generate_components(); - let start = Point::new(0, 0); - let goal_1 = Point::new(4, 4); - let goal_2 = Point::new(3, 3); - let goals = vec![&goal_1, &goal_2]; - let (selected_goal, path) = pathing_grid.get_path_multiple_goals(start, goals).unwrap(); - assert_eq!(selected_goal, Point::new(3, 3)); - assert!(path.len() == expected); - } - } - - #[test] - fn test_complex() { - for (allow_diag, pruning, expected) in - [(false, false, 15), (true, false, 10), (true, true, 10)] - { - let mut pathing_grid: Pathfinder = Pathfinder::new(10, 10, false); - pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); - pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); - pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); - pathing_grid.set_rect(Rect::new(8, 8, 1, 1), true); - // pathing_grid.improved_pruning = false; - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.improved_pruning = pruning; - pathing_grid.generate_components(); - let start = Point::new(0, 0); - let end = Point::new(7, 7); - let path = pathing_grid - .get_path_single_goal(start, end, false) - .unwrap(); - assert!(path.len() == expected); - } - } - #[test] - fn test_complex_waypoints() { - for (allow_diag, pruning, expected) in - [(false, false, 11), (true, false, 7), (true, true, 5)] - { - let mut pathing_grid: Pathfinder = Pathfinder::new(10, 10, false); - pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); - pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); - pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); - pathing_grid.set_rect(Rect::new(8, 8, 1, 1), true); - // pathing_grid.improved_pruning = false; - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.improved_pruning = pruning; - pathing_grid.generate_components(); - let start = Point::new(0, 0); - let end = Point::new(7, 7); - let path = pathing_grid - .get_waypoints_single_goal(start, end, false) - .unwrap(); - assert!(path.len() == expected); - } - } - - // Tests whether allowing diagonals has the expected effect on diagonal reachability in a minimal setting. - #[test] - fn test_diagonal_switch_reachable() { - // ___ - // | #| - // |# | - // __ - let mut pathing_grid: Pathfinder = Pathfinder::new(2, 2, true); - pathing_grid.allow_diagonal_move = false; - let mut pathing_grid_diag: Pathfinder = Pathfinder::new(2, 2, true); - for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { - pathing_grid.set(0, 0, false); - pathing_grid.set(1, 1, false); - pathing_grid.generate_components(); - } - let start = Point::new(0, 0); - let end = Point::new(1, 1); - assert!(pathing_grid.unreachable(&start, &end)); - assert!(pathing_grid_diag.reachable(&start, &end)); - } - - // Tests whether allowing diagonals has the expected effect on path existence in a minimal setting. - #[test] - fn test_diagonal_switch_path() { - // ___ - // | #| - // |# | - // __ - let mut pathing_grid: Pathfinder = Pathfinder::new(2, 2, true); - pathing_grid.allow_diagonal_move = false; - let mut pathing_grid_diag: Pathfinder = Pathfinder::new(2, 2, true); - for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { - pathing_grid.set(0, 0, false); - pathing_grid.set(1, 1, false); - pathing_grid.generate_components(); - } - let start = Point::new(0, 0); - let goal = Point::new(1, 1); - let path = pathing_grid.get_path_single_goal(start, goal, false); - let path_diag = pathing_grid_diag.get_path_single_goal(start, goal, false); - assert!(path.is_none()); - assert!(path_diag.is_some()); - } -} diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs index 9fa53f8..f420dac 100644 --- a/src/pathing_grid.rs +++ b/src/pathing_grid.rs @@ -264,3 +264,91 @@ impl ValueGrid for PathingGrid { self.grid.height() } } + +#[cfg(test)] +mod tests { + use super::*; + + /// Tests whether points are correctly mapped to different connected components + #[test] + fn test_component_generation() { + // Corresponds to the following 3x3 grid: + // ___ + // | # | + // | # | + // ___ + let mut path_graph = PathingGrid::new(3, 2, false); + path_graph.grid.set(1, 0, true); + path_graph.grid.set(1, 1, true); + let f_ix = |p| path_graph.get_ix_point(p); + let p1 = Point::new(0, 0); + let p2 = Point::new(1, 1); + let p3 = Point::new(0, 1); + let p4 = Point::new(2, 0); + let p1_ix = f_ix(&p1); + let p2_ix = f_ix(&p2); + let p3_ix = f_ix(&p3); + let p4_ix = f_ix(&p4); + path_graph.generate_components(); + assert!(!path_graph.components.equiv(p1_ix, p2_ix)); + assert!(path_graph.components.equiv(p1_ix, p3_ix)); + assert!(!path_graph.components.equiv(p1_ix, p4_ix)); + } + + #[test] + fn reachable_with_diagonals() { + let mut path_graph = PathingGrid::new(3, 2, false); + path_graph.grid.set(1, 0, true); + path_graph.grid.set(1, 1, true); + let p1 = Point::new(0, 0); + let p2 = Point::new(1, 0); + let p3 = Point::new(0, 1); + let p4 = Point::new(2, 0); + path_graph.generate_components(); + assert!(path_graph.unreachable(&p1, &p2)); + assert!(!path_graph.unreachable(&p1, &p3)); + assert!(path_graph.unreachable(&p1, &p4)); + assert!(!path_graph.neighbours_unreachable(&p1, &p2)); + assert!(path_graph.neighbours_unreachable(&p1, &p4)); + } + + /// Asserts that the two corners are connected on a 4-grid. + #[test] + fn reachable_without_diagonals() { + // |S | + // | # | + // | G| + // ___ + let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + pathing_grid.allow_diagonal_move = false; + pathing_grid.set(1, 1, true); + pathing_grid.generate_components(); + let start = Point::new(0, 0); + let end = Point::new(2, 2); + assert!(pathing_grid.reachable(&start, &end)); + } + // Tests whether allowing diagonals has the expected effect on diagonal reachability in a minimal setting. + #[test] + fn test_diagonal_switch_reachable() { + // ___ + // | #| + // |# | + // __ + let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); + pathing_grid.allow_diagonal_move = false; + let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); + for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { + pathing_grid.set(0, 0, false); + pathing_grid.set(1, 1, false); + pathing_grid.generate_components(); + } + let start = Point::new(0, 0); + let end = Point::new(1, 1); + assert!(pathing_grid.unreachable(&start, &end)); + if ALLOW_CORNER_CUTTING { + assert!(pathing_grid_diag.reachable(&start, &end)); + } else { + assert!(pathing_grid_diag.unreachable(&start, &end)); + } + } +} diff --git a/src/solver/astar.rs b/src/solver/astar.rs index 7f7c13f..c59a540 100644 --- a/src/solver/astar.rs +++ b/src/solver/astar.rs @@ -51,3 +51,132 @@ impl GridSolver for AstarSolver { (self.cost(grid, p1, p2) as f32 * self.heuristic_factor) as i32 } } + +#[cfg(test)] +mod tests { + use grid_util::{Rect, ValueGrid}; + + use crate::ALLOW_CORNER_CUTTING; + + use super::*; + + /// Asserts that the case in which start and goal are equal is handled correctly. + #[test] + fn equal_start_goal() { + for allow_diag in [false, true] { + let mut pathing_grid: PathingGrid = PathingGrid::new(1, 1, false); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let start = Point::new(0, 0); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, start, false) + .unwrap(); + assert!(path.len() == 1); + } + } + + /// Asserts that the optimal 4 step solution is found. + #[test] + fn solve_simple_problem() { + let arr = if ALLOW_CORNER_CUTTING { + [(false, 5), (true, 4)] + } else { + [(false, 5), (true, 5)] + }; + for (allow_diag, expected) in arr { + let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.set(1, 1, true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + + let start = Point::new(0, 0); + let end = Point::new(2, 2); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, end, false) + .unwrap(); + assert!(path.len() == expected); + } + } + + #[test] + fn test_multiple_goals() { + let arr = if ALLOW_CORNER_CUTTING { + [(false, 7), (true, 5)] + } else { + [(false, 7), (true, 6)] + }; + for (allow_diag, expected) in arr { + let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.set(1, 1, true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let start = Point::new(0, 0); + let goal_1 = Point::new(4, 4); + let goal_2 = Point::new(3, 3); + let goals = vec![&goal_1, &goal_2]; + let (selected_goal, path) = solver + .get_path_multiple_goals(&mut pathing_grid, start, goals) + .unwrap(); + assert_eq!(selected_goal, Point::new(3, 3)); + assert!(path.len() == expected); + } + } + + #[test] + fn test_complex() { + let arr = if ALLOW_CORNER_CUTTING { + [(false, 15), (true, 10)] + } else { + [(false, 15), (true, 11)] + }; + for (allow_diag, expected) in arr { + let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); + pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); + pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); + pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); + pathing_grid.set_rect(Rect::new(8, 8, 1, 1), true); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + + let start = Point::new(0, 0); + let end = Point::new(7, 7); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, end, false) + .unwrap(); + assert!(path.len() == expected); + } + } + + // Tests whether allowing diagonals has the expected effect on path existence in a minimal setting. + #[test] + fn test_diagonal_switch_path() { + // ___ + // | #| + // |# | + // __ + let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); + pathing_grid.allow_diagonal_move = false; + let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); + for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { + pathing_grid.set(0, 0, false); + pathing_grid.set(1, 1, false); + pathing_grid.generate_components(); + } + let solver = AstarSolver::new(); + + let start = Point::new(0, 0); + let goal = Point::new(1, 1); + let path = solver.get_path_single_goal(&mut pathing_grid, start, goal, false); + let path_diag = solver.get_path_single_goal(&mut pathing_grid_diag, start, goal, false); + assert!(path.is_none()); + if ALLOW_CORNER_CUTTING { + assert!(path_diag.is_some()); + } else { + assert!(path_diag.is_none()); + } + } +} From d168b5665eba163f5ec2bffc75a05bfdaa011dce Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 10:56:41 +0100 Subject: [PATCH 22/41] Replaced gen_bool with random_bool in random_grid --- tests/fuzz_test.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index da01340..0ad4948 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -12,7 +12,7 @@ fn random_grid(w: usize, h: usize, rng: &mut StdRng, diagonal: bool) -> PathingG pathing_grid.allow_diagonal_move = diagonal; for x in 0..pathing_grid.width() as i32 { for y in 0..pathing_grid.height() as i32 { - pathing_grid.set(x, y, rng.gen_bool(0.4)) + pathing_grid.set(x, y, rng.random_bool(0.4)) } } pathing_grid.generate_components(); From 9b85df520f651acee2a3f9fd79a576b37faba23f Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 11:57:19 +0100 Subject: [PATCH 23/41] Added WIP support for disabling corner cutting to original PathingGrid/Pathfinder --- src/lib.rs | 31 +++++++++++++++++++++++-------- src/solver/jps.rs | 3 +-- tests/benchmark_distances.rs | 1 - 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7474b2e..aa7343c 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,7 +160,7 @@ impl Pathfinder { let mut forced_mask: u8 = 0; for dir_num in 0..8 { if dir_num % 2 == 1 { - if !self.indexed_neighbor(node, 3 + dir_num) + if ALLOW_CORNER_CUTTING && !self.indexed_neighbor(node, 3 + dir_num) || !self.indexed_neighbor(node, 5 + dir_num) { forced_mask |= 1 << dir_num; @@ -196,12 +196,18 @@ impl Pathfinder { n_mask |= 1 << ((dir_num + 6) % 8); } } else { - n_mask = 0b00000001 << dir_num; - if !self.indexed_neighbor(node, 2 + dir_num) { - n_mask |= 1 << ((dir_num + 1) % 8); - } - if !self.indexed_neighbor(node, 6 + dir_num) { - n_mask |= 1 << ((dir_num + 7) % 8); + if ALLOW_CORNER_CUTTING { + n_mask = 0b00000001 << dir_num; + if !self.indexed_neighbor(node, 2 + dir_num) { + n_mask |= 1 << ((dir_num + 1) % 8); + } + if !self.indexed_neighbor(node, 6 + dir_num) { + n_mask |= 1 << ((dir_num + 7) % 8); + } + } else { + // TODO: look into whether this is minimal, this at least makes the algorithm + // optimal and complete following the no corner cutting rule + n_mask = 0b11010111_u8.rotate_left(dir_num as u32); } } let comb_mask = neighbours & n_mask; @@ -277,7 +283,7 @@ impl Pathfinder { // When using a 4-neighborhood (specified by setting allow_diagonal_move to false), // jumps perpendicular to the direction are performed. This is necessary to not miss the // goal when passing by. - if !self.allow_diagonal_move { + if !self.allow_diagonal_move || !ALLOW_CORNER_CUTTING && !direction.diagonal() { let perp_1 = direction.rotate_ccw(2); let perp_2 = direction.rotate_cw(2); if self.jump_straight(initial, 1, perp_1, goal).is_some() @@ -285,6 +291,15 @@ impl Pathfinder { { return Some((initial, cost)); } + if !ALLOW_CORNER_CUTTING && !direction.diagonal() { + let diag_1 = direction.rotate_ccw(1); + let diag_2 = direction.rotate_cw(1); + if self.jump(initial, 1, diag_1, goal).is_some() + || self.jump(initial, 1, diag_2, goal).is_some() + { + return Some((initial, cost)); + } + } } // See comment in pruned_neighborhood about cost calculation diff --git a/src/solver/jps.rs b/src/solver/jps.rs index 88421b1..83f6227 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -28,8 +28,7 @@ impl GridSolver for JPSSolver { Some(parent_node) => { let mut succ = SmallVec::new(); let dir = parent_node.dir_obj(node); - let p: Vec<_> = self.pruned_neighborhood(dir, node, grid).collect(); - for (n, c) in p { + for (n, c) in self.pruned_neighborhood(dir, node, grid) { let dir = node.dir_obj(&n); // Jumps the neighbor, skipping over unnecessary nodes. if let Some((jumped_node, cost)) = self.jump(*node, c, dir, goal, grid) { diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index efadb54..e47b8c8 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -1,6 +1,5 @@ use grid_pathfinding::pathing_grid::PathingGrid; use grid_pathfinding::solver::{astar::AstarSolver, jps::JPSSolver, GridSolver}; -use grid_pathfinding::*; use grid_pathfinding_benchmark::get_benchmark; use grid_util::*; use std::fs::File; From 5fc3b82c786a31fec78c859b777b40335bbdf4c6 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 12:22:21 +0100 Subject: [PATCH 24/41] Moved neighbours grid and logic from PathingGrid to JPSSolver, further minimizing PathingGrid --- src/pathing_grid.rs | 61 +------------------- src/solver/jps.rs | 108 ++++++++++++++++++++++++++++------- tests/benchmark_distances.rs | 4 +- 3 files changed, 92 insertions(+), 81 deletions(-) diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs index f420dac..c9fcf6b 100644 --- a/src/pathing_grid.rs +++ b/src/pathing_grid.rs @@ -9,7 +9,6 @@ use std::sync::{Arc, Mutex}; #[derive(Clone, Debug)] pub struct PathingGrid { pub grid: BoolGrid, - pub neighbours: SimpleValueGrid, pub components: UnionFind, pub components_dirty: bool, pub allow_diagonal_move: bool, @@ -18,15 +17,13 @@ pub struct PathingGrid { impl Default for PathingGrid { fn default() -> PathingGrid { - let mut grid = PathingGrid { + let grid = PathingGrid { grid: BoolGrid::default(), - neighbours: SimpleValueGrid::default(), components: UnionFind::new(0), components_dirty: false, allow_diagonal_move: true, context: Arc::new(Mutex::new(SearchContext::new())), }; - grid.initialize(); grid } } @@ -65,28 +62,7 @@ impl PathingGrid { fn in_bounds(&self, x: i32, y: i32) -> bool { self.grid.index_in_bounds(x, y) } - /// The neighbour indexing used here corresponds to that used in [grid_util::Direction]. - pub fn indexed_neighbor(&self, node: &Point, index: i32) -> bool { - (self.neighbours.get_point(*node) & 1 << (index.rem_euclid(8))) != 0 - } - /// Updates the neighbours grid after changing the grid. - fn update_neighbours(&mut self, x: i32, y: i32, blocked: bool) { - let p = Point::new(x, y); - for i in 0..8 { - let neighbor = p.moore_neighbor(i); - if self.in_bounds(neighbor.x, neighbor.y) { - let ix = (i + 4) % 8; - let mut n_mask = self.neighbours.get_point(neighbor); - if blocked { - n_mask &= !(1 << ix); - } else { - n_mask |= 1 << ix; - } - self.neighbours.set_point(neighbor, n_mask); - } - } - } /// Retrieves the component id a given [Point] belongs to. pub fn get_component(&self, point: &Point) -> usize { self.components.find(self.get_ix_point(point)) @@ -141,28 +117,6 @@ impl PathingGrid { } } - pub fn update_all_neighbours(&mut self) { - for x in 0..self.width() as i32 { - for y in 0..self.height() as i32 { - self.update_neighbours(x, y, self.get(x, y)); - } - } - } - - pub fn initialize(&mut self) { - // Emulates 'placing' of blocked tile around map border to correctly initialize neighbours - // and make behaviour of a map bordered by tiles the same as a borderless map. - for i in -1..=(self.width() as i32) { - self.update_neighbours(i, -1, true); - self.update_neighbours(i, self.height() as i32, true); - } - for j in -1..=(self.height() as i32) { - self.update_neighbours(-1, j, true); - self.update_neighbours(self.width() as i32, j, true); - } - self.update_all_neighbours(); - } - /// Generates a new [UnionFind] structure and links up grid neighbours to the same components. pub fn generate_components(&mut self) { let w = self.grid.width; @@ -213,28 +167,20 @@ impl fmt::Display for PathingGrid { .collect::>(); writeln!(f, "{:?}", values)?; } - writeln!(f, "\nNeighbours:")?; - for y in 0..self.neighbours.height as i32 { - let values = (0..self.neighbours.width as i32) - .map(|x| self.neighbours.get(x, y) as i32) - .collect::>(); - writeln!(f, "{:?}", values)?; - } Ok(()) } } impl ValueGrid for PathingGrid { fn new(width: usize, height: usize, default_value: bool) -> Self { - let mut base_grid = PathingGrid { + let base_grid = PathingGrid { grid: BoolGrid::new(width, height, default_value), - neighbours: SimpleValueGrid::new(width, height, 0b11111111), + components: UnionFind::new(width * height), components_dirty: false, allow_diagonal_move: true, context: Arc::new(Mutex::new(SearchContext::new())), }; - base_grid.initialize(); base_grid } fn get(&self, x: i32, y: i32) -> bool { @@ -254,7 +200,6 @@ impl ValueGrid for PathingGrid { } } } - self.update_neighbours(p.x, p.y, blocked); self.grid.set(x, y, blocked); } fn width(&self) -> usize { diff --git a/src/solver/jps.rs b/src/solver/jps.rs index 83f6227..df11748 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -1,3 +1,4 @@ +use core::fmt; use grid_util::{Direction, Point, SimpleValueGrid, ValueGrid}; use smallvec::SmallVec; @@ -8,6 +9,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct JPSSolver { pub jump_point: SimpleValueGrid, + pub neighbours: SimpleValueGrid, pub improved_pruning: bool, } @@ -81,6 +83,7 @@ impl JPSSolver { pub fn new(grid: &PathingGrid, improved_pruning: bool) -> JPSSolver { let mut solver = JPSSolver { jump_point: SimpleValueGrid::new(grid.width(), grid.height(), 0), + neighbours: SimpleValueGrid::new(grid.width(), grid.height(), 0b11111111), improved_pruning, }; solver.initialize(grid); @@ -91,19 +94,48 @@ impl JPSSolver { let dir_num = dir.num(); self.jump_point.get_point(*node) & (1 << dir_num) != 0 } - - fn forced_mask(&self, node: &Point, grid: &PathingGrid) -> u8 { + /// The neighbour indexing used here corresponds to that used in [grid_util::Direction]. + pub fn indexed_neighbor(&self, node: &Point, index: i32) -> bool { + (self.neighbours.get_point(*node) & 1 << (index.rem_euclid(8))) != 0 + } + fn width(&self) -> usize { + self.neighbours.width() + } + fn height(&self) -> usize { + self.neighbours.height() + } + fn in_bounds(&self, x: i32, y: i32) -> bool { + self.neighbours.index_in_bounds(x, y) + } + /// Updates the neighbours grid after changing the grid. + fn update_neighbours(&mut self, x: i32, y: i32, blocked: bool) { + let p = Point::new(x, y); + for i in 0..8 { + let neighbor = p.moore_neighbor(i); + if self.in_bounds(neighbor.x, neighbor.y) { + let ix = (i + 4) % 8; + let mut n_mask = self.neighbours.get_point(neighbor); + if blocked { + n_mask &= !(1 << ix); + } else { + n_mask |= 1 << ix; + } + self.neighbours.set_point(neighbor, n_mask); + } + } + } + fn forced_mask(&self, node: &Point) -> u8 { let mut forced_mask: u8 = 0; for dir_num in 0..8 { if dir_num % 2 == 1 { - if ALLOW_CORNER_CUTTING && !grid.indexed_neighbor(node, 3 + dir_num) - || !grid.indexed_neighbor(node, 5 + dir_num) + if ALLOW_CORNER_CUTTING && !self.indexed_neighbor(node, 3 + dir_num) + || !self.indexed_neighbor(node, 5 + dir_num) { forced_mask |= 1 << dir_num; } } else { - if !grid.indexed_neighbor(node, 2 + dir_num) - || !grid.indexed_neighbor(node, 6 + dir_num) + if !self.indexed_neighbor(node, 2 + dir_num) + || !self.indexed_neighbor(node, 6 + dir_num) { forced_mask |= 1 << dir_num; } @@ -120,25 +152,25 @@ impl JPSSolver { ) -> impl Iterator + 'a { let dir_num = dir.num(); let mut n_mask: u8; - let mut neighbours = grid.neighbours.get_point(*node); + let mut neighbours = self.neighbours.get_point(*node); if !grid.allow_diagonal_move { neighbours &= 0b01010101; n_mask = 0b01000101_u8.rotate_left(dir_num as u32); } else if dir.diagonal() { n_mask = 0b10000011_u8.rotate_left(dir_num as u32); - if !grid.indexed_neighbor(node, 3 + dir_num) { + if !self.indexed_neighbor(node, 3 + dir_num) { n_mask |= 1 << ((dir_num + 2) % 8); } - if !grid.indexed_neighbor(node, 5 + dir_num) { + if !self.indexed_neighbor(node, 5 + dir_num) { n_mask |= 1 << ((dir_num + 6) % 8); } } else { if ALLOW_CORNER_CUTTING { n_mask = 0b00000001 << dir_num; - if !grid.indexed_neighbor(node, 2 + dir_num) { + if !self.indexed_neighbor(node, 2 + dir_num) { n_mask |= 1 << ((dir_num + 1) % 8); } - if !grid.indexed_neighbor(node, 6 + dir_num) { + if !self.indexed_neighbor(node, 6 + dir_num) { n_mask |= 1 << ((dir_num + 7) % 8); } } else { @@ -246,29 +278,65 @@ impl JPSSolver { } } - pub fn set_jumppoints(&mut self, point: Point, grid: &PathingGrid) { - let value = self.forced_mask(&point, grid); + pub fn set_jumppoints(&mut self, point: Point) { + let value = self.forced_mask(&point); self.jump_point.set_point(point, value); } pub fn fix_jumppoints(&mut self, point: Point, grid: &PathingGrid) { - self.set_jumppoints(point, grid); + self.set_jumppoints(point); for p in grid.neighborhood_points(&point) { if grid.point_in_bounds(p) { - self.set_jumppoints(p, grid); + self.set_jumppoints(p); } } } /// Performs the full jump point precomputation - pub fn set_all_jumppoints(&mut self, grid: &PathingGrid) { - for x in 0..grid.width() { - for y in 0..grid.height() { - self.set_jumppoints(Point::new(x as i32, y as i32), grid); + pub fn set_all_jumppoints(&mut self) { + for x in 0..self.width() { + for y in 0..self.height() { + self.set_jumppoints(Point::new(x as i32, y as i32)); } } } + /// Updates the neighbours + fn set(&mut self, x: i32, y: i32, blocked: bool) { + let p = Point::new(x, y); + self.update_neighbours(p.x, p.y, blocked); + } + pub fn update_all_neighbours(&mut self, grid: &PathingGrid) { + for x in 0..self.width() as i32 { + for y in 0..self.height() as i32 { + self.update_neighbours(x, y, grid.get(x, y)); + } + } + } pub fn initialize(&mut self, grid: &PathingGrid) { - self.set_all_jumppoints(grid); + // Emulates 'placing' of blocked tile around map border to correctly initialize neighbours + // and make behaviour of a map bordered by tiles the same as a borderless map. + for i in -1..=(self.width() as i32) { + self.update_neighbours(i, -1, true); + self.update_neighbours(i, self.height() as i32, true); + } + for j in -1..=(self.height() as i32) { + self.update_neighbours(-1, j, true); + self.update_neighbours(self.width() as i32, j, true); + } + self.update_all_neighbours(grid); + self.set_all_jumppoints(); + } +} + +impl fmt::Display for JPSSolver { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "Neighbours:")?; + for y in 0..self.neighbours.height as i32 { + let values = (0..self.neighbours.width as i32) + .map(|x| self.neighbours.get(x, y) as i32) + .collect::>(); + writeln!(f, "{:?}", values)?; + } + Ok(()) } } diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index e47b8c8..3600f72 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -24,10 +24,9 @@ fn verify_solution_distance_jps() { pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = true; - pathing_grid.initialize(); pathing_grid.generate_components(); let mut solver = JPSSolver::new(&pathing_grid, false); - solver.set_all_jumppoints(&pathing_grid); + solver.initialize(&pathing_grid); for (start, end, distance) in &scenarios { println!("Start: {start}; End: {end}; Distance: {distance}"); @@ -56,7 +55,6 @@ fn verify_solution_distance_astar() { PathingGrid::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = true; - pathing_grid.initialize(); pathing_grid.generate_components(); for (start, end, distance) in &scenarios { println!("Start: {start}; End: {end}; Distance: {distance}"); From ce3cfe2ec71df92a2748ab8337145e11e9820354 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 12:23:13 +0100 Subject: [PATCH 25/41] Expanded comparison bench to compare old JPS, new JPS and A* --- benches/comparison_bench.rs | 79 +++++++++++++++++++++++++++++++++++-- benches/single_bench.rs | 20 +++++++--- 2 files changed, 90 insertions(+), 9 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 970fefa..87c5696 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -1,11 +1,15 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use grid_pathfinding::Pathfinder; +use grid_pathfinding::{ + pathing_grid::PathingGrid, + solver::{astar::AstarSolver, jps::JPSSolver, GridSolver}, + Pathfinder, +}; use grid_pathfinding_benchmark::*; use grid_util::grid::ValueGrid; use std::hint::black_box; fn dao_bench(c: &mut Criterion) { - for (allow_diag, pruning) in [(true, false), (true, true)] { + for (allow_diag, pruning) in [(true, false)] { let bench_set = if allow_diag { ["dao/arena", "dao/den312d", "dao/arena2"] } else { @@ -34,5 +38,74 @@ fn dao_bench(c: &mut Criterion) { } } -criterion_group!(benches, dao_bench); +fn dao_bench_jps(c: &mut Criterion) { + for (allow_diag, pruning) in [(true, false)] { + let bench_set = if allow_diag { + ["dao/arena", "dao/den312d", "dao/arena2"] + } else { + ["dao/arena", "dao/den009d", "dao/den312d"] + }; + for name in bench_set { + let (bool_grid, scenarios) = get_benchmark(name.to_owned()); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.generate_components(); + let mut solver = JPSSolver::new(&pathing_grid, pruning); + solver.initialize(&pathing_grid); + let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; + let improved_str = if pruning { " (improved pruning)" } else { "" }; + + c.bench_function( + format!("{name}, JPS {diag_str}{improved_str}").as_str(), + |b| { + b.iter(|| { + for (start, end, _) in &scenarios { + black_box(solver.get_path_single_goal( + &mut pathing_grid, + *start, + *end, + false, + )); + } + }) + }, + ); + } + } +} +fn dao_bench_astar(c: &mut Criterion) { + for allow_diag in [false, true] { + let bench_set = if allow_diag { + ["dao/arena", "dao/den312d", "dao/arena2"] + } else { + ["dao/arena", "dao/den009d", "dao/den312d"] + }; + for name in bench_set { + let (bool_grid, scenarios) = get_benchmark(name.to_owned()); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; + + c.bench_function(format!("{name}, A* {diag_str}").as_str(), |b| { + b.iter(|| { + for (start, end, _) in &scenarios { + black_box(solver.get_path_single_goal( + &mut pathing_grid, + *start, + *end, + false, + )); + } + }) + }); + } + } +} +criterion_group!(benches, dao_bench, dao_bench_jps, dao_bench_astar); criterion_main!(benches); diff --git a/benches/single_bench.rs b/benches/single_bench.rs index 8bed392..cfcd3de 100644 --- a/benches/single_bench.rs +++ b/benches/single_bench.rs @@ -1,5 +1,8 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use grid_pathfinding::Pathfinder; +use grid_pathfinding::{ + pathing_grid::PathingGrid, + solver::{jps::JPSSolver, GridSolver}, +}; use grid_pathfinding_benchmark::*; use grid_util::grid::ValueGrid; use std::hint::black_box; @@ -9,20 +12,25 @@ fn dao_bench_single(c: &mut Criterion) { let bench_set = ["dao/arena"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: Pathfinder = - Pathfinder::new(bool_grid.width, bool_grid.height, true); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.improved_pruning = pruning; - pathing_grid.initialize(); pathing_grid.generate_components(); + let mut solver = JPSSolver::new(&pathing_grid, pruning); + solver.initialize(&pathing_grid); let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; let improved_str = if pruning { " (improved pruning)" } else { "" }; c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { b.iter(|| { for (start, end, _) in &scenarios { - black_box(pathing_grid.get_path_single_goal(*start, *end, false)); + black_box(solver.get_path_single_goal( + &mut pathing_grid, + *start, + *end, + false, + )); } }) }); From b55fdbfa753a116da6aabfa59e4d7f24b5c3d0e6 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 13:27:06 +0100 Subject: [PATCH 26/41] Fixed bug in forced_mask --- src/lib.rs | 4 ++-- src/solver/jps.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index aa7343c..3acfdeb 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,8 +160,8 @@ impl Pathfinder { let mut forced_mask: u8 = 0; for dir_num in 0..8 { if dir_num % 2 == 1 { - if ALLOW_CORNER_CUTTING && !self.indexed_neighbor(node, 3 + dir_num) - || !self.indexed_neighbor(node, 5 + dir_num) + if ALLOW_CORNER_CUTTING && (!self.indexed_neighbor(node, 3 + dir_num) + || !self.indexed_neighbor(node, 5 + dir_num)) { forced_mask |= 1 << dir_num; } diff --git a/src/solver/jps.rs b/src/solver/jps.rs index df11748..d62e31f 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -128,8 +128,8 @@ impl JPSSolver { let mut forced_mask: u8 = 0; for dir_num in 0..8 { if dir_num % 2 == 1 { - if ALLOW_CORNER_CUTTING && !self.indexed_neighbor(node, 3 + dir_num) - || !self.indexed_neighbor(node, 5 + dir_num) + if ALLOW_CORNER_CUTTING && (!self.indexed_neighbor(node, 3 + dir_num) + || !self.indexed_neighbor(node, 5 + dir_num)) { forced_mask |= 1 << dir_num; } From 4d1ee52a339883ce0e810bf2c7ffbdefa98c4168 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 16:22:21 +0100 Subject: [PATCH 27/41] Gave cost a default implementation in GridSolver to reduce duplication --- src/lib.rs | 5 +++-- src/solver/astar.rs | 16 +--------------- src/solver/jps.rs | 21 ++++----------------- src/solver/mod.rs | 16 ++++++++++++++-- 4 files changed, 22 insertions(+), 36 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3acfdeb..6448ebb 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -160,8 +160,9 @@ impl Pathfinder { let mut forced_mask: u8 = 0; for dir_num in 0..8 { if dir_num % 2 == 1 { - if ALLOW_CORNER_CUTTING && (!self.indexed_neighbor(node, 3 + dir_num) - || !self.indexed_neighbor(node, 5 + dir_num)) + if ALLOW_CORNER_CUTTING + && (!self.indexed_neighbor(node, 3 + dir_num) + || !self.indexed_neighbor(node, 5 + dir_num)) { forced_mask |= 1 << dir_num; } diff --git a/src/solver/astar.rs b/src/solver/astar.rs index c59a540..3df0eb1 100644 --- a/src/solver/astar.rs +++ b/src/solver/astar.rs @@ -32,21 +32,7 @@ impl GridSolver for AstarSolver { grid.neighborhood_points_and_cost(node) } - /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. - fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { - if grid.allow_diagonal_move { - let delta_x = (p1.x - p2.x).abs(); - let delta_y = (p1.y - p2.y).abs(); - // Formula from https://github.com/riscy/a_star_on_grids - // to efficiently compute the cost of a path taking the maximal amount - // of diagonal steps before going straight - (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 - } else { - p1.manhattan_distance(p2) * C - } - } - - /// Just the cost times a heuristic factor. + /// Just the normal cost times a heuristic factor. fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { (self.cost(grid, p1, p2) as f32 * self.heuristic_factor) as i32 } diff --git a/src/solver/jps.rs b/src/solver/jps.rs index d62e31f..f6c716e 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -62,21 +62,7 @@ impl GridSolver for JPSSolver { /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { - if grid.allow_diagonal_move { - let delta_x = (p1.x - p2.x).abs(); - let delta_y = (p1.y - p2.y).abs(); - // Formula from https://github.com/riscy/a_star_on_grids - // to efficiently compute the cost of a path taking the maximal amount - // of diagonal steps before going straight - (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 - } else { - p1.manhattan_distance(p2) * C - } - } - - /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. - fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { - self.heuristic(grid, p1, p2) + self.cost(grid, p1, p2) } } impl JPSSolver { @@ -128,8 +114,9 @@ impl JPSSolver { let mut forced_mask: u8 = 0; for dir_num in 0..8 { if dir_num % 2 == 1 { - if ALLOW_CORNER_CUTTING && (!self.indexed_neighbor(node, 3 + dir_num) - || !self.indexed_neighbor(node, 5 + dir_num)) + if ALLOW_CORNER_CUTTING + && (!self.indexed_neighbor(node, 3 + dir_num) + || !self.indexed_neighbor(node, 5 + dir_num)) { forced_mask |= 1 << dir_num; } diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 5dc67cd..c5ec8ca 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -1,4 +1,4 @@ -use crate::{pathing_grid::PathingGrid, waypoints_to_path, C, EQUAL_EDGE_COST}; +use crate::{pathing_grid::PathingGrid, waypoints_to_path, C, D, E, EQUAL_EDGE_COST}; use grid_util::Point; pub mod astar; @@ -14,7 +14,19 @@ pub trait GridSolver { fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; - fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; + /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. + fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + if grid.allow_diagonal_move { + let delta_x = (p1.x - p2.x).abs(); + let delta_y = (p1.y - p2.y).abs(); + // Formula from https://github.com/riscy/a_star_on_grids + // to efficiently compute the cost of a path taking the maximal amount + // of diagonal steps before going straight + (E * (delta_x - delta_y).abs() + D * (delta_x + delta_y)) / 2 + } else { + p1.manhattan_distance(p2) * C + } + } fn successors( &self, From ebad550d5cdf7c581a6bb18c6bcbfbcd26fb78b7 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 16:24:02 +0100 Subject: [PATCH 28/41] Some final changes related to neighbours having moved to JPSSolver --- src/solver/jps.rs | 6 ++++-- tests/fuzz_test.rs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/solver/jps.rs b/src/solver/jps.rs index f6c716e..a1da442 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -286,10 +286,12 @@ impl JPSSolver { } } } - /// Updates the neighbours - fn set(&mut self, x: i32, y: i32, blocked: bool) { + + /// Updates the neighbours and jumppoints + fn set(&mut self, x: i32, y: i32, blocked: bool, grid: &PathingGrid) { let p = Point::new(x, y); self.update_neighbours(p.x, p.y, blocked); + self.fix_jumppoints(p, grid); } pub fn update_all_neighbours(&mut self, grid: &PathingGrid) { diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index 0ad4948..68ca160 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -55,7 +55,7 @@ fn fuzz() { let mut solver = JPSSolver::new(&random_grid, improved_pruning); random_grid.set_point(start, false); random_grid.set_point(end, false); - solver.set_all_jumppoints(&random_grid); + solver.initialize(&random_grid); let reachable = random_grid.reachable(&start, &end); let path = solver.get_path_single_goal(&mut random_grid, start, end, false); // Show the grid if a path is not found @@ -86,7 +86,7 @@ fn fuzz_distance() { let mut jps_solver = JPSSolver::new(&random_grid, improved_pruning); random_grid.set_point(start, false); random_grid.set_point(end, false); - jps_solver.set_all_jumppoints(&random_grid); + jps_solver.initialize(&random_grid); let reachable = random_grid.reachable(&start, &end); if reachable { let jps_path = jps_solver From 81a5ea7bb23cde6b470ad1e6f25ff6d807de5365 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 16:26:54 +0100 Subject: [PATCH 29/41] Added DijkstraSolver as A* with a zero heuristic --- src/solver/astar.rs | 2 +- src/solver/dijkstra.rs | 29 +++++++++++++++++++++++++++++ src/solver/jps.rs | 2 +- src/solver/mod.rs | 1 + 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 src/solver/dijkstra.rs diff --git a/src/solver/astar.rs b/src/solver/astar.rs index 3df0eb1..ead6707 100644 --- a/src/solver/astar.rs +++ b/src/solver/astar.rs @@ -1,7 +1,7 @@ use grid_util::Point; use smallvec::SmallVec; -use crate::{pathing_grid::PathingGrid, solver::GridSolver, C, D, E, N_SMALLVEC_SIZE}; +use crate::{pathing_grid::PathingGrid, solver::GridSolver, N_SMALLVEC_SIZE}; #[derive(Clone, Debug)] pub struct AstarSolver { diff --git a/src/solver/dijkstra.rs b/src/solver/dijkstra.rs new file mode 100644 index 0000000..4a15990 --- /dev/null +++ b/src/solver/dijkstra.rs @@ -0,0 +1,29 @@ +use grid_util::Point; +use smallvec::SmallVec; + +use crate::{pathing_grid::PathingGrid, solver::GridSolver, N_SMALLVEC_SIZE}; + +#[derive(Clone, Debug)] +pub struct DijkstraSolver; + +impl GridSolver for DijkstraSolver { + type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; + + fn successors( + &self, + grid: &PathingGrid, + _parent: Option<&Point>, + node: &Point, + _goal: &F, + ) -> Self::Successors + where + F: Fn(&Point) -> bool, + { + grid.neighborhood_points_and_cost(node) + } + + /// Just the cost times a heuristic factor. + fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + 0 + } +} diff --git a/src/solver/jps.rs b/src/solver/jps.rs index a1da442..f90569d 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -3,7 +3,7 @@ use grid_util::{Direction, Point, SimpleValueGrid, ValueGrid}; use smallvec::SmallVec; use crate::{ - pathing_grid::PathingGrid, solver::GridSolver, ALLOW_CORNER_CUTTING, C, D, E, N_SMALLVEC_SIZE, + pathing_grid::PathingGrid, solver::GridSolver, ALLOW_CORNER_CUTTING, C, D, N_SMALLVEC_SIZE, }; #[derive(Clone, Debug)] diff --git a/src/solver/mod.rs b/src/solver/mod.rs index c5ec8ca..0e28757 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -2,6 +2,7 @@ use crate::{pathing_grid::PathingGrid, waypoints_to_path, C, D, E, EQUAL_EDGE_CO use grid_util::Point; pub mod astar; +pub mod dijkstra; pub mod jps; /// Converts the integer cost to an approximate floating point equivalent where cardinal directions have cost 1.0. From 5f6f34f2c87712ea015dfbb9b3f773cd0f5b93d6 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sat, 15 Nov 2025 16:28:47 +0100 Subject: [PATCH 30/41] Improved fuzz_distance, using A* to trace points where JPS paths suboptimally --- tests/fuzz_test.rs | 43 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index 68ca160..8d0b529 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -3,6 +3,7 @@ use grid_pathfinding::{ pathing_grid::PathingGrid, solver::{astar::AstarSolver, jps::JPSSolver, GridSolver}, + ALLOW_CORNER_CUTTING, }; use grid_util::*; use rand::prelude::*; @@ -69,8 +70,10 @@ fn fuzz() { #[test] fn fuzz_distance() { - const N: usize = 5; + const N: usize = 10; const N_GRIDS: usize = 10000; + let tolerance = 0.001; + let mut rng = StdRng::seed_from_u64(0); let astar_solver = AstarSolver::new(); @@ -98,17 +101,41 @@ fn fuzz_distance() { let astar_cost = astar_solver.get_path_cost_float(&astar_path, &random_grid); let jps_cost = jps_solver.get_path_cost_float(&jps_path, &random_grid); - if astar_cost >= 0.01 { + if astar_cost >= tolerance { let delta_dist = (jps_cost - astar_cost).abs() / astar_cost; - if delta_dist >= 0.01 { - println!("Astar distance: {astar_cost}; JPS distance: {jps_cost}"); - println!("Astar path: {astar_path:?}\n JPS path: {jps_path:?}\n"); + if delta_dist >= tolerance { + println!("Astar distance: {astar_cost:4}; JPS distance: {jps_cost:4}"); let grid_diag = random_grid.allow_diagonal_move; - println!("diagonal: {diagonal}; grid_diag: {grid_diag}; improved_pruning: {improved_pruning}"); + println!("diagonal: {diagonal}; grid_diag: {grid_diag}; improved_pruning: {improved_pruning}; corner_cutting: {ALLOW_CORNER_CUTTING}"); + + let mut problem_start: Point = start; + for (idx, &p) in jps_path.iter().enumerate().rev() { + let jps_suffix = &jps_path[idx..].to_vec(); + let jps_suffix_cost = + jps_solver.get_path_cost_float(jps_suffix, &random_grid); + let astar_suffix_path = astar_solver + .get_path_single_goal(&mut random_grid, p, end, false) + .expect("A* should find a path from intermediate JPS node"); + let astar_suffix_cost = + astar_solver.get_path_cost_float(&astar_suffix_path, &random_grid); - visualize_grid(&random_grid, &start, &end); + let rel_diff = (jps_suffix_cost - astar_suffix_cost).abs() + / astar_suffix_cost.max(1e-6); + + // First point where JPS suffix is no longer optimal + if rel_diff >= tolerance { + println!("=> First suboptimal JPS step at index {idx}, point {p:?}"); + println!("- JPS suffix from here: {jps_suffix:?}"); + println!("- A* optimal suffix: {astar_suffix_path:?}"); + problem_start = p; + break; + } + } + + visualize_grid(&random_grid, &problem_start, &end); } - assert!(delta_dist < 0.01); + + assert!(delta_dist < tolerance); } } } From 60970d4a09770420b8aa606fb9867bffa0dcb638 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 10:48:48 +0100 Subject: [PATCH 31/41] Introduced Frontier trait, abstracting over frontier priority queue implementation --- src/astar_jps.rs | 43 ++++++++++++++++++++++++++++++++++++------- src/lib.rs | 6 ++++-- src/pathing_grid.rs | 6 ++++-- tests/fuzz_test.rs | 4 +++- 4 files changed, 47 insertions(+), 12 deletions(-) diff --git a/src/astar_jps.rs b/src/astar_jps.rs index acd286a..45d8d7c 100644 --- a/src/astar_jps.rs +++ b/src/astar_jps.rs @@ -17,7 +17,7 @@ use std::hash::Hash; #[derive(Clone, Debug)] -struct SearchNode { +pub(crate) struct SearchNode { estimated_cost: K, cost: K, index: usize, @@ -68,21 +68,50 @@ where path } +pub trait Frontier: Default +where + C: Zero + Ord + Copy, +{ + fn clear(&mut self); + fn pop(&mut self) -> Option>; + fn push(&mut self, item: SearchNode); +} + +impl Frontier for BinaryHeap> +where + C: Zero + Ord + Copy, +{ + fn clear(&mut self) { + self.clear(); + } + + fn pop(&mut self) -> Option> { + self.pop() + } + + fn push(&mut self, item: SearchNode) { + self.push(item); + } +} + /// [AstarContext] represents the search fringe and node parent map, facilitating reuse of memory allocations. #[derive(Clone, Debug)] -pub struct SearchContext { - fringe: BinaryHeap>, +pub struct SearchContext { + fringe: F, parents: FxIndexMap, } -impl SearchContext +pub type DefaultSearchContext = SearchContext>>; + +impl SearchContext where N: Eq + Hash + Clone, C: Zero + Ord + Copy, + F: Frontier, { - pub fn new() -> SearchContext { + pub fn new() -> SearchContext { SearchContext { - fringe: BinaryHeap::new(), + fringe: F::default(), parents: FxIndexMap::default(), } } @@ -174,6 +203,6 @@ where FH: FnMut(&N) -> C, FS: FnMut(&N) -> bool, { - let mut search = SearchContext::new(); + let mut search = DefaultSearchContext::new(); search.astar_jps(start, successors, heuristic, success) } diff --git a/src/lib.rs b/src/lib.rs index 6448ebb..0c39ce0 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,10 @@ use smallvec::SmallVec; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; +use crate::astar_jps::DefaultSearchContext; + +pub const ALLOW_CORNER_CUTTING: bool = true; const EQUAL_EDGE_COST: bool = false; -const ALLOW_CORNER_CUTTING: bool = false; const GRAPH_PRUNING: bool = true; const N_SMALLVEC_SIZE: usize = 8; @@ -79,7 +81,7 @@ pub struct Pathfinder { pub heuristic_factor: f32, pub improved_pruning: bool, pub allow_diagonal_move: bool, - context: Arc>>, + context: Arc>>, } impl Default for Pathfinder { diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs index c9fcf6b..a3010a1 100644 --- a/src/pathing_grid.rs +++ b/src/pathing_grid.rs @@ -1,6 +1,8 @@ +use crate::astar_jps::DefaultSearchContext; + use super::*; use core::fmt; -use grid_util::grid::{BoolGrid, SimpleValueGrid, ValueGrid}; +use grid_util::grid::{BoolGrid, ValueGrid}; use grid_util::point::Point; use petgraph::unionfind::UnionFind; use smallvec::SmallVec; @@ -12,7 +14,7 @@ pub struct PathingGrid { pub components: UnionFind, pub components_dirty: bool, pub allow_diagonal_move: bool, - pub(crate) context: Arc>>, + pub(crate) context: Arc>>, } impl Default for PathingGrid { diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index 8d0b529..e09c02d 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -124,7 +124,9 @@ fn fuzz_distance() { // First point where JPS suffix is no longer optimal if rel_diff >= tolerance { - println!("=> First suboptimal JPS step at index {idx}, point {p:?}"); + println!( + "=> First suboptimal JPS step at index {idx}, point {p:?}" + ); println!("- JPS suffix from here: {jps_suffix:?}"); println!("- A* optimal suffix: {astar_suffix_path:?}"); problem_start = p; From 0482a52486e97f0e0e090701e39d1bd025e5181b Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 12:24:32 +0100 Subject: [PATCH 32/41] Added priority_queue::PriorityQueue based Frontier implementation, making it the default replacing the BinaryHeap based implementation --- Cargo.lock | 12 ++++++++++++ Cargo.toml | 1 + src/astar_jps.rs | 24 ++++++++++++++++++++++-- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21ae8a9..d5f9c1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,6 +347,7 @@ dependencies = [ "log", "num-traits", "petgraph", + "priority-queue", "rand 0.9.2", "smallvec", ] @@ -601,6 +602,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "priority-queue" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" +dependencies = [ + "equivalent", + "indexmap 2.12.0", + "serde", +] + [[package]] name = "proc-macro-crate" version = "3.4.0" diff --git a/Cargo.toml b/Cargo.toml index e99b9fb..56f511b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ num-traits = "0.2" grid_util = { version = "0.2", features = ["smallvec"]} log = "0.4" smallvec = "1.13.2" +priority-queue = "2.7.0" [lib] bench = false diff --git a/src/astar_jps.rs b/src/astar_jps.rs index 45d8d7c..9e54184 100644 --- a/src/astar_jps.rs +++ b/src/astar_jps.rs @@ -6,6 +6,7 @@ use fxhash::FxBuildHasher; use indexmap::map::Entry::{Occupied, Vacant}; use indexmap::IndexMap; use num_traits::Zero; +use priority_queue::PriorityQueue; type FxIndexMap = IndexMap; @@ -94,6 +95,23 @@ where } } +impl Frontier for PriorityQueue> +where + C: Zero + Ord + Copy + Hash, +{ + fn clear(&mut self) { + self.clear(); + } + + fn pop(&mut self) -> Option> { + self.pop().map(|x| x.1) + } + + fn push(&mut self, item: SearchNode) { + self.push(item.estimated_cost, item); + } +} + /// [AstarContext] represents the search fringe and node parent map, facilitating reuse of memory allocations. #[derive(Clone, Debug)] pub struct SearchContext { @@ -101,7 +119,9 @@ pub struct SearchContext { parents: FxIndexMap, } -pub type DefaultSearchContext = SearchContext>>; +pub type BinaryHeapSearchContext = SearchContext>>; +pub type PQSearchContext = SearchContext>>; +pub type DefaultSearchContext = PQSearchContext; impl SearchContext where @@ -197,7 +217,7 @@ pub fn astar_jps( ) -> Option<(Vec, C)> where N: Eq + Hash + Clone, - C: Zero + Ord + Copy, + C: Zero + Ord + Copy + Hash, FN: FnMut(&Option<&N>, &N) -> IN, IN: IntoIterator, FH: FnMut(&N) -> C, From f352c0361d43f5f1ed1c661e650306a7dc647556 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 20:19:49 +0100 Subject: [PATCH 33/41] Removed priority_queue based PQSearchContext again as it appeared to be broken --- Cargo.lock | 12 ------------ Cargo.toml | 1 - src/astar_jps.rs | 21 +-------------------- 3 files changed, 1 insertion(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d5f9c1f..21ae8a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,7 +347,6 @@ dependencies = [ "log", "num-traits", "petgraph", - "priority-queue", "rand 0.9.2", "smallvec", ] @@ -602,17 +601,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "priority-queue" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93980406f12d9f8140ed5abe7155acb10bb1e69ea55c88960b9c2f117445ef96" -dependencies = [ - "equivalent", - "indexmap 2.12.0", - "serde", -] - [[package]] name = "proc-macro-crate" version = "3.4.0" diff --git a/Cargo.toml b/Cargo.toml index 56f511b..e99b9fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,6 @@ num-traits = "0.2" grid_util = { version = "0.2", features = ["smallvec"]} log = "0.4" smallvec = "1.13.2" -priority-queue = "2.7.0" [lib] bench = false diff --git a/src/astar_jps.rs b/src/astar_jps.rs index 9e54184..507ac06 100644 --- a/src/astar_jps.rs +++ b/src/astar_jps.rs @@ -6,7 +6,6 @@ use fxhash::FxBuildHasher; use indexmap::map::Entry::{Occupied, Vacant}; use indexmap::IndexMap; use num_traits::Zero; -use priority_queue::PriorityQueue; type FxIndexMap = IndexMap; @@ -95,23 +94,6 @@ where } } -impl Frontier for PriorityQueue> -where - C: Zero + Ord + Copy + Hash, -{ - fn clear(&mut self) { - self.clear(); - } - - fn pop(&mut self) -> Option> { - self.pop().map(|x| x.1) - } - - fn push(&mut self, item: SearchNode) { - self.push(item.estimated_cost, item); - } -} - /// [AstarContext] represents the search fringe and node parent map, facilitating reuse of memory allocations. #[derive(Clone, Debug)] pub struct SearchContext { @@ -120,8 +102,7 @@ pub struct SearchContext { } pub type BinaryHeapSearchContext = SearchContext>>; -pub type PQSearchContext = SearchContext>>; -pub type DefaultSearchContext = PQSearchContext; +pub type DefaultSearchContext = BinaryHeapSearchContext; impl SearchContext where From 1cf9ab7c4db2a5b3aeb810a0cab2335588e3c078 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 23:00:52 +0100 Subject: [PATCH 34/41] Implemented simple bool const generic for toggling diagonals --- benches/comparison_bench.rs | 70 ++++++------ benches/single_bench.rs | 11 +- src/lib.rs | 1 + src/pathing_grid.rs | 45 ++++---- src/solver/astar.rs | 213 +++++++++++++++++++++-------------- src/solver/dijkstra.rs | 15 ++- src/solver/jps.rs | 57 +++++++--- src/solver/mod.rs | 48 +++++--- tests/benchmark_distances.rs | 6 +- tests/fuzz_test.rs | 68 ++++++++--- 10 files changed, 326 insertions(+), 208 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 87c5696..d98f146 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -38,23 +38,22 @@ fn dao_bench(c: &mut Criterion) { } } -fn dao_bench_jps(c: &mut Criterion) { - for (allow_diag, pruning) in [(true, false)] { - let bench_set = if allow_diag { +fn dao_bench_jps(c: &mut Criterion) { + for (pruning) in [false] { + let bench_set = if ALLOW_DIAGONAL { ["dao/arena", "dao/den312d", "dao/arena2"] } else { ["dao/arena", "dao/den009d", "dao/den312d"] }; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = + let mut pathing_grid: PathingGrid = PathingGrid::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.generate_components(); let mut solver = JPSSolver::new(&pathing_grid, pruning); solver.initialize(&pathing_grid); - let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; + let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; let improved_str = if pruning { " (improved pruning)" } else { "" }; c.bench_function( @@ -75,37 +74,36 @@ fn dao_bench_jps(c: &mut Criterion) { } } } -fn dao_bench_astar(c: &mut Criterion) { - for allow_diag in [false, true] { - let bench_set = if allow_diag { - ["dao/arena", "dao/den312d", "dao/arena2"] - } else { - ["dao/arena", "dao/den009d", "dao/den312d"] - }; - for name in bench_set { - let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); - pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.generate_components(); - let solver = AstarSolver::new(); - let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; +fn dao_bench_astar(c: &mut Criterion) { + let bench_set = if ALLOW_DIAGONAL { + ["dao/arena", "dao/den312d", "dao/arena2"] + } else { + ["dao/arena", "dao/den009d", "dao/den312d"] + }; + for name in bench_set { + let (bool_grid, scenarios) = get_benchmark(name.to_owned()); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; - c.bench_function(format!("{name}, A* {diag_str}").as_str(), |b| { - b.iter(|| { - for (start, end, _) in &scenarios { - black_box(solver.get_path_single_goal( - &mut pathing_grid, - *start, - *end, - false, - )); - } - }) - }); - } + c.bench_function(format!("{name}, A* {diag_str}").as_str(), |b| { + b.iter(|| { + for (start, end, _) in &scenarios { + black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end, false)); + } + }) + }); } } -criterion_group!(benches, dao_bench, dao_bench_jps, dao_bench_astar); +criterion_group!( + benches, + dao_bench, + dao_bench_jps, + dao_bench_jps, + dao_bench_astar, + dao_bench_astar +); criterion_main!(benches); diff --git a/benches/single_bench.rs b/benches/single_bench.rs index cfcd3de..74aa3cb 100644 --- a/benches/single_bench.rs +++ b/benches/single_bench.rs @@ -7,19 +7,18 @@ use grid_pathfinding_benchmark::*; use grid_util::grid::ValueGrid; use std::hint::black_box; -fn dao_bench_single(c: &mut Criterion) { - for (allow_diag, pruning) in [(true, false)] { +fn dao_bench_single(c: &mut Criterion) { + for pruning in [false] { let bench_set = ["dao/arena"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = + let mut pathing_grid: PathingGrid = PathingGrid::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.generate_components(); let mut solver = JPSSolver::new(&pathing_grid, pruning); solver.initialize(&pathing_grid); - let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; + let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; let improved_str = if pruning { " (improved pruning)" } else { "" }; c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { @@ -38,5 +37,5 @@ fn dao_bench_single(c: &mut Criterion) { } } -criterion_group!(benches, dao_bench_single); +criterion_group!(benches, dao_bench_single, dao_bench_single); criterion_main!(benches); diff --git a/src/lib.rs b/src/lib.rs index 0c39ce0..17b16ad 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ //! pathfinding. Note that this assumes a uniform-cost grid. Pre-computes //! [connected components](https://en.wikipedia.org/wiki/Component_(graph_theory)) //! to avoid flood-filling behaviour if no path exists. + mod astar_jps; pub mod pathing_grid; pub mod solver; diff --git a/src/pathing_grid.rs b/src/pathing_grid.rs index a3010a1..b13be3e 100644 --- a/src/pathing_grid.rs +++ b/src/pathing_grid.rs @@ -9,29 +9,28 @@ use smallvec::SmallVec; use std::sync::{Arc, Mutex}; #[derive(Clone, Debug)] -pub struct PathingGrid { +pub struct PathingGrid { pub grid: BoolGrid, pub components: UnionFind, pub components_dirty: bool, - pub allow_diagonal_move: bool, pub(crate) context: Arc>>, } -impl Default for PathingGrid { - fn default() -> PathingGrid { +impl Default for PathingGrid { + fn default() -> PathingGrid { let grid = PathingGrid { grid: BoolGrid::default(), components: UnionFind::new(0), components_dirty: false, - allow_diagonal_move: true, context: Arc::new(Mutex::new(SearchContext::new())), }; grid } } -impl PathingGrid { + +impl PathingGrid { pub fn neighborhood_points(&self, point: &Point) -> SmallVec<[Point; 8]> { - if self.allow_diagonal_move { + if ALLOW_DIAGONAL { point.moore_neighborhood_smallvec() } else { point.neumann_neighborhood_smallvec() @@ -48,6 +47,7 @@ impl PathingGrid { .map(move |p| (p, (pos.dir_obj(&p).num() % 2) * (D - C) + C)) .collect::>() } + pub fn can_move_to(&self, pos: Point, start: Point) -> bool { if ALLOW_CORNER_CUTTING { self.can_move_to_simple(pos) @@ -131,7 +131,7 @@ impl PathingGrid { let point = Point::new(x, y); let parent_ix = self.grid.get_ix_point(&point); - if self.allow_diagonal_move { + if ALLOW_DIAGONAL { vec![ Point::new(point.x, point.y + 1), Point::new(point.x, point.y - 1), @@ -160,7 +160,7 @@ impl PathingGrid { } } } -impl fmt::Display for PathingGrid { +impl fmt::Display for PathingGrid { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Grid:")?; for y in 0..self.grid.height as i32 { @@ -173,14 +173,13 @@ impl fmt::Display for PathingGrid { } } -impl ValueGrid for PathingGrid { +impl ValueGrid for PathingGrid { fn new(width: usize, height: usize, default_value: bool) -> Self { let base_grid = PathingGrid { grid: BoolGrid::new(width, height, default_value), components: UnionFind::new(width * height), components_dirty: false, - allow_diagonal_move: true, context: Arc::new(Mutex::new(SearchContext::new())), }; base_grid @@ -224,7 +223,7 @@ mod tests { // | # | // | # | // ___ - let mut path_graph = PathingGrid::new(3, 2, false); + let mut path_graph: PathingGrid = PathingGrid::new(3, 2, false); path_graph.grid.set(1, 0, true); path_graph.grid.set(1, 1, true); let f_ix = |p| path_graph.get_ix_point(p); @@ -244,7 +243,7 @@ mod tests { #[test] fn reachable_with_diagonals() { - let mut path_graph = PathingGrid::new(3, 2, false); + let mut path_graph: PathingGrid = PathingGrid::new(3, 2, false); path_graph.grid.set(1, 0, true); path_graph.grid.set(1, 1, true); let p1 = Point::new(0, 0); @@ -266,8 +265,7 @@ mod tests { // | # | // | G| // ___ - let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); - pathing_grid.allow_diagonal_move = false; + let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); let start = Point::new(0, 0); @@ -281,14 +279,15 @@ mod tests { // | #| // |# | // __ - let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); - pathing_grid.allow_diagonal_move = false; - let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); - for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { - pathing_grid.set(0, 0, false); - pathing_grid.set(1, 1, false); - pathing_grid.generate_components(); - } + let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); + let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); + + pathing_grid.set(0, 0, false); + pathing_grid.set(1, 1, false); + pathing_grid.generate_components(); + pathing_grid_diag.set(0, 0, false); + pathing_grid_diag.set(1, 1, false); + pathing_grid_diag.generate_components(); let start = Point::new(0, 0); let end = Point::new(1, 1); assert!(pathing_grid.unreachable(&start, &end)); diff --git a/src/solver/astar.rs b/src/solver/astar.rs index ead6707..28ffb49 100644 --- a/src/solver/astar.rs +++ b/src/solver/astar.rs @@ -19,9 +19,9 @@ impl AstarSolver { impl GridSolver for AstarSolver { type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; - fn successors( + fn successors( &self, - grid: &PathingGrid, + grid: &PathingGrid, _parent: Option<&Point>, node: &Point, _goal: &F, @@ -33,7 +33,12 @@ impl GridSolver for AstarSolver { } /// Just the normal cost times a heuristic factor. - fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + fn heuristic( + &self, + grid: &PathingGrid, + p1: &Point, + p2: &Point, + ) -> i32 { (self.cost(grid, p1, p2) as f32 * self.heuristic_factor) as i32 } } @@ -49,94 +54,134 @@ mod tests { /// Asserts that the case in which start and goal are equal is handled correctly. #[test] fn equal_start_goal() { - for allow_diag in [false, true] { - let mut pathing_grid: PathingGrid = PathingGrid::new(1, 1, false); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.generate_components(); - let solver = AstarSolver::new(); - let start = Point::new(0, 0); - let path = solver - .get_path_single_goal(&mut pathing_grid, start, start, false) - .unwrap(); - assert!(path.len() == 1); - } + let mut pathing_grid: PathingGrid = PathingGrid::new(1, 1, false); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let start = Point::new(0, 0); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, start, false) + .unwrap(); + assert!(path.len() == 1); + } + + /// Asserts that the case in which start and goal are equal is handled correctly. + #[test] + fn equal_start_goal_diagonal() { + let mut pathing_grid: PathingGrid = PathingGrid::new(1, 1, false); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let start = Point::new(0, 0); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, start, false) + .unwrap(); + assert!(path.len() == 1); } /// Asserts that the optimal 4 step solution is found. #[test] fn solve_simple_problem() { - let arr = if ALLOW_CORNER_CUTTING { - [(false, 5), (true, 4)] - } else { - [(false, 5), (true, 5)] - }; - for (allow_diag, expected) in arr { - let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.set(1, 1, true); - pathing_grid.generate_components(); - let solver = AstarSolver::new(); - - let start = Point::new(0, 0); - let end = Point::new(2, 2); - let path = solver - .get_path_single_goal(&mut pathing_grid, start, end, false) - .unwrap(); - assert!(path.len() == expected); - } + let expected = 5; + let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + pathing_grid.set(1, 1, true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + + let start = Point::new(0, 0); + let end = Point::new(2, 2); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, end, false) + .unwrap(); + assert!(path.len() == expected); + } + + /// Asserts that the optimal 4 step solution is found. + #[test] + fn solve_simple_problem_diagonal() { + let expected = if ALLOW_CORNER_CUTTING { 4 } else { 5 }; + let mut pathing_grid: PathingGrid = PathingGrid::new(3, 3, false); + pathing_grid.set(1, 1, true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + + let start = Point::new(0, 0); + let end = Point::new(2, 2); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, end, false) + .unwrap(); + assert!(path.len() == expected); } #[test] fn test_multiple_goals() { - let arr = if ALLOW_CORNER_CUTTING { - [(false, 7), (true, 5)] - } else { - [(false, 7), (true, 6)] - }; - for (allow_diag, expected) in arr { - let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.set(1, 1, true); - pathing_grid.generate_components(); - let solver = AstarSolver::new(); - let start = Point::new(0, 0); - let goal_1 = Point::new(4, 4); - let goal_2 = Point::new(3, 3); - let goals = vec![&goal_1, &goal_2]; - let (selected_goal, path) = solver - .get_path_multiple_goals(&mut pathing_grid, start, goals) - .unwrap(); - assert_eq!(selected_goal, Point::new(3, 3)); - assert!(path.len() == expected); - } + let expected = 7; + let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); + pathing_grid.set(1, 1, true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let start = Point::new(0, 0); + let goal_1 = Point::new(4, 4); + let goal_2 = Point::new(3, 3); + let goals = vec![&goal_1, &goal_2]; + let (selected_goal, path) = solver + .get_path_multiple_goals(&mut pathing_grid, start, goals) + .unwrap(); + assert_eq!(selected_goal, Point::new(3, 3)); + assert!(path.len() == expected); } + #[test] + fn test_multiple_goal_diagonal() { + let expected = if ALLOW_CORNER_CUTTING { 5 } else { 6 }; + let mut pathing_grid: PathingGrid = PathingGrid::new(5, 5, false); + pathing_grid.set(1, 1, true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let start = Point::new(0, 0); + let goal_1 = Point::new(4, 4); + let goal_2 = Point::new(3, 3); + let goals = vec![&goal_1, &goal_2]; + let (selected_goal, path) = solver + .get_path_multiple_goals(&mut pathing_grid, start, goals) + .unwrap(); + assert_eq!(selected_goal, Point::new(3, 3)); + assert!(path.len() == expected); + } #[test] fn test_complex() { - let arr = if ALLOW_CORNER_CUTTING { - [(false, 15), (true, 10)] - } else { - [(false, 15), (true, 11)] - }; - for (allow_diag, expected) in arr { - let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); - pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); - pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); - pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); - pathing_grid.set_rect(Rect::new(8, 8, 1, 1), true); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.generate_components(); - let solver = AstarSolver::new(); - - let start = Point::new(0, 0); - let end = Point::new(7, 7); - let path = solver - .get_path_single_goal(&mut pathing_grid, start, end, false) - .unwrap(); - assert!(path.len() == expected); - } + let expected = 15; + let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); + pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); + pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); + pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); + pathing_grid.set_rect(Rect::new(8, 8, 1, 1), true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + + let start = Point::new(0, 0); + let end = Point::new(7, 7); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, end, false) + .unwrap(); + assert!(path.len() == expected); } + #[test] + fn test_complex_diagonal() { + let expected = if ALLOW_CORNER_CUTTING { 10 } else { 11 }; + let mut pathing_grid: PathingGrid = PathingGrid::new(10, 10, false); + pathing_grid.set_rect(Rect::new(1, 1, 1, 1), true); + pathing_grid.set_rect(Rect::new(5, 0, 1, 1), true); + pathing_grid.set_rect(Rect::new(0, 5, 1, 1), true); + pathing_grid.set_rect(Rect::new(8, 8, 1, 1), true); + pathing_grid.generate_components(); + let solver = AstarSolver::new(); + let start = Point::new(0, 0); + let end = Point::new(7, 7); + let path = solver + .get_path_single_goal(&mut pathing_grid, start, end, false) + .unwrap(); + assert!(path.len() == expected); + } // Tests whether allowing diagonals has the expected effect on path existence in a minimal setting. #[test] fn test_diagonal_switch_path() { @@ -144,14 +189,14 @@ mod tests { // | #| // |# | // __ - let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); - pathing_grid.allow_diagonal_move = false; - let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); - for pathing_grid in [&mut pathing_grid, &mut pathing_grid_diag] { - pathing_grid.set(0, 0, false); - pathing_grid.set(1, 1, false); - pathing_grid.generate_components(); - } + let mut pathing_grid: PathingGrid = PathingGrid::new(2, 2, true); + let mut pathing_grid_diag: PathingGrid = PathingGrid::new(2, 2, true); + pathing_grid.set(0, 0, false); + pathing_grid.set(1, 1, false); + pathing_grid.generate_components(); + pathing_grid_diag.set(0, 0, false); + pathing_grid_diag.set(1, 1, false); + pathing_grid_diag.generate_components(); let solver = AstarSolver::new(); let start = Point::new(0, 0); diff --git a/src/solver/dijkstra.rs b/src/solver/dijkstra.rs index 4a15990..a45a3e3 100644 --- a/src/solver/dijkstra.rs +++ b/src/solver/dijkstra.rs @@ -9,12 +9,12 @@ pub struct DijkstraSolver; impl GridSolver for DijkstraSolver { type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; - fn successors( + fn successors( &self, - grid: &PathingGrid, - _parent: Option<&Point>, + grid: &PathingGrid, + _: Option<&Point>, node: &Point, - _goal: &F, + _: &F, ) -> Self::Successors where F: Fn(&Point) -> bool, @@ -23,7 +23,12 @@ impl GridSolver for DijkstraSolver { } /// Just the cost times a heuristic factor. - fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + fn heuristic( + &self, + _: &PathingGrid, + _: &Point, + _: &Point, + ) -> i32 { 0 } } diff --git a/src/solver/jps.rs b/src/solver/jps.rs index f90569d..f7970a0 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -16,9 +16,9 @@ pub struct JPSSolver { impl GridSolver for JPSSolver { type Successors = SmallVec<[(Point, i32); N_SMALLVEC_SIZE]>; - fn successors( + fn successors( &self, - grid: &PathingGrid, + grid: &PathingGrid, parent: Option<&Point>, node: &Point, goal: &F, @@ -61,12 +61,20 @@ impl GridSolver for JPSSolver { } /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. - fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { + fn heuristic( + &self, + grid: &PathingGrid, + p1: &Point, + p2: &Point, + ) -> i32 { self.cost(grid, p1, p2) } } impl JPSSolver { - pub fn new(grid: &PathingGrid, improved_pruning: bool) -> JPSSolver { + pub fn new( + grid: &PathingGrid, + improved_pruning: bool, + ) -> JPSSolver { let mut solver = JPSSolver { jump_point: SimpleValueGrid::new(grid.width(), grid.height(), 0), neighbours: SimpleValueGrid::new(grid.width(), grid.height(), 0b11111111), @@ -131,16 +139,16 @@ impl JPSSolver { forced_mask } - fn pruned_neighborhood<'a>( + fn pruned_neighborhood<'a, const ALLOW_DIAGONAL: bool>( &self, dir: Direction, node: &'a Point, - grid: &PathingGrid, + grid: &PathingGrid, ) -> impl Iterator + 'a { let dir_num = dir.num(); let mut n_mask: u8; let mut neighbours = self.neighbours.get_point(*node); - if !grid.allow_diagonal_move { + if !ALLOW_DIAGONAL { neighbours &= 0b01010101; n_mask = 0b01000101_u8.rotate_left(dir_num as u32); } else if dir.diagonal() { @@ -168,7 +176,7 @@ impl JPSSolver { } let comb_mask = neighbours & n_mask; (0..8) - .step_by(if grid.allow_diagonal_move { 1 } else { 2 }) + .step_by(if ALLOW_DIAGONAL { 1 } else { 2 }) .filter(move |x| comb_mask & (1 << *x) != 0) // (dir_num % 2) * (D-C) + C) // is an optimized version without a conditional of @@ -177,13 +185,13 @@ impl JPSSolver { } /// Straight jump in a cardinal direction. - fn jump_straight( + fn jump_straight( &self, mut initial: Point, mut cost: i32, direction: Direction, goal: &F, - grid: &PathingGrid, + grid: &PathingGrid, ) -> Option<(Point, i32)> where F: Fn(&Point) -> bool, @@ -205,13 +213,13 @@ impl JPSSolver { } /// Performs the jumping of node neighbours, skipping over unnecessary nodes until a goal or a forced node is found. - fn jump( + fn jump( &self, mut initial: Point, mut cost: i32, direction: Direction, goal: &F, - grid: &PathingGrid, + grid: &PathingGrid, ) -> Option<(Point, i32)> where F: Fn(&Point) -> bool, @@ -241,8 +249,8 @@ impl JPSSolver { // When using a 4-neighborhood (specified by setting allow_diagonal_move to false), // jumps perpendicular to the direction are performed. This is necessary to not miss the // goal when passing by. - if !grid.allow_diagonal_move || !ALLOW_CORNER_CUTTING && !direction.diagonal() { - if grid.allow_diagonal_move { + if !ALLOW_DIAGONAL || !ALLOW_CORNER_CUTTING && !direction.diagonal() { + if ALLOW_DIAGONAL { let diag_1 = direction.rotate_ccw(1); let diag_2 = direction.rotate_cw(1); if self.jump(initial, 1, diag_1, goal, grid).is_some() @@ -269,7 +277,11 @@ impl JPSSolver { let value = self.forced_mask(&point); self.jump_point.set_point(point, value); } - pub fn fix_jumppoints(&mut self, point: Point, grid: &PathingGrid) { + pub fn fix_jumppoints( + &mut self, + point: Point, + grid: &PathingGrid, + ) { self.set_jumppoints(point); for p in grid.neighborhood_points(&point) { if grid.point_in_bounds(p) { @@ -288,20 +300,29 @@ impl JPSSolver { } /// Updates the neighbours and jumppoints - fn set(&mut self, x: i32, y: i32, blocked: bool, grid: &PathingGrid) { + fn set( + &mut self, + x: i32, + y: i32, + blocked: bool, + grid: &PathingGrid, + ) { let p = Point::new(x, y); self.update_neighbours(p.x, p.y, blocked); self.fix_jumppoints(p, grid); } - pub fn update_all_neighbours(&mut self, grid: &PathingGrid) { + pub fn update_all_neighbours( + &mut self, + grid: &PathingGrid, + ) { for x in 0..self.width() as i32 { for y in 0..self.height() as i32 { self.update_neighbours(x, y, grid.get(x, y)); } } } - pub fn initialize(&mut self, grid: &PathingGrid) { + pub fn initialize(&mut self, grid: &PathingGrid) { // Emulates 'placing' of blocked tile around map border to correctly initialize neighbours // and make behaviour of a map bordered by tiles the same as a borderless map. for i in -1..=(self.width() as i32) { diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 0e28757..5730cf2 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -13,11 +13,21 @@ pub fn convert_cost_to_unit_cost_float(cost: i32) -> f64 { pub trait GridSolver { type Successors: IntoIterator; - fn heuristic(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32; + fn heuristic( + &self, + grid: &PathingGrid, + p1: &Point, + p2: &Point, + ) -> i32; /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. - fn cost(&self, grid: &PathingGrid, p1: &Point, p2: &Point) -> i32 { - if grid.allow_diagonal_move { + fn cost( + &self, + grid: &PathingGrid, + p1: &Point, + p2: &Point, + ) -> i32 { + if ALLOW_DIAGONAL { let delta_x = (p1.x - p2.x).abs(); let delta_y = (p1.y - p2.y).abs(); // Formula from https://github.com/riscy/a_star_on_grids @@ -29,9 +39,9 @@ pub trait GridSolver { } } - fn successors( + fn successors( &self, - grid: &PathingGrid, + grid: &PathingGrid, parent: Option<&Point>, node: &Point, goal: &F, @@ -39,7 +49,11 @@ pub trait GridSolver { where F: Fn(&Point) -> bool; - fn get_path_cost(&self, path: &Vec, pathing_grid: &PathingGrid) -> i32 { + fn get_path_cost( + &self, + path: &Vec, + pathing_grid: &PathingGrid, + ) -> i32 { let mut v = path[0]; let n = path.len(); let mut total_cost_int = 0; @@ -51,12 +65,16 @@ pub trait GridSolver { } total_cost_int } - fn get_path_cost_float(&self, path: &Vec, pathing_grid: &PathingGrid) -> f64 { + fn get_path_cost_float( + &self, + path: &Vec, + pathing_grid: &PathingGrid, + ) -> f64 { convert_cost_to_unit_cost_float(self.get_path_cost(path, pathing_grid)) } - fn get_path_single_goal( + fn get_path_single_goal( &self, - grid: &mut PathingGrid, + grid: &mut PathingGrid, start: Point, goal: Point, approximate: bool, @@ -65,9 +83,9 @@ pub trait GridSolver { .map(waypoints_to_path) } /// The raw waypoints (jump points) from which [get_path_single_goal](Self::get_path_single_goal) makes a path. - fn get_waypoints_single_goal( + fn get_waypoints_single_goal( &self, - grid: &mut PathingGrid, + grid: &mut PathingGrid, start: Point, goal: Point, approximate: bool, @@ -108,9 +126,9 @@ pub trait GridSolver { .map(|(v, _c)| v) } /// Computes a path from the start to one of the given goals and returns the selected goal in addition to the found path. Otherwise behaves similar to [get_path_single_goal](Self::get_path_single_goal). - fn get_path_multiple_goals( + fn get_path_multiple_goals( &self, - grid: &mut PathingGrid, + grid: &mut PathingGrid, start: Point, goals: Vec<&Point>, ) -> Option<(Point, Vec)> { @@ -118,9 +136,9 @@ pub trait GridSolver { .map(|(x, y)| (x, waypoints_to_path(y))) } /// The raw waypoints (jump points) from which [get_path_multiple_goals](Self::get_path_multiple_goals) makes a path. - fn get_waypoints_multiple_goals( + fn get_waypoints_multiple_goals( &self, - grid: &mut PathingGrid, + grid: &mut PathingGrid, start: Point, goals: Vec<&Point>, ) -> Option<(Point, Vec)> { diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index 3600f72..087083e 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -19,11 +19,10 @@ fn verify_solution_distance_jps() { let bench_set = ["dao/arena", "dao/lak107d", "dao/den101d"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = + let mut pathing_grid: PathingGrid = PathingGrid::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = true; pathing_grid.generate_components(); let mut solver = JPSSolver::new(&pathing_grid, false); solver.initialize(&pathing_grid); @@ -51,10 +50,9 @@ fn verify_solution_distance_astar() { for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = + let mut pathing_grid: PathingGrid = PathingGrid::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = true; pathing_grid.generate_components(); for (start, end, distance) in &scenarios { println!("Start: {start}; End: {end}; Distance: {distance}"); diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index e09c02d..311e22b 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -7,10 +7,14 @@ use grid_pathfinding::{ }; use grid_util::*; use rand::prelude::*; +use smallvec::{smallvec, SmallVec}; -fn random_grid(w: usize, h: usize, rng: &mut StdRng, diagonal: bool) -> PathingGrid { - let mut pathing_grid: PathingGrid = PathingGrid::new(w, h, false); - pathing_grid.allow_diagonal_move = diagonal; +fn random_grid( + w: usize, + h: usize, + rng: &mut StdRng, +) -> PathingGrid { + let mut pathing_grid: PathingGrid = PathingGrid::new(w, h, false); for x in 0..pathing_grid.width() as i32 { for y in 0..pathing_grid.height() as i32 { pathing_grid.set(x, y, rng.random_bool(0.4)) @@ -20,7 +24,11 @@ fn random_grid(w: usize, h: usize, rng: &mut StdRng, diagonal: bool) -> PathingG pathing_grid } -fn visualize_grid(grid: &PathingGrid, start: &Point, end: &Point) { +fn visualize_grid( + grid: &PathingGrid, + start: &Point, + end: &Point, +) { let grid = &grid.grid; for y in (0..grid.height as i32).rev() { for x in 0..grid.width as i32 { @@ -39,15 +47,19 @@ fn visualize_grid(grid: &PathingGrid, start: &Point, end: &Point) { } } -#[test] -fn fuzz() { +fn reachable_fuzzer() { const N: usize = 10; const N_GRIDS: usize = 10000; let mut rng = StdRng::seed_from_u64(0); - for (diagonal, improved_pruning) in [(false, false), (true, false), (true, true)] { - let mut random_grids: Vec = Vec::new(); + let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { + smallvec![false, true] + } else { + smallvec![false] + }; + for improved_pruning in arr { + let mut random_grids: Vec> = Vec::new(); for _ in 0..N_GRIDS { - random_grids.push(random_grid(N, N, &mut rng, diagonal)) + random_grids.push(random_grid(N, N, &mut rng)) } let start = Point::new(0, 0); @@ -68,19 +80,22 @@ fn fuzz() { } } -#[test] -fn fuzz_distance() { +fn distance_fuzzer() { const N: usize = 10; const N_GRIDS: usize = 10000; let tolerance = 0.001; let mut rng = StdRng::seed_from_u64(0); let astar_solver = AstarSolver::new(); - - for (diagonal, improved_pruning) in [(false, false), (true, false)] { - let mut random_grids: Vec = Vec::new(); + let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { + smallvec![false, true] + } else { + smallvec![false] + }; + for improved_pruning in arr { + let mut random_grids: Vec> = Vec::new(); for _ in 0..N_GRIDS { - random_grids.push(random_grid(N, N, &mut rng, diagonal)) + random_grids.push(random_grid(N, N, &mut rng)) } let start = Point::new(0, 0); @@ -105,8 +120,7 @@ fn fuzz_distance() { let delta_dist = (jps_cost - astar_cost).abs() / astar_cost; if delta_dist >= tolerance { println!("Astar distance: {astar_cost:4}; JPS distance: {jps_cost:4}"); - let grid_diag = random_grid.allow_diagonal_move; - println!("diagonal: {diagonal}; grid_diag: {grid_diag}; improved_pruning: {improved_pruning}; corner_cutting: {ALLOW_CORNER_CUTTING}"); + println!("diagonal: {ALLOW_DIAGONAL}; improved_pruning: {improved_pruning}; corner_cutting: {ALLOW_CORNER_CUTTING}"); let mut problem_start: Point = start; for (idx, &p) in jps_path.iter().enumerate().rev() { @@ -143,3 +157,23 @@ fn fuzz_distance() { } } } + +#[test] +fn fuzz_reachable() { + reachable_fuzzer::() +} + +#[test] +fn fuzz_reachable_diagonal() { + reachable_fuzzer::() +} + +#[test] +fn fuzz_distance() { + distance_fuzzer::() +} + +#[test] +fn fuzz_distance_diagonal() { + distance_fuzzer::() +} From 5f106cbad8b34f2e0c0d81f94ae5a84f1c8f0921 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 23:07:39 +0100 Subject: [PATCH 35/41] Added dao_bench_dijkstra --- benches/comparison_bench.rs | 38 ++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 87c5696..122b210 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -1,7 +1,7 @@ use criterion::{criterion_group, criterion_main, Criterion}; use grid_pathfinding::{ pathing_grid::PathingGrid, - solver::{astar::AstarSolver, jps::JPSSolver, GridSolver}, + solver::{astar::AstarSolver, dijkstra::DijkstraSolver, jps::JPSSolver, GridSolver}, Pathfinder, }; use grid_pathfinding_benchmark::*; @@ -77,11 +77,7 @@ fn dao_bench_jps(c: &mut Criterion) { } fn dao_bench_astar(c: &mut Criterion) { for allow_diag in [false, true] { - let bench_set = if allow_diag { - ["dao/arena", "dao/den312d", "dao/arena2"] - } else { - ["dao/arena", "dao/den009d", "dao/den312d"] - }; + let bench_set = ["dao/arena", "dao/den009d", "dao/den312d"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); let mut pathing_grid: PathingGrid = @@ -107,5 +103,33 @@ fn dao_bench_astar(c: &mut Criterion) { } } } -criterion_group!(benches, dao_bench, dao_bench_jps, dao_bench_astar); +fn dao_bench_dijkstra(c: &mut Criterion) { + for allow_diag in [false, true] { + let bench_set = ["dao/arena", "dao/den009d", "dao/den312d"]; + for name in bench_set { + let (bool_grid, scenarios) = get_benchmark(name.to_owned()); + let mut pathing_grid: PathingGrid = + PathingGrid::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.allow_diagonal_move = allow_diag; + pathing_grid.generate_components(); + let solver = DijkstraSolver; + let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; + + c.bench_function(format!("{name}, Dijkstra {diag_str}").as_str(), |b| { + b.iter(|| { + for (start, end, _) in &scenarios { + black_box(solver.get_path_single_goal( + &mut pathing_grid, + *start, + *end, + false, + )); + } + }) + }); + } + } +} +criterion_group!(benches, dao_bench, dao_bench_jps, dao_bench_dijkstra, dao_bench_astar); criterion_main!(benches); From 8ccab603d88f9f0bd81a646d9488b30e288eb802 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 23:28:50 +0100 Subject: [PATCH 36/41] Added ALLOW_DIAGONAL const generic to Pathfinder also to keep comparison with new JPS fair --- benches/comparison_bench.rs | 38 +++++++++++++------------------ examples/benchmark_runner.rs | 40 ++++++++++++++++----------------- examples/heuristic_factor.rs | 2 +- examples/multiple_goals.rs | 2 +- examples/paths_and_waypoints.rs | 2 +- examples/simple_4.rs | 3 +-- examples/simple_8.rs | 2 +- src/lib.rs | 27 ++++++++++------------ 8 files changed, 53 insertions(+), 63 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 126da09..a88aa13 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -6,25 +6,26 @@ use grid_pathfinding::{ }; use grid_pathfinding_benchmark::*; use grid_util::grid::ValueGrid; +use smallvec::{smallvec, SmallVec}; use std::hint::black_box; -fn dao_bench(c: &mut Criterion) { - for (allow_diag, pruning) in [(true, false)] { - let bench_set = if allow_diag { - ["dao/arena", "dao/den312d", "dao/arena2"] - } else { - ["dao/arena", "dao/den009d", "dao/den312d"] - }; +fn dao_bench(c: &mut Criterion) { + let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { + smallvec![false, true] + } else { + smallvec![false] + }; + for pruning in arr { + let bench_set = ["dao/arena", "dao/den312d", "dao/arena2"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: Pathfinder = + let mut pathing_grid: Pathfinder = Pathfinder::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = allow_diag; pathing_grid.improved_pruning = pruning; pathing_grid.initialize(); pathing_grid.generate_components(); - let diag_str = if allow_diag { "8-grid" } else { "4-grid" }; + let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; let improved_str = if pruning { " (improved pruning)" } else { "" }; c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { @@ -39,12 +40,8 @@ fn dao_bench(c: &mut Criterion) { } fn dao_bench_jps(c: &mut Criterion) { - for (pruning) in [false] { - let bench_set = if ALLOW_DIAGONAL { - ["dao/arena", "dao/den312d", "dao/arena2"] - } else { - ["dao/arena", "dao/den009d", "dao/den312d"] - }; + for pruning in [false, true] { + let bench_set = ["dao/arena", "dao/den312d", "dao/arena2"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); let mut pathing_grid: PathingGrid = @@ -75,11 +72,7 @@ fn dao_bench_jps(c: &mut Criterion) { } } fn dao_bench_astar(c: &mut Criterion) { - let bench_set = if ALLOW_DIAGONAL { - ["dao/arena", "dao/den312d", "dao/arena2"] - } else { - ["dao/arena", "dao/den009d", "dao/den312d"] - }; + let bench_set = ["dao/arena", "dao/den009d", "dao/den312d"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); let mut pathing_grid: PathingGrid = @@ -121,7 +114,8 @@ fn dao_bench_dijkstra(c: &mut Criterion) { criterion_group!( benches, - dao_bench, + dao_bench, + dao_bench, dao_bench_jps, dao_bench_jps, dao_bench_astar, diff --git a/examples/benchmark_runner.rs b/examples/benchmark_runner.rs index 747c87d..c23746b 100644 --- a/examples/benchmark_runner.rs +++ b/examples/benchmark_runner.rs @@ -12,30 +12,30 @@ fn main() { let (bool_grid, scenarios) = get_benchmark(name); // for (allow_diag, pruning) in [(false, false), (true, false), (true, true)] { - for (allow_diag, pruning) in [(true, false)] { - let mut pathing_grid: Pathfinder = - Pathfinder::new(bool_grid.width, bool_grid.height, true); - pathing_grid.grid = bool_grid.clone(); - pathing_grid.allow_diagonal_move = allow_diag; - pathing_grid.improved_pruning = pruning; - pathing_grid.initialize(); - pathing_grid.generate_components(); - let number_of_scenarios = scenarios.len() as u32; - let before = Instant::now(); - run_scenarios(&pathing_grid, &scenarios); - let elapsed = before.elapsed(); - println!( - "\tElapsed time: {:.2?}; per scenario: {:.2?}", - elapsed, - elapsed / number_of_scenarios - ); - total_time += elapsed; - } + let mut pathing_grid: Pathfinder = + Pathfinder::new(bool_grid.width, bool_grid.height, true); + pathing_grid.grid = bool_grid.clone(); + pathing_grid.improved_pruning = false; + pathing_grid.initialize(); + pathing_grid.generate_components(); + let number_of_scenarios = scenarios.len() as u32; + let before = Instant::now(); + run_scenarios(&pathing_grid, &scenarios); + let elapsed = before.elapsed(); + println!( + "\tElapsed time: {:.2?}; per scenario: {:.2?}", + elapsed, + elapsed / number_of_scenarios + ); + total_time += elapsed; } println!("\tTotal benchmark time: {:.2?}", total_time); } -pub fn run_scenarios(pathing_grid: &Pathfinder, scenarios: &Vec<(Point, Point, f64)>) { +pub fn run_scenarios( + pathing_grid: &Pathfinder, + scenarios: &Vec<(Point, Point, f64)>, +) { for (start, goal, _) in scenarios { let path: Option> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); assert!(path.is_some()); diff --git a/examples/heuristic_factor.rs b/examples/heuristic_factor.rs index 825646d..f00c297 100644 --- a/examples/heuristic_factor.rs +++ b/examples/heuristic_factor.rs @@ -8,7 +8,7 @@ use grid_util::Rect; fn main() { const N: i32 = 30; - let mut pathing_grid: Pathfinder = Pathfinder::new(N as usize, N as usize, true); + let mut pathing_grid: Pathfinder = Pathfinder::new(N as usize, N as usize, true); pathing_grid.heuristic_factor = 1.3; pathing_grid.set_rect(Rect::new(1, 1, N - 2, N - 2), false); pathing_grid.set_rect(Rect::new(8, 8, 8, 8), true); diff --git a/examples/multiple_goals.rs b/examples/multiple_goals.rs index 524a29f..8930816 100644 --- a/examples/multiple_goals.rs +++ b/examples/multiple_goals.rs @@ -15,7 +15,7 @@ use grid_util::point::Point; // The found path moves to the closest goal, which is the top one. fn main() { - let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); println!("{}", pathing_grid); diff --git a/examples/paths_and_waypoints.rs b/examples/paths_and_waypoints.rs index b6df4ba..0791df8 100644 --- a/examples/paths_and_waypoints.rs +++ b/examples/paths_and_waypoints.rs @@ -20,7 +20,7 @@ use grid_util::point::Point; // path, as a shorthand for the two previous calls. fn main() { - let mut pathing_grid: Pathfinder = Pathfinder::new(5, 5, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(5, 5, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); println!("{}", pathing_grid); diff --git a/examples/simple_4.rs b/examples/simple_4.rs index 2882b8f..278585d 100644 --- a/examples/simple_4.rs +++ b/examples/simple_4.rs @@ -16,8 +16,7 @@ use grid_util::point::Point; // Nodes have a 4-neighborhood fn main() { - let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); - pathing_grid.allow_diagonal_move = false; + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); println!("{}", pathing_grid); diff --git a/examples/simple_8.rs b/examples/simple_8.rs index 7ec57db..0f4eedb 100644 --- a/examples/simple_8.rs +++ b/examples/simple_8.rs @@ -16,7 +16,7 @@ use grid_util::point::Point; // Nodes have an 8-neighborhood fn main() { - let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); + let mut pathing_grid: Pathfinder = Pathfinder::new(3, 3, false); pathing_grid.set(1, 1, true); pathing_grid.generate_components(); println!("{}", pathing_grid); diff --git a/src/lib.rs b/src/lib.rs index 17b16ad..2405529 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -73,7 +73,7 @@ pub fn waypoints_to_path(waypoints: Vec) -> Vec { /// empty ([false]). It also records neighbours in [u8] format for fast lookups during search. /// Implements [Grid] by building on [BoolGrid]. #[derive(Clone, Debug)] -pub struct Pathfinder { +pub struct Pathfinder { pub grid: BoolGrid, pub neighbours: SimpleValueGrid, pub jump_point: SimpleValueGrid, @@ -81,12 +81,11 @@ pub struct Pathfinder { pub components_dirty: bool, pub heuristic_factor: f32, pub improved_pruning: bool, - pub allow_diagonal_move: bool, context: Arc>>, } -impl Default for Pathfinder { - fn default() -> Pathfinder { +impl Default for Pathfinder { + fn default() -> Pathfinder { let mut grid = Pathfinder { grid: BoolGrid::default(), neighbours: SimpleValueGrid::default(), @@ -95,16 +94,15 @@ impl Default for Pathfinder { components_dirty: false, improved_pruning: true, heuristic_factor: 1.0, - allow_diagonal_move: true, context: Arc::new(Mutex::new(SearchContext::new())), }; grid.initialize(); grid } } -impl Pathfinder { +impl Pathfinder { fn neighborhood_points(&self, point: &Point) -> SmallVec<[Point; 8]> { - if self.allow_diagonal_move { + if ALLOW_DIAGONAL { point.moore_neighborhood_smallvec() } else { point.neumann_neighborhood_smallvec() @@ -123,7 +121,7 @@ impl Pathfinder { } /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. pub fn heuristic(&self, p1: &Point, p2: &Point) -> i32 { - if self.allow_diagonal_move { + if ALLOW_DIAGONAL { let delta_x = (p1.x - p2.x).abs(); let delta_y = (p1.y - p2.y).abs(); // Formula from https://github.com/riscy/a_star_on_grids @@ -188,7 +186,7 @@ impl Pathfinder { let dir_num = dir.num(); let mut n_mask: u8; let mut neighbours = self.neighbours.get_point(*node); - if !self.allow_diagonal_move { + if !ALLOW_DIAGONAL { neighbours &= 0b01010101; n_mask = 0b01000101_u8.rotate_left(dir_num as u32); } else if dir.diagonal() { @@ -216,7 +214,7 @@ impl Pathfinder { } let comb_mask = neighbours & n_mask; (0..8) - .step_by(if self.allow_diagonal_move { 1 } else { 2 }) + .step_by(if ALLOW_DIAGONAL { 1 } else { 2 }) .filter(move |x| comb_mask & (1 << *x) != 0) // (dir_num % 2) * (D-C) + C) // is an optimized version without a conditional of @@ -287,7 +285,7 @@ impl Pathfinder { // When using a 4-neighborhood (specified by setting allow_diagonal_move to false), // jumps perpendicular to the direction are performed. This is necessary to not miss the // goal when passing by. - if !self.allow_diagonal_move || !ALLOW_CORNER_CUTTING && !direction.diagonal() { + if !ALLOW_DIAGONAL || !ALLOW_CORNER_CUTTING && !direction.diagonal() { let perp_1 = direction.rotate_ccw(2); let perp_2 = direction.rotate_cw(2); if self.jump_straight(initial, 1, perp_1, goal).is_some() @@ -590,7 +588,7 @@ impl Pathfinder { let point = Point::new(x, y); let parent_ix = self.grid.get_ix_point(&point); - if self.allow_diagonal_move { + if ALLOW_DIAGONAL { vec![ Point::new(point.x, point.y + 1), Point::new(point.x, point.y - 1), @@ -619,7 +617,7 @@ impl Pathfinder { } } } -impl fmt::Display for Pathfinder { +impl fmt::Display for Pathfinder { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!(f, "Grid:")?; for y in 0..self.grid.height as i32 { @@ -639,7 +637,7 @@ impl fmt::Display for Pathfinder { } } -impl ValueGrid for Pathfinder { +impl ValueGrid for Pathfinder { fn new(width: usize, height: usize, default_value: bool) -> Self { let mut base_grid = Pathfinder { grid: BoolGrid::new(width, height, default_value), @@ -649,7 +647,6 @@ impl ValueGrid for Pathfinder { components_dirty: false, improved_pruning: true, heuristic_factor: 1.0, - allow_diagonal_move: true, context: Arc::new(Mutex::new(SearchContext::new())), }; base_grid.initialize(); From 47bf375c7c328847fbc2db237f329fdba2785b83 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 23:39:58 +0100 Subject: [PATCH 37/41] Removed useless 'improved pruning 4 grid' bench for new jps and changed bench_sets for 4 grid case --- benches/comparison_bench.rs | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index a88aa13..72f1a83 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -15,8 +15,13 @@ fn dao_bench(c: &mut Criterion) { } else { smallvec![false] }; + let bench_set = if ALLOW_DIAGONAL{ + ["dao/arena", "dao/den312d", "dao/arena2"] + } + else{ + ["dao/arena", "dao/den009d", "dao/den312d"] + }; for pruning in arr { - let bench_set = ["dao/arena", "dao/den312d", "dao/arena2"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); let mut pathing_grid: Pathfinder = @@ -40,8 +45,18 @@ fn dao_bench(c: &mut Criterion) { } fn dao_bench_jps(c: &mut Criterion) { - for pruning in [false, true] { - let bench_set = ["dao/arena", "dao/den312d", "dao/arena2"]; + let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { + smallvec![false, true] + } else { + smallvec![false] + }; + let bench_set = if ALLOW_DIAGONAL{ + ["dao/arena", "dao/den312d", "dao/arena2"] + } + else{ + ["dao/arena", "dao/den009d", "dao/den312d"] + }; + for pruning in arr { for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); let mut pathing_grid: PathingGrid = From 17eb8d35268c26d98dd8d724b9649f559fefc470 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 16 Nov 2025 23:46:27 +0100 Subject: [PATCH 38/41] Removed grid argument from pruned_neighborhood in new JPS --- benches/comparison_bench.rs | 24 +++++++++++------------- src/solver/jps.rs | 3 +-- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 72f1a83..33258a5 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -15,10 +15,9 @@ fn dao_bench(c: &mut Criterion) { } else { smallvec![false] }; - let bench_set = if ALLOW_DIAGONAL{ + let bench_set = if ALLOW_DIAGONAL { ["dao/arena", "dao/den312d", "dao/arena2"] - } - else{ + } else { ["dao/arena", "dao/den009d", "dao/den312d"] }; for pruning in arr { @@ -50,10 +49,9 @@ fn dao_bench_jps(c: &mut Criterion) { } else { smallvec![false] }; - let bench_set = if ALLOW_DIAGONAL{ + let bench_set = if ALLOW_DIAGONAL { ["dao/arena", "dao/den312d", "dao/arena2"] - } - else{ + } else { ["dao/arena", "dao/den009d", "dao/den312d"] }; for pruning in arr { @@ -129,13 +127,13 @@ fn dao_bench_dijkstra(c: &mut Criterion) { criterion_group!( benches, - dao_bench, - dao_bench, - dao_bench_jps, + // dao_bench, + // dao_bench, + // dao_bench_jps, dao_bench_jps, - dao_bench_astar, - dao_bench_astar, - dao_bench_dijkstra, - dao_bench_dijkstra + // dao_bench_astar, + // dao_bench_astar, + // dao_bench_dijkstra, + // dao_bench_dijkstra ); criterion_main!(benches); diff --git a/src/solver/jps.rs b/src/solver/jps.rs index f7970a0..3fb7584 100644 --- a/src/solver/jps.rs +++ b/src/solver/jps.rs @@ -30,7 +30,7 @@ impl GridSolver for JPSSolver { Some(parent_node) => { let mut succ = SmallVec::new(); let dir = parent_node.dir_obj(node); - for (n, c) in self.pruned_neighborhood(dir, node, grid) { + for (n, c) in self.pruned_neighborhood::(dir, node) { let dir = node.dir_obj(&n); // Jumps the neighbor, skipping over unnecessary nodes. if let Some((jumped_node, cost)) = self.jump(*node, c, dir, goal, grid) { @@ -143,7 +143,6 @@ impl JPSSolver { &self, dir: Direction, node: &'a Point, - grid: &PathingGrid, ) -> impl Iterator + 'a { let dir_num = dir.num(); let mut n_mask: u8; From 8e8767efb2aca91a43433a19b43f9ee3c6cbc4e4 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 23 Nov 2025 10:38:08 +0100 Subject: [PATCH 39/41] Introduced get_path_single_goal_approximate and get_waypoints_single_goal_approximate, cleaning up the API by avoiding always having to pass 'false' to approximate for normal pathfinding --- benches/comparison_bench.rs | 17 ++---- benches/single_bench.rs | 7 +-- examples/benchmark_runner.rs | 2 +- examples/heuristic_factor.rs | 4 +- examples/paths_and_waypoints.rs | 6 +- examples/simple_4.rs | 4 +- examples/simple_8.rs | 4 +- src/lib.rs | 97 +++++++++++++++++---------------- src/solver/astar.rs | 16 +++--- src/solver/mod.rs | 83 ++++++++++++++++------------ tests/benchmark_distances.rs | 4 +- tests/fuzz_test.rs | 8 +-- 12 files changed, 125 insertions(+), 127 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 33258a5..bdbeaac 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -35,7 +35,7 @@ fn dao_bench(c: &mut Criterion) { c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { b.iter(|| { for (start, end, _) in &scenarios { - black_box(pathing_grid.get_path_single_goal(*start, *end, false)); + black_box(pathing_grid.get_path_single_goal(*start, *end)); } }) }); @@ -71,12 +71,7 @@ fn dao_bench_jps(c: &mut Criterion) { |b| { b.iter(|| { for (start, end, _) in &scenarios { - black_box(solver.get_path_single_goal( - &mut pathing_grid, - *start, - *end, - false, - )); + black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end)); } }) }, @@ -95,10 +90,10 @@ fn dao_bench_astar(c: &mut Criterion) { let solver = AstarSolver::new(); let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; - c.bench_function(format!("{name}, A* {diag_str}").as_str(), |b| { + c.bench_function(format!("{name}, Astar {diag_str}").as_str(), |b| { b.iter(|| { for (start, end, _) in &scenarios { - black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end, false)); + black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end)); } }) }); @@ -118,7 +113,7 @@ fn dao_bench_dijkstra(c: &mut Criterion) { c.bench_function(format!("{name}, Dijkstra {diag_str}").as_str(), |b| { b.iter(|| { for (start, end, _) in &scenarios { - black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end, false)); + black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end)); } }) }); @@ -128,7 +123,7 @@ fn dao_bench_dijkstra(c: &mut Criterion) { criterion_group!( benches, // dao_bench, - // dao_bench, + dao_bench, // dao_bench_jps, dao_bench_jps, // dao_bench_astar, diff --git a/benches/single_bench.rs b/benches/single_bench.rs index 74aa3cb..8aecb15 100644 --- a/benches/single_bench.rs +++ b/benches/single_bench.rs @@ -24,12 +24,7 @@ fn dao_bench_single(c: &mut Criterion) { c.bench_function(format!("{name}, {diag_str}{improved_str}").as_str(), |b| { b.iter(|| { for (start, end, _) in &scenarios { - black_box(solver.get_path_single_goal( - &mut pathing_grid, - *start, - *end, - false, - )); + black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end)); } }) }); diff --git a/examples/benchmark_runner.rs b/examples/benchmark_runner.rs index c23746b..9a1f7cf 100644 --- a/examples/benchmark_runner.rs +++ b/examples/benchmark_runner.rs @@ -37,7 +37,7 @@ pub fn run_scenarios( scenarios: &Vec<(Point, Point, f64)>, ) { for (start, goal, _) in scenarios { - let path: Option> = pathing_grid.get_waypoints_single_goal(*start, *goal, false); + let path: Option> = pathing_grid.get_waypoints_single_goal(*start, *goal); assert!(path.is_some()); } } diff --git a/examples/heuristic_factor.rs b/examples/heuristic_factor.rs index f00c297..4b73706 100644 --- a/examples/heuristic_factor.rs +++ b/examples/heuristic_factor.rs @@ -18,9 +18,7 @@ fn main() { println!("{}", pathing_grid); let start = Point::new(1, 1); let end = Point::new(N - 3, N - 3); - let path = pathing_grid - .get_path_single_goal(start, end, false) - .unwrap(); + let path = pathing_grid.get_path_single_goal(start, end).unwrap(); println!("Path:"); for p in path { println!("{:?}", p); diff --git a/examples/paths_and_waypoints.rs b/examples/paths_and_waypoints.rs index 0791df8..89b9e2c 100644 --- a/examples/paths_and_waypoints.rs +++ b/examples/paths_and_waypoints.rs @@ -26,7 +26,7 @@ fn main() { println!("{}", pathing_grid); let start = Point::new(0, 0); let end = Point::new(4, 4); - if let Some(path) = pathing_grid.get_waypoints_single_goal(start, end, false) { + if let Some(path) = pathing_grid.get_waypoints_single_goal(start, end) { println!("Waypoints:"); for p in &path { println!("{:?}", p); @@ -37,9 +37,7 @@ fn main() { } } println!("\nDirectly computed path"); - let expanded_path = pathing_grid - .get_path_single_goal(start, end, false) - .unwrap(); + let expanded_path = pathing_grid.get_path_single_goal(start, end).unwrap(); for p in expanded_path { println!("{:?}", p); } diff --git a/examples/simple_4.rs b/examples/simple_4.rs index 278585d..7fce188 100644 --- a/examples/simple_4.rs +++ b/examples/simple_4.rs @@ -22,9 +22,7 @@ fn main() { println!("{}", pathing_grid); let start = Point::new(0, 0); let end = Point::new(2, 2); - let path = pathing_grid - .get_path_single_goal(start, end, false) - .unwrap(); + let path = pathing_grid.get_path_single_goal(start, end).unwrap(); println!("Path:"); for p in path { println!("{:?}", p); diff --git a/examples/simple_8.rs b/examples/simple_8.rs index 0f4eedb..4837c91 100644 --- a/examples/simple_8.rs +++ b/examples/simple_8.rs @@ -22,9 +22,7 @@ fn main() { println!("{}", pathing_grid); let start = Point::new(0, 0); let end = Point::new(2, 2); - let path = pathing_grid - .get_path_single_goal(start, end, false) - .unwrap(); + let path = pathing_grid.get_path_single_goal(start, end).unwrap(); println!("Path:"); for p in path { println!("{:?}", p); diff --git a/src/lib.rs b/src/lib.rs index 2405529..1246142 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ use std::sync::{Arc, Mutex}; use crate::astar_jps::DefaultSearchContext; -pub const ALLOW_CORNER_CUTTING: bool = true; +pub const ALLOW_CORNER_CUTTING: bool = false; const EQUAL_EDGE_COST: bool = false; const GRAPH_PRUNING: bool = true; const N_SMALLVEC_SIZE: usize = 8; @@ -423,13 +423,16 @@ impl Pathfinder { /// called Weighted A*. In pathfinding language, a factor greater than /// 1.0 will make the heuristic [inadmissible](https://en.wikipedia.org/wiki/Admissible_heuristic), a requirement for solution optimality. By default, /// the [heuristic_factor](Self::heuristic_factor) is 1.0 which gives optimal solutions. - pub fn get_path_single_goal( + pub fn get_path_single_goal(&self, start: Point, goal: Point) -> Option> { + self.get_waypoints_single_goal(start, goal) + .map(waypoints_to_path) + } + pub fn get_path_single_goal_approximate( &self, start: Point, goal: Point, - approximate: bool, ) -> Option> { - self.get_waypoints_single_goal(start, goal, approximate) + self.get_waypoints_single_goal_approximate(start, goal) .map(waypoints_to_path) } @@ -474,54 +477,54 @@ impl Pathfinder { result.map(|(v, _c)| (*v.last().unwrap(), v)) } /// The raw waypoints (jump points) from which [get_path_single_goal](Self::get_path_single_goal) makes a path. - pub fn get_waypoints_single_goal( + pub fn get_waypoints_single_goal(&self, start: Point, goal: Point) -> Option> { + // Check if start and goal are on the same connected component. + if self.unreachable(&start, &goal) { + return None; + } + // The goal is reachable from the start, compute a path + let mut ct = self.context.lock().unwrap(); + ct.astar_jps( + &start, + |parent, node| { + if GRAPH_PRUNING { + self.jps_neighbours(*parent, node, &|node_pos| *node_pos == goal) + } else { + self.neighborhood_points_and_cost(node) + } + }, + |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, + |point| *point == goal, + ) + .map(|(v, _c)| v) + } + /// The raw waypoints (jump points) from which [get_path_single_goal](Self::get_path_single_goal) makes a path. + pub fn get_waypoints_single_goal_approximate( &self, start: Point, goal: Point, - approximate: bool, ) -> Option> { - if approximate { - // Check if start and one of the goal neighbours are on the same connected component. - if self.neighbours_unreachable(&start, &goal) { - // No neigbhours of the goal are reachable from the start - return None; - } - // A neighbour of the goal can be reached, compute a path - let mut ct = self.context.lock().unwrap(); - ct.astar_jps( - &start, - |parent, node| { - if GRAPH_PRUNING { - self.jps_neighbours(*parent, node, &|node_pos| { - self.heuristic(node_pos, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 } - }) - } else { - self.neighborhood_points_and_cost(node) - } - }, - |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, - |point| self.heuristic(point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }, - ) - } else { - // Check if start and goal are on the same connected component. - if self.unreachable(&start, &goal) { - return None; - } - // The goal is reachable from the start, compute a path - let mut ct = self.context.lock().unwrap(); - ct.astar_jps( - &start, - |parent, node| { - if GRAPH_PRUNING { - self.jps_neighbours(*parent, node, &|node_pos| *node_pos == goal) - } else { - self.neighborhood_points_and_cost(node) - } - }, - |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, - |point| *point == goal, - ) + // Check if start and one of the goal neighbours are on the same connected component. + if self.neighbours_unreachable(&start, &goal) { + // No neigbhours of the goal are reachable from the start + return None; } + // A neighbour of the goal can be reached, compute a path + let mut ct = self.context.lock().unwrap(); + ct.astar_jps( + &start, + |parent, node| { + if GRAPH_PRUNING { + self.jps_neighbours(*parent, node, &|node_pos| { + self.heuristic(node_pos, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 } + }) + } else { + self.neighborhood_points_and_cost(node) + } + }, + |point| (self.heuristic(point, &goal) as f32 * self.heuristic_factor) as i32, + |point| self.heuristic(point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }, + ) .map(|(v, _c)| v) } /// Regenerates the components if they are marked as dirty. diff --git a/src/solver/astar.rs b/src/solver/astar.rs index 28ffb49..b926c74 100644 --- a/src/solver/astar.rs +++ b/src/solver/astar.rs @@ -59,7 +59,7 @@ mod tests { let solver = AstarSolver::new(); let start = Point::new(0, 0); let path = solver - .get_path_single_goal(&mut pathing_grid, start, start, false) + .get_path_single_goal(&mut pathing_grid, start, start) .unwrap(); assert!(path.len() == 1); } @@ -72,7 +72,7 @@ mod tests { let solver = AstarSolver::new(); let start = Point::new(0, 0); let path = solver - .get_path_single_goal(&mut pathing_grid, start, start, false) + .get_path_single_goal(&mut pathing_grid, start, start) .unwrap(); assert!(path.len() == 1); } @@ -89,7 +89,7 @@ mod tests { let start = Point::new(0, 0); let end = Point::new(2, 2); let path = solver - .get_path_single_goal(&mut pathing_grid, start, end, false) + .get_path_single_goal(&mut pathing_grid, start, end) .unwrap(); assert!(path.len() == expected); } @@ -106,7 +106,7 @@ mod tests { let start = Point::new(0, 0); let end = Point::new(2, 2); let path = solver - .get_path_single_goal(&mut pathing_grid, start, end, false) + .get_path_single_goal(&mut pathing_grid, start, end) .unwrap(); assert!(path.len() == expected); } @@ -160,7 +160,7 @@ mod tests { let start = Point::new(0, 0); let end = Point::new(7, 7); let path = solver - .get_path_single_goal(&mut pathing_grid, start, end, false) + .get_path_single_goal(&mut pathing_grid, start, end) .unwrap(); assert!(path.len() == expected); } @@ -178,7 +178,7 @@ mod tests { let start = Point::new(0, 0); let end = Point::new(7, 7); let path = solver - .get_path_single_goal(&mut pathing_grid, start, end, false) + .get_path_single_goal(&mut pathing_grid, start, end) .unwrap(); assert!(path.len() == expected); } @@ -201,8 +201,8 @@ mod tests { let start = Point::new(0, 0); let goal = Point::new(1, 1); - let path = solver.get_path_single_goal(&mut pathing_grid, start, goal, false); - let path_diag = solver.get_path_single_goal(&mut pathing_grid_diag, start, goal, false); + let path = solver.get_path_single_goal(&mut pathing_grid, start, goal); + let path_diag = solver.get_path_single_goal(&mut pathing_grid_diag, start, goal); assert!(path.is_none()); if ALLOW_CORNER_CUTTING { assert!(path_diag.is_some()); diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 5730cf2..fc132cb 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -77,9 +77,17 @@ pub trait GridSolver { grid: &mut PathingGrid, start: Point, goal: Point, - approximate: bool, ) -> Option> { - self.get_waypoints_single_goal(grid, start, goal, approximate) + self.get_waypoints_single_goal(grid, start, goal) + .map(waypoints_to_path) + } + fn get_path_single_goal_approximate( + &self, + grid: &mut PathingGrid, + start: Point, + goal: Point, + ) -> Option> { + self.get_waypoints_single_goal_approximate(grid, start, goal) .map(waypoints_to_path) } /// The raw waypoints (jump points) from which [get_path_single_goal](Self::get_path_single_goal) makes a path. @@ -88,41 +96,46 @@ pub trait GridSolver { grid: &mut PathingGrid, start: Point, goal: Point, - approximate: bool, ) -> Option> { - if approximate { - // Check if start and one of the goal neighbours are on the same connected component. - if grid.neighbours_unreachable(&start, &goal) { - // No neigbhours of the goal are reachable from the start - return None; - } - // A neighbour of the goal can be reached, compute a path - let mut ct = grid.context.lock().unwrap(); - ct.astar_jps( - &start, - |parent, node| { - self.successors(grid, *parent, node, &|node_pos| { - self.heuristic(grid, node_pos, &goal) - <= if EQUAL_EDGE_COST { 1 } else { 99 } - }) - }, - |point| self.heuristic(grid, point, &goal), - |point| self.heuristic(grid, point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }, - ) - } else { - // Check if start and goal are on the same connected component. - if grid.unreachable(&start, &goal) { - return None; - } - // The goal is reachable from the start, compute a path - let mut ct = grid.context.lock().unwrap(); - ct.astar_jps( - &start, - |parent, node| self.successors(grid, *parent, node, &|node_pos| *node_pos == goal), - |point| self.heuristic(grid, point, &goal), - |point| *point == goal, - ) + // Check if start and goal are on the same connected component. + if grid.unreachable(&start, &goal) { + return None; } + // The goal is reachable from the start, compute a path + let mut ct = grid.context.lock().unwrap(); + ct.astar_jps( + &start, + |parent, node| self.successors(grid, *parent, node, &|node_pos| *node_pos == goal), + |point| self.heuristic(grid, point, &goal), + |point| *point == goal, + ) + .map(|(v, _c)| v) + } + + /// The raw waypoints (jump points) from which [get_path_single_goal](Self::get_path_single_goal) makes a path. + fn get_waypoints_single_goal_approximate( + &self, + grid: &mut PathingGrid, + start: Point, + goal: Point, + ) -> Option> { + // Check if start and one of the goal neighbours are on the same connected component. + if grid.neighbours_unreachable(&start, &goal) { + // No neigbhours of the goal are reachable from the start + return None; + } + // A neighbour of the goal can be reached, compute a path + let mut ct = grid.context.lock().unwrap(); + ct.astar_jps( + &start, + |parent, node| { + self.successors(grid, *parent, node, &|node_pos| { + self.heuristic(grid, node_pos, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 } + }) + }, + |point| self.heuristic(grid, point, &goal), + |point| self.heuristic(grid, point, &goal) <= if EQUAL_EDGE_COST { 1 } else { 99 }, + ) .map(|(v, _c)| v) } /// Computes a path from the start to one of the given goals and returns the selected goal in addition to the found path. Otherwise behaves similar to [get_path_single_goal](Self::get_path_single_goal). diff --git a/tests/benchmark_distances.rs b/tests/benchmark_distances.rs index 087083e..20a737a 100644 --- a/tests/benchmark_distances.rs +++ b/tests/benchmark_distances.rs @@ -30,7 +30,7 @@ fn verify_solution_distance_jps() { for (start, end, distance) in &scenarios { println!("Start: {start}; End: {end}; Distance: {distance}"); let path = solver - .get_path_single_goal(&mut pathing_grid, *start, *end, false) + .get_path_single_goal(&mut pathing_grid, *start, *end) .unwrap(); save_path(path.clone(), "path.csv").unwrap(); let float_cost = solver.get_path_cost_float(&path, &pathing_grid); @@ -57,7 +57,7 @@ fn verify_solution_distance_astar() { for (start, end, distance) in &scenarios { println!("Start: {start}; End: {end}; Distance: {distance}"); let path = solver - .get_path_single_goal(&mut pathing_grid, *start, *end, false) + .get_path_single_goal(&mut pathing_grid, *start, *end) .unwrap(); save_path(path.clone(), "path.csv").unwrap(); let float_cost = solver.get_path_cost_float(&path, &pathing_grid); diff --git a/tests/fuzz_test.rs b/tests/fuzz_test.rs index 311e22b..e657805 100644 --- a/tests/fuzz_test.rs +++ b/tests/fuzz_test.rs @@ -70,7 +70,7 @@ fn reachable_fuzzer() { random_grid.set_point(end, false); solver.initialize(&random_grid); let reachable = random_grid.reachable(&start, &end); - let path = solver.get_path_single_goal(&mut random_grid, start, end, false); + let path = solver.get_path_single_goal(&mut random_grid, start, end); // Show the grid if a path is not found if path.is_some() != reachable { visualize_grid(&random_grid, &start, &end); @@ -108,10 +108,10 @@ fn distance_fuzzer() { let reachable = random_grid.reachable(&start, &end); if reachable { let jps_path = jps_solver - .get_path_single_goal(&mut random_grid, start, end, false) + .get_path_single_goal(&mut random_grid, start, end) .unwrap(); let astar_path = astar_solver - .get_path_single_goal(&mut random_grid, start, end, false) + .get_path_single_goal(&mut random_grid, start, end) .unwrap(); let astar_cost = astar_solver.get_path_cost_float(&astar_path, &random_grid); @@ -128,7 +128,7 @@ fn distance_fuzzer() { let jps_suffix_cost = jps_solver.get_path_cost_float(jps_suffix, &random_grid); let astar_suffix_path = astar_solver - .get_path_single_goal(&mut random_grid, p, end, false) + .get_path_single_goal(&mut random_grid, p, end) .expect("A* should find a path from intermediate JPS node"); let astar_suffix_cost = astar_solver.get_path_cost_float(&astar_suffix_path, &random_grid); From bf2847ed27c44a834dec88315a0ed41a5cde3b69 Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 30 Nov 2025 16:37:19 +0100 Subject: [PATCH 40/41] Cherry picked comparison_bench changes from altsolver --- benches/comparison_bench.rs | 130 ++++++++++++++++++------------------ 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index bdbeaac..91ea14c 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -1,24 +1,27 @@ use criterion::{criterion_group, criterion_main, Criterion}; use grid_pathfinding::{ pathing_grid::PathingGrid, - solver::{astar::AstarSolver, dijkstra::DijkstraSolver, jps::JPSSolver, GridSolver}, + solver::{ + astar::AstarSolver, dijkstra::DijkstraSolver, jps::JPSSolver, GridSolver, + }, Pathfinder, }; use grid_pathfinding_benchmark::*; -use grid_util::grid::ValueGrid; +use grid_util::{grid::ValueGrid, Point}; +use rand::{rngs::StdRng, Rng, SeedableRng}; use smallvec::{smallvec, SmallVec}; use std::hint::black_box; fn dao_bench(c: &mut Criterion) { let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { - smallvec![false, true] + smallvec![false] } else { smallvec![false] }; let bench_set = if ALLOW_DIAGONAL { - ["dao/arena", "dao/den312d", "dao/arena2"] + ["dao/arena2"] } else { - ["dao/arena", "dao/den009d", "dao/den312d"] + ["dao/arena2"] }; for pruning in arr { for name in bench_set { @@ -43,54 +46,29 @@ fn dao_bench(c: &mut Criterion) { } } -fn dao_bench_jps(c: &mut Criterion) { - let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { - smallvec![false, true] - } else { - smallvec![false] - }; +fn dao_bench_solver( + c: &mut Criterion, + solver_name: &str, + create_solver: FS, +) where + S: GridSolver, + FS: Fn(&mut PathingGrid) -> S, +{ let bench_set = if ALLOW_DIAGONAL { - ["dao/arena", "dao/den312d", "dao/arena2"] + ["dao/arena2"] } else { - ["dao/arena", "dao/den009d", "dao/den312d"] + ["dao/arena2"] }; - for pruning in arr { - for name in bench_set { - let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); - pathing_grid.grid = bool_grid.clone(); - pathing_grid.generate_components(); - let mut solver = JPSSolver::new(&pathing_grid, pruning); - solver.initialize(&pathing_grid); - let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; - let improved_str = if pruning { " (improved pruning)" } else { "" }; - - c.bench_function( - format!("{name}, JPS {diag_str}{improved_str}").as_str(), - |b| { - b.iter(|| { - for (start, end, _) in &scenarios { - black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end)); - } - }) - }, - ); - } - } -} -fn dao_bench_astar(c: &mut Criterion) { - let bench_set = ["dao/arena", "dao/den009d", "dao/den312d"]; for name in bench_set { let (bool_grid, scenarios) = get_benchmark(name.to_owned()); let mut pathing_grid: PathingGrid = PathingGrid::new(bool_grid.width, bool_grid.height, true); pathing_grid.grid = bool_grid.clone(); pathing_grid.generate_components(); - let solver = AstarSolver::new(); + let solver = create_solver(&mut pathing_grid); let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; - c.bench_function(format!("{name}, Astar {diag_str}").as_str(), |b| { + c.bench_function(format!("{name}, {solver_name} {diag_str}").as_str(), |b| { b.iter(|| { for (start, end, _) in &scenarios { black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end)); @@ -99,36 +77,58 @@ fn dao_bench_astar(c: &mut Criterion) { }); } } -fn dao_bench_dijkstra(c: &mut Criterion) { - let bench_set = ["dao/arena", "dao/den009d", "dao/den312d"]; - for name in bench_set { - let (bool_grid, scenarios) = get_benchmark(name.to_owned()); - let mut pathing_grid: PathingGrid = - PathingGrid::new(bool_grid.width, bool_grid.height, true); - pathing_grid.grid = bool_grid.clone(); - pathing_grid.generate_components(); - let solver = DijkstraSolver; - let diag_str = if ALLOW_DIAGONAL { "8-grid" } else { "4-grid" }; +pub fn generate_landmarks_mc( + pathing_grid: &PathingGrid, + number: usize, +) -> Vec { + let mut landmarks = Vec::new(); + let mut rng = StdRng::seed_from_u64(0); + while landmarks.len() < number { + let p = Point::new( + rng.random_range(0..pathing_grid.width() as i32), + rng.random_range(0..pathing_grid.height() as i32), + ); + if pathing_grid.can_move_to_simple(p) { + landmarks.push(p); + } + } + landmarks +} - c.bench_function(format!("{name}, Dijkstra {diag_str}").as_str(), |b| { - b.iter(|| { - for (start, end, _) in &scenarios { - black_box(solver.get_path_single_goal(&mut pathing_grid, *start, *end)); - } - }) - }); +fn dao_bench_jps(c: &mut Criterion) { + let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { + smallvec![false] + } else { + smallvec![false] + }; + for pruning in arr { + let improved_str = if pruning { " (improved pruning)" } else { "" }; + dao_bench_solver( + c, + format!("JPS{improved_str}").as_str(), + |pathing_grid: &mut PathingGrid| { + let mut solver = JPSSolver::new(&pathing_grid, pruning); + solver.initialize(&pathing_grid); + solver + }, + ); } } +fn dao_bench_astar(c: &mut Criterion) { + dao_bench_solver(c, "Astar", |_: &mut PathingGrid| { + AstarSolver::new() + }); +} + +fn dao_bench_dijkstra(c: &mut Criterion) { + dao_bench_solver(c, "Dijkstra", |_: &mut PathingGrid| { + DijkstraSolver + }); +} criterion_group!( benches, - // dao_bench, dao_bench, - // dao_bench_jps, dao_bench_jps, - // dao_bench_astar, - // dao_bench_astar, - // dao_bench_dijkstra, - // dao_bench_dijkstra ); criterion_main!(benches); From b914fa58686f1dd4fb2ea688ce36999015c1ab8d Mon Sep 17 00:00:00 2001 From: Thom van der Woude Date: Sun, 30 Nov 2025 16:38:48 +0100 Subject: [PATCH 41/41] Removed generate_landmarks_mc --- benches/comparison_bench.rs | 30 +++--------------------------- src/solver/mod.rs | 2 +- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/benches/comparison_bench.rs b/benches/comparison_bench.rs index 91ea14c..3a2cff3 100644 --- a/benches/comparison_bench.rs +++ b/benches/comparison_bench.rs @@ -1,14 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; use grid_pathfinding::{ pathing_grid::PathingGrid, - solver::{ - astar::AstarSolver, dijkstra::DijkstraSolver, jps::JPSSolver, GridSolver, - }, + solver::{astar::AstarSolver, dijkstra::DijkstraSolver, jps::JPSSolver, GridSolver}, Pathfinder, }; use grid_pathfinding_benchmark::*; -use grid_util::{grid::ValueGrid, Point}; -use rand::{rngs::StdRng, Rng, SeedableRng}; +use grid_util::grid::ValueGrid; use smallvec::{smallvec, SmallVec}; use std::hint::black_box; @@ -77,23 +74,6 @@ fn dao_bench_solver( }); } } -pub fn generate_landmarks_mc( - pathing_grid: &PathingGrid, - number: usize, -) -> Vec { - let mut landmarks = Vec::new(); - let mut rng = StdRng::seed_from_u64(0); - while landmarks.len() < number { - let p = Point::new( - rng.random_range(0..pathing_grid.width() as i32), - rng.random_range(0..pathing_grid.height() as i32), - ); - if pathing_grid.can_move_to_simple(p) { - landmarks.push(p); - } - } - landmarks -} fn dao_bench_jps(c: &mut Criterion) { let arr: SmallVec<[bool; 2]> = if ALLOW_DIAGONAL { @@ -126,9 +106,5 @@ fn dao_bench_dijkstra(c: &mut Criterion) { }); } -criterion_group!( - benches, - dao_bench, - dao_bench_jps, -); +criterion_group!(benches, dao_bench, dao_bench_jps,); criterion_main!(benches); diff --git a/src/solver/mod.rs b/src/solver/mod.rs index fc132cb..b042d87 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -23,7 +23,7 @@ pub trait GridSolver { /// Uses C as cost for cardinal (straight) moves and D for diagonal moves. fn cost( &self, - grid: &PathingGrid, + _grid: &PathingGrid, p1: &Point, p2: &Point, ) -> i32 {