803-a-star-pathfinding — A* Pathfinding
Tutorial Video
Text description (accessibility)
This video demonstrates the "803-a-star-pathfinding — A* Pathfinding" functional Rust example. Difficulty level: Advanced. Key concepts covered: Functional Programming. A* (Hart, Nilsson, Raphael, 1968) is the standard pathfinding algorithm in games, robotics, and navigation systems. Key difference from OCaml: 1. **Priority queue**: Rust's `BinaryHeap<Reverse<...>>` is efficient; OCaml's balanced BST
Tutorial
The Problem
A* (Hart, Nilsson, Raphael, 1968) is the standard pathfinding algorithm in games, robotics, and navigation systems. It improves on Dijkstra by adding a heuristic that guides the search toward the goal, dramatically reducing explored nodes. The Manhattan distance heuristic is admissible (never overestimates) on grids, guaranteeing optimal paths. Used in every major game engine (Unity NavMesh, Unreal AI), Google Maps, and robot motion planning.
🎯 Learning Outcomes
BinaryHeap<Reverse<(f_score, position)>>) with came_from map|dx| + |dy| as an admissible heuristic for grid movementg_score (cost from start) and f_score = g_score + h(goal) per nodecame_from map backward from goal to startCode Example
#![allow(clippy::all)]
//! # A* Pathfinding
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap};
pub fn astar(
start: (i32, i32),
goal: (i32, i32),
obstacles: &[(i32, i32)],
) -> Option<Vec<(i32, i32)>> {
let obs: std::collections::HashSet<_> = obstacles.iter().copied().collect();
let heuristic = |p: (i32, i32)| ((p.0 - goal.0).abs() + (p.1 - goal.1).abs()) as usize;
let dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)];
let mut open = BinaryHeap::new();
let mut g_score = HashMap::new();
let mut came_from = HashMap::new();
g_score.insert(start, 0usize);
open.push(Reverse((heuristic(start), start)));
while let Some(Reverse((_, current))) = open.pop() {
if current == goal {
let mut path = vec![current];
let mut c = current;
while let Some(&p) = came_from.get(&c) {
path.push(p);
c = p;
}
path.reverse();
return Some(path);
}
for &(dx, dy) in &dirs {
let next = (current.0 + dx, current.1 + dy);
if obs.contains(&next) {
continue;
}
let new_g = g_score[¤t] + 1;
if !g_score.contains_key(&next) || new_g < g_score[&next] {
g_score.insert(next, new_g);
came_from.insert(next, current);
open.push(Reverse((new_g + heuristic(next), next)));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_astar() {
let path = astar((0, 0), (2, 2), &[(1, 1)]);
assert!(path.is_some());
}
}Key Differences
BinaryHeap<Reverse<...>> is efficient; OCaml's balanced BST-based Set has higher constant factors but equivalent asymptotic complexity.let heuristic = |p| ... makes it easy to swap heuristics; OCaml uses first-class functions similarly.Reverse((f_score, pos)) sorts by f_score first due to tuple lexicographic order; OCaml's custom Set.compare must be explicit.OCaml Approach
OCaml implements A* using Map.t for g_score and came_from, and Set.t as the open set with a custom ordering by f_score. OCaml's Hashtbl provides faster mutable alternatives. The OcamlAI library and various game frameworks use OCaml A* implementations. The path reconstruction uses List.rev on the accumulated path.
Full Source
#![allow(clippy::all)]
//! # A* Pathfinding
use std::cmp::Reverse;
use std::collections::{BinaryHeap, HashMap};
pub fn astar(
start: (i32, i32),
goal: (i32, i32),
obstacles: &[(i32, i32)],
) -> Option<Vec<(i32, i32)>> {
let obs: std::collections::HashSet<_> = obstacles.iter().copied().collect();
let heuristic = |p: (i32, i32)| ((p.0 - goal.0).abs() + (p.1 - goal.1).abs()) as usize;
let dirs = [(0, 1), (0, -1), (1, 0), (-1, 0)];
let mut open = BinaryHeap::new();
let mut g_score = HashMap::new();
let mut came_from = HashMap::new();
g_score.insert(start, 0usize);
open.push(Reverse((heuristic(start), start)));
while let Some(Reverse((_, current))) = open.pop() {
if current == goal {
let mut path = vec![current];
let mut c = current;
while let Some(&p) = came_from.get(&c) {
path.push(p);
c = p;
}
path.reverse();
return Some(path);
}
for &(dx, dy) in &dirs {
let next = (current.0 + dx, current.1 + dy);
if obs.contains(&next) {
continue;
}
let new_g = g_score[¤t] + 1;
if !g_score.contains_key(&next) || new_g < g_score[&next] {
g_score.insert(next, new_g);
came_from.insert(next, current);
open.push(Reverse((new_g + heuristic(next), next)));
}
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_astar() {
let path = astar((0, 0), (2, 2), &[(1, 1)]);
assert!(path.is_some());
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_astar() {
let path = astar((0, 0), (2, 2), &[(1, 1)]);
assert!(path.is_some());
}
}
Deep Comparison
OCaml vs Rust: A Star Pathfinding
Overview
See the example.rs and example.ml files for detailed implementations.
Key Differences
| Aspect | OCaml | Rust |
|---|---|---|
| Type system | Hindley-Milner | Ownership + traits |
| Memory | GC | Zero-cost abstractions |
| Mutability | Explicit ref | mut keyword |
| Error handling | Option/Result | Result<T, E> |
See README.md for detailed comparison.
Exercises
√2 for diagonals, and switch the heuristic to Euclidean distance.w > 1) to trade optimality for speed. Compare path quality and nodes expanded for w = 1, 1.5, 2.0.max_steps limit to A* and return the partial path if the goal is not reached within the step budget — useful for real-time game AI.