use std::{cmp::Ordering, collections::BinaryHeap};
use crate::Point;
pub type NodeId = usize;
pub type Cost = f32;
pub trait PathMap {
fn dimensions(&self) -> (i32, i32);
fn is_walkable(&self, position: Point) -> bool;
}
pub fn astar_path_fourwaygrid<T: PathMap>(map: &T, from: Point, to: Point) -> Option<Vec<Point>> {
fn assert_in_bounds<T: PathMap>(map: &T, (x, y): Point) {
let (width, height) = map.dimensions();
if x < 0 || y < 0 || x >= width || y >= height {
panic!(
"(x, y) should be between (0,0) and ({}, {}), got ({}, {}).",
width, height, x, y
);
}
}
assert_in_bounds(map, from);
assert_in_bounds(map, to);
let graph = FourWayGridGraph::new(map);
astar_path(&graph, graph.point_to_index(from), graph.point_to_index(to)).map(|indices| {
indices
.into_iter()
.map(|index| graph.index_to_point(index))
.collect()
})
}
pub fn astar_path<T: Graph>(
graph: &T,
from_index: NodeId,
to_index: NodeId,
) -> Option<Vec<NodeId>> {
fn assert_in_bounds<T: Graph>(graph: &T, index: NodeId) {
if index >= graph.node_count() {
panic!(
"Index {} is out of bounds for a graph of size {}.",
index,
graph.node_count()
);
}
}
assert_in_bounds(graph, from_index);
assert_in_bounds(graph, to_index);
let capacity = graph.node_count() / 2;
let mut frontier = BinaryHeap::with_capacity(capacity);
frontier.push(State {
cost: 0.,
cost_from_start: 0.,
item: from_index,
});
let mut came_from: Vec<NodeId> = vec![usize::MAX; graph.node_count()];
let mut costs: Vec<Cost> = vec![f32::INFINITY; graph.node_count()];
costs[from_index] = 0.;
let mut neighbors: Vec<(NodeId, Cost)> = Vec::with_capacity(4);
let mut to_cost = 0.;
while let Some(State {
item: current_index,
cost_from_start,
..
}) = frontier.pop()
{
if current_index == to_index {
to_cost = cost_from_start;
break;
}
if cost_from_start > costs[current_index] {
continue;
}
neighbors.clear();
graph.neighbors(current_index, &mut neighbors);
for &(next_index, step_cost) in neighbors.iter() {
let new_cost = cost_from_start + step_cost;
if new_cost < costs[next_index] {
let priority = new_cost + graph.heuristic(next_index, to_index);
frontier.push(State {
cost: priority,
cost_from_start: new_cost,
item: next_index,
});
came_from[next_index] = current_index;
costs[next_index] = new_cost;
}
}
}
reconstruct_path(from_index, to_index, came_from, to_cost)
}
fn reconstruct_path(
from: NodeId,
to: NodeId,
came_from: Vec<NodeId>,
cost: Cost,
) -> Option<Vec<NodeId>> {
let mut current = to;
let target_index = from;
let mut path = Vec::with_capacity((cost.floor() + 2.0) as usize);
while current != target_index {
path.push(current);
let entry = came_from[current];
if entry == usize::MAX {
return None;
}
current = entry;
}
path.push(target_index);
path.reverse();
Some(path)
}
struct State<C: PartialOrd, T> {
cost: C,
cost_from_start: C,
item: T,
}
impl<C: PartialOrd, T> PartialEq for State<C, T> {
fn eq(&self, other: &Self) -> bool {
self.cost.eq(&other.cost)
}
}
impl<C: PartialOrd, T> Eq for State<C, T> {}
impl<C: PartialOrd, T> Ord for State<C, T> {
fn cmp(&self, other: &State<C, T>) -> Ordering {
other
.cost
.partial_cmp(&self.cost)
.unwrap_or(Ordering::Equal)
}
}
impl<C: PartialOrd, T> PartialOrd for State<C, T> {
fn partial_cmp(&self, other: &State<C, T>) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub trait Graph {
fn node_count(&self) -> usize;
fn heuristic(&self, a: NodeId, b: NodeId) -> Cost;
fn neighbors(&self, a: NodeId, into: &mut Vec<(NodeId, Cost)>);
}
pub struct FourWayGridGraph<'a, T: PathMap> {
map: &'a T,
width: i32,
height: i32,
}
impl<'a, T: PathMap> FourWayGridGraph<'a, T> {
pub fn new(map: &'a T) -> Self {
let (width, height) = map.dimensions();
FourWayGridGraph { map, width, height }
}
fn is_walkable(&self, x: i32, y: i32) -> bool {
self.map.is_walkable((x, y))
}
fn point_to_index(&self, (x, y): Point) -> usize {
(x + y * self.width) as usize
}
fn index_to_point(&self, index: usize) -> Point {
(index as i32 % self.width, index as i32 / self.width)
}
}
impl<'a, T: PathMap> Graph for FourWayGridGraph<'a, T> {
fn node_count(&self) -> usize {
(self.width * self.height) as usize
}
fn heuristic(&self, a: NodeId, b: NodeId) -> Cost {
let (xa, ya) = self.index_to_point(a);
let (xb, yb) = self.index_to_point(b);
((xa - xb).abs() + (ya - yb).abs()) as f32
}
fn neighbors(&self, a: NodeId, into: &mut Vec<(NodeId, Cost)>) {
let (x, y) = self.index_to_point(a);
let source_even = (x + y) % 2 == 0;
fn add_if_qualified<'a, T: PathMap>(
graph: &FourWayGridGraph<'a, T>,
(x, y): Point,
source_even: bool,
moves_horizontally: bool,
into: &mut Vec<(NodeId, Cost)>,
) {
if x < 0 || y < 0 || x >= graph.width || y >= graph.height || !graph.is_walkable(x, y) {
return;
}
let nudge = if source_even == moves_horizontally {
1.
} else {
0.
};
into.push((graph.point_to_index((x, y)), 1. + 0.001 * nudge));
}
add_if_qualified(self, (x, y + 1), source_even, false, into);
add_if_qualified(self, (x, y - 1), source_even, false, into);
add_if_qualified(self, (x - 1, y), source_even, true, into);
add_if_qualified(self, (x + 1, y), source_even, true, into);
}
}
#[cfg(test)]
mod tests {
use crate::{Point, bresenham::BresenhamLine, path::astar_path};
use super::{FourWayGridGraph, PathMap, astar_path_fourwaygrid};
struct SampleMap {
width: i32,
height: i32,
walkable: Vec<bool>,
}
impl SampleMap {
fn new(width: i32, height: i32) -> Self {
SampleMap {
width,
height,
walkable: vec![true; (width * height) as usize],
}
}
fn build_wall(&mut self, from: Point, to: Point) {
let bresenham = BresenhamLine::new(from, to);
for (x, y) in bresenham {
self.walkable[(x + y * self.width) as usize] = false;
}
}
}
impl PathMap for SampleMap {
fn dimensions(&self) -> (i32, i32) {
(self.width, self.height)
}
fn is_walkable(&self, (x, y): Point) -> bool {
self.walkable[(x + y * self.width) as usize]
}
}
#[test]
fn astar_find_path() {
let mut map = SampleMap::new(10, 10);
map.build_wall((3, 3), (3, 6));
map.build_wall((0, 3), (3, 3));
let from = (0, 4);
let to = (5, 4);
let path = astar_path_fourwaygrid(&map, from, to);
assert!(path.is_some());
if let Some(path) = path {
assert_eq!(from, path[0]);
assert_eq!(to, path[path.len() - 1]);
assert_eq!(
path,
[
(0, 4),
(0, 5),
(1, 5),
(1, 6),
(2, 6),
(2, 7),
(3, 7),
(4, 7),
(5, 7),
(5, 6),
(5, 5),
(5, 4)
]
);
}
}
#[test]
fn astar_no_path() {
let mut map = SampleMap::new(10, 10);
map.build_wall((3, 3), (3, 6));
map.build_wall((0, 3), (3, 3));
map.build_wall((0, 6), (3, 6));
let from = (0, 4);
let to = (5, 4);
let path = astar_path_fourwaygrid(&map, from, to);
assert!(path.is_none());
}
#[test]
#[should_panic(expected = "Index 120 is out of bounds for a graph of size 100.")]
fn astar_path_out_of_bounds_index_panics() {
let map = SampleMap::new(10, 10);
let graph = FourWayGridGraph::new(&map);
astar_path(&graph, 0, 120);
}
#[test]
#[should_panic(expected = "(x, y) should be between (0,0) and (10, 10), got (0, 12).")]
fn astar_fourway_out_of_bounds_index_panics() {
let map = SampleMap::new(10, 10);
astar_path_fourwaygrid(&map, (0, 0), (0, 12));
}
}