Pathfinding and re-pickup of the project

This commit is contained in:
2026-07-16 21:08:30 +02:00
parent 5c8494961c
commit 674cf91ef3
7 changed files with 344 additions and 19 deletions
+106
View File
@@ -0,0 +1,106 @@
//! Grid A* over the client's chunk cache. The client only *plans* here — every step
//! still goes to the server as a cardinal action, and the server stays authoritative.
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap};
/// Hard cap on expanded nodes, so a click on an unreachable tile can't stall the frame.
const MAX_EXPANSIONS: usize = 4096;
/// Longest route we will plan. Keeps the reply small and bounds replan cost.
const MAX_ROUTE_LEN: usize = 64;
/// 4-connected A* from `start` to `goal` (manhattan heuristic — admissible on a
/// cardinal grid). Returns the tiles to walk, excluding `start`, ending on `goal`,
/// or `None` if the goal is unreachable within the search budget.
pub fn find_path(
start: (i32, i32),
goal: (i32, i32),
blocked: impl Fn(i32, i32) -> bool,
) -> Option<Vec<(i32, i32)>> {
if start == goal || blocked(goal.0, goal.1) {
return None;
}
let h = |p: (i32, i32)| (p.0 - goal.0).abs() + (p.1 - goal.1).abs();
// (f, tile) min-heap; g and parent per visited tile.
let mut open = BinaryHeap::new();
let mut best: HashMap<(i32, i32), (i32, (i32, i32))> = HashMap::new();
open.push(Reverse((h(start), start)));
best.insert(start, (0, start));
let mut expanded = 0;
while let Some(Reverse((_, cur))) = open.pop() {
if cur == goal {
let mut route = Vec::new();
let mut p = goal;
while p != start {
route.push(p);
p = best[&p].1;
}
if route.len() > MAX_ROUTE_LEN {
return None;
}
route.reverse();
return Some(route);
}
expanded += 1;
if expanded > MAX_EXPANSIONS {
return None;
}
let g = best[&cur].0;
for (dx, dy) in [(0, -1), (0, 1), (-1, 0), (1, 0)] {
let next = (cur.0 + dx, cur.1 + dy);
if blocked(next.0, next.1) {
continue;
}
let ng = g + 1;
if best.get(&next).is_none_or(|&(og, _)| ng < og) {
best.insert(next, (ng, cur));
open.push(Reverse((ng + h(next), next)));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::find_path;
#[test]
fn straight_line() {
let path = find_path((0, 0), (3, 0), |_, _| false).unwrap();
assert_eq!(path, vec![(1, 0), (2, 0), (3, 0)]);
}
#[test]
fn detours_around_wall() {
// Vertical wall at x=2 with a gap at y=5.
let blocked = |x: i32, y: i32| x == 2 && y != 5;
let path = find_path((0, 0), (4, 0), blocked).unwrap();
assert_eq!(path.last(), Some(&(4, 0)));
assert!(path.iter().all(|&(x, y)| !blocked(x, y)));
assert!(path.windows(2).all(|w| {
(w[1].0 - w[0].0).abs() + (w[1].1 - w[0].1).abs() == 1
}));
assert!(path.contains(&(2, 5)));
assert_eq!(path.len(), 14); // 4 across + 2·5 detour
}
#[test]
fn unreachable_is_none() {
// Goal sealed in by a ring.
let blocked = |x: i32, y: i32| (x - 10).abs().max((y - 10).abs()) == 1;
assert_eq!(find_path((0, 0), (10, 10), blocked), None);
}
#[test]
fn degenerate_cases() {
assert_eq!(find_path((5, 5), (5, 5), |_, _| false), None);
assert_eq!(find_path((0, 0), (1, 0), |x, y| (x, y) == (1, 0)), None);
}
}