use std::{
cmp::Reverse,
collections::{BinaryHeap, HashMap},
hash::Hash,
};
pub(crate) trait AStarProblem {
type ActionType: Clone;
type StateType: Hash + Eq + Clone;
fn initial_state(&self) -> Self::StateType;
fn successors(
&self,
state: &Self::StateType,
) -> Vec<(f32, Self::ActionType, Self::StateType)>;
fn heuristic(&self, state: &Self::StateType) -> f32;
fn is_goal_state(&self, state: &Self::StateType) -> bool;
}
struct Node<ProblemType: AStarProblem> {
cost: f32,
state: ProblemType::StateType,
previous_node: Option<(usize, ProblemType::ActionType)>,
}
struct NodeRef {
cost: f32,
estimate: f32,
index: usize,
}
impl PartialEq for NodeRef {
fn eq(&self, other: &Self) -> bool {
self.estimate == other.estimate
}
}
impl Eq for NodeRef {}
#[allow(clippy::non_canonical_partial_ord_impl)]
impl PartialOrd for NodeRef {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match self.estimate.partial_cmp(&other.estimate) {
Some(std::cmp::Ordering::Equal) => {
Reverse(self.cost).partial_cmp(&Reverse(other.estimate))
}
Some(ord) => Some(ord),
None => None,
}
}
}
impl Ord for NodeRef {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.partial_cmp(other).unwrap()
}
}
fn recover_path_from_node<ProblemType: AStarProblem>(
node_ref: &NodeRef,
nodes: &[Node<ProblemType>],
) -> Vec<ProblemType::ActionType> {
let mut path = Vec::new();
let mut node_index = node_ref.index;
loop {
let node = &nodes[node_index];
match &node.previous_node {
None => break,
Some((next_index, action)) => {
path.push(action.clone());
node_index = *next_index;
}
}
}
path.reverse();
path
}
#[derive(Debug)]
pub(crate) struct PathStats {
pub(crate) explored_nodes: u32,
}
#[derive(Debug)]
pub(crate) struct PathResult<ActionType> {
pub(crate) stats: PathStats,
pub(crate) path: Option<Vec<ActionType>>,
}
pub(crate) fn find_path<ProblemType: AStarProblem>(
problem: &ProblemType,
) -> PathResult<ProblemType::ActionType> {
let mut stats = PathStats { explored_nodes: 0 };
let mut best_estimates = HashMap::new();
let mut all_nodes = Vec::<Node<ProblemType>>::new();
let mut open_nodes = BinaryHeap::new();
fn try_add_node<ProblemType: AStarProblem>(
problem: &ProblemType,
node: Node<ProblemType>,
all_nodes: &mut Vec<Node<ProblemType>>,
open_nodes: &mut BinaryHeap<Reverse<NodeRef>>,
best_estimates: &mut HashMap<ProblemType::StateType, f32>,
) {
let estimate = node.cost + problem.heuristic(&node.state);
let best_estimate =
best_estimates.entry(node.state.clone()).or_insert(f32::INFINITY);
if *best_estimate <= estimate {
return;
}
*best_estimate = estimate;
open_nodes.push(Reverse(NodeRef {
cost: node.cost,
estimate,
index: all_nodes.len(),
}));
all_nodes.push(node);
}
let initial_node =
Node { cost: 0.0, state: problem.initial_state(), previous_node: None };
try_add_node(
problem,
initial_node,
&mut all_nodes,
&mut open_nodes,
&mut best_estimates,
);
while let Some(Reverse(current_node_ref)) = open_nodes.pop() {
let current_node = &all_nodes[current_node_ref.index];
if *best_estimates.get(¤t_node.state).unwrap()
< current_node_ref.estimate
{
continue;
}
stats.explored_nodes += 1;
if problem.is_goal_state(¤t_node.state) {
return PathResult {
stats,
path: Some(recover_path_from_node(¤t_node_ref, &all_nodes)),
};
}
let current_cost = current_node.cost;
for (action_cost, action, state) in problem.successors(¤t_node.state)
{
let new_node = Node {
cost: current_cost + action_cost,
state,
previous_node: Some((current_node_ref.index, action)),
};
try_add_node(
problem,
new_node,
&mut all_nodes,
&mut open_nodes,
&mut best_estimates,
);
}
}
PathResult { stats, path: None }
}
#[cfg(test)]
#[path = "astar_test.rs"]
mod test;