use std::cmp::Ordering;
use std::collections::{
hash_map::Entry,
{BinaryHeap, HashMap},
};
use std::hash::Hash;
use num::CheckedAdd;
use screeps::constants::Direction;
use screeps::{Position, RoomXY};
use crate::common::traits::AddDirection;
use crate::utils::goals::goal_exact_node_multigoal;
use crate::utils::heuristics::heuristic_get_range_to_multigoal;
pub trait AStarNode: Eq + Hash + Copy + Ord {}
impl<T> AStarNode for T where T: Eq + Hash + Copy + Ord {}
pub trait AStarGridNode: AStarNode + AddDirection + std::fmt::Debug {}
impl<T> AStarGridNode for T where T: AStarNode + AddDirection + std::fmt::Debug {}
pub trait AStarCost:
std::ops::Add<Self, Output = Self>
+ CheckedAdd
+ Copy
+ Eq
+ Sized
+ std::cmp::Ord
+ std::fmt::Debug
{
}
impl<T> AStarCost for T where
T: std::ops::Add<Self, Output = Self>
+ CheckedAdd
+ Copy
+ Eq
+ Sized
+ std::cmp::Ord
+ std::fmt::Debug
{
}
#[derive(Debug)]
pub struct AStarSearchResults<T, O>
where
T: AStarNode,
O: AStarCost,
{
ops_used: u32,
cost: Option<O>,
incomplete: bool,
path: Vec<T>,
}
impl<T: AStarNode, O: AStarCost> AStarSearchResults<T, O> {
pub fn ops(&self) -> u32 {
self.ops_used
}
pub fn cost(&self) -> Option<O> {
self.cost
}
pub fn incomplete(&self) -> bool {
self.incomplete
}
pub fn path(&self) -> &[T] {
&self.path
}
}
fn compare_option_scores<O: AStarCost>(a: Option<O>, b: Option<O>) -> Ordering {
match (a, b) {
(None, None) => Ordering::Equal,
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(Some(l), Some(r)) => l.cmp(&r),
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct State<T, O>
where
T: Ord,
O: AStarCost,
{
g_score: O,
f_score: O,
position: T,
}
impl<T, O> Ord for State<T, O>
where
T: Ord,
O: AStarCost,
{
fn cmp(&self, other: &Self) -> Ordering {
other.f_score.cmp(&self.f_score).then_with(|| {
self.g_score.cmp(&other.g_score).then_with(|| {
self.position.cmp(&other.position)
})
})
}
}
impl<T, O> PartialOrd for State<T, O>
where
T: Ord,
O: AStarCost,
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
struct GridState<T, O>
where
T: Ord,
O: AStarCost,
{
g_score: Option<O>,
f_score: Option<O>,
open_direction: Option<Direction>,
position: T,
}
impl<T, O> Ord for GridState<T, O>
where
T: Ord,
O: AStarCost,
{
fn cmp(&self, other: &Self) -> Ordering {
compare_option_scores(other.f_score, self.f_score).then_with(|| {
compare_option_scores(self.g_score, other.g_score).then_with(|| {
self.position.cmp(&other.position)
})
})
}
}
impl<T, O> PartialOrd for GridState<T, O>
where
T: Ord,
O: AStarCost,
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
fn expand_neighbors<T: AStarNode, O: AStarCost>(
start: T,
neighbors: &[(T, O, O)],
heap: &mut BinaryHeap<State<T, O>>,
parents: &mut HashMap<T, T>,
) {
for (neighbor, next_f_score, next_g_score) in neighbors {
if let Entry::Vacant(v) = parents.entry(*neighbor) {
v.insert(start);
heap.push(State {
g_score: *next_g_score,
f_score: *next_f_score,
position: *neighbor,
});
}
}
}
#[allow(clippy::too_many_arguments)]
fn expand_grid_neighbors<T: AStarGridNode, G, F, O>(
start: T,
g_score: O,
neighbors: &[(T, Direction)],
cost_fn: G,
heuristic_fn: F,
heap: &mut BinaryHeap<GridState<T, O>>,
lowest_seen_g_scores: &mut HashMap<T, O>,
parents: &mut HashMap<T, T>,
) where
G: Fn(T, T) -> Option<O>,
F: Fn(T) -> O,
O: AStarCost,
{
for (neighbor, direction) in neighbors {
if let Some(next_tile_cost) = cost_fn(start, *neighbor) {
if let Some(next_g_score) = g_score.checked_add(&next_tile_cost) {
let add_state_to_heap: bool = match parents.entry(*neighbor) {
Entry::Occupied(mut v) => {
match lowest_seen_g_scores.entry(*neighbor) {
Entry::Vacant(score_entry) => {
score_entry.insert(next_g_score);
v.insert(start);
true
}
Entry::Occupied(mut score_entry) => {
if next_g_score < *score_entry.get() {
let _ = score_entry.insert(next_g_score);
let _ = v.insert(start);
true
} else {
false
}
}
}
}
Entry::Vacant(v) => {
lowest_seen_g_scores.insert(*neighbor, next_g_score);
v.insert(start);
true
}
};
if add_state_to_heap {
let raw_h_score = heuristic_fn(*neighbor);
let h_score = raw_h_score;
if let Some(f_score) = next_g_score.checked_add(&h_score) {
heap.push(GridState {
g_score: Some(next_g_score),
f_score: Some(f_score),
position: *neighbor,
open_direction: Some(*direction),
});
} else {
continue;
}
}
} else {
continue;
}
} else {
continue;
}
}
}
pub fn shortest_path_generic_grid<T: AStarGridNode, P, G, F, O>(
start: &[T],
goal_fn: &P,
cost_fn: G,
heuristic_fn: F,
max_ops: u32,
max_cost: O,
initial_cost: O,
) -> AStarSearchResults<T, O>
where
P: Fn(T) -> bool,
G: Fn(T, T) -> Option<O>,
F: Fn(T) -> O,
O: AStarCost,
{
let mut remaining_ops: u32 = max_ops;
let mut best_reached = start[0];
let mut best_reached_f_score: Option<O> = None;
let all_directions_iter = Direction::iter();
let all_directions = all_directions_iter.as_slice();
let mut parents: HashMap<T, T> = HashMap::new();
let mut heap = BinaryHeap::new();
let mut lowest_seen_g_scores: HashMap<T, O> = HashMap::new();
for s in start.iter().copied() {
let initial_open_entry = GridState {
g_score: Some(initial_cost),
f_score: Some(heuristic_fn(s)),
position: s,
open_direction: None,
};
heap.push(initial_open_entry);
lowest_seen_g_scores.insert(s, initial_cost);
}
while let Some(GridState {
g_score: g_score_opt,
position,
f_score: f_score_opt,
open_direction,
}) = heap.pop()
{
if goal_fn(position) {
let path_opt = get_path_from_parents(&parents, position);
return AStarSearchResults {
ops_used: max_ops - remaining_ops,
cost: g_score_opt,
incomplete: false,
path: path_opt.unwrap_or_else(|| Vec::new()),
};
}
if g_score_opt.is_none() {
continue;
}
let g_score = g_score_opt.unwrap();
if g_score >= max_cost {
continue;
}
let should_skip_node: bool = match lowest_seen_g_scores.entry(position) {
Entry::Vacant(_) => {
false
}
Entry::Occupied(score_entry) => {
if g_score > *score_entry.get() {
true
} else {
false
}
}
};
if should_skip_node {
continue;
}
if f_score_opt.is_none() {
continue;
}
let f_score = f_score_opt.unwrap();
if best_reached_f_score.is_none_or(|brfs| f_score < brfs) {
best_reached = position;
best_reached_f_score = Some(f_score);
}
remaining_ops -= 1;
if remaining_ops == 0 {
break;
}
let directions: &[Direction] = if let Some(open_direction) = open_direction {
if open_direction.is_diagonal() {
&[
open_direction,
open_direction.multi_rot(1),
open_direction.multi_rot(-1),
open_direction.multi_rot(2),
open_direction.multi_rot(-2),
]
} else {
&[
open_direction,
open_direction.multi_rot(1),
open_direction.multi_rot(-1),
]
}
} else {
all_directions
};
let neighbors: Vec<(T, Direction)> = directions
.iter()
.map(|d| (position.checked_add_direction(*d), *d))
.filter(|(opt, _)| opt.is_some())
.map(|(opt, d)| (opt.unwrap(), d))
.collect();
expand_grid_neighbors(
position,
g_score,
&neighbors,
&cost_fn,
&heuristic_fn,
&mut heap,
&mut lowest_seen_g_scores,
&mut parents,
);
}
let path_opt = get_path_from_parents(&parents, best_reached);
AStarSearchResults {
ops_used: max_ops - remaining_ops,
cost: best_reached_f_score,
incomplete: true,
path: path_opt.unwrap_or_else(|| Vec::new()),
}
}
pub fn shortest_path_generic_graph<T: AStarNode, P, N, G, F, O>(
start: &[T],
goal_fn: &P,
neighbors_fn: &N,
max_ops: u32,
max_cost: O,
initial_cost: O,
initial_estimate: O,
) -> AStarSearchResults<T, O>
where
P: Fn(T) -> bool,
N: Fn(T, O) -> Vec<(T, O, O)>,
O: AStarCost,
{
let mut remaining_ops: u32 = max_ops;
let mut best_reached = start[0];
let mut best_reached_f_score = initial_estimate;
let mut parents: HashMap<T, T> = HashMap::new();
let mut heap = BinaryHeap::new();
for s in start.iter().copied() {
let initial_open_entry = State {
g_score: initial_cost,
f_score: initial_estimate,
position: s,
};
heap.push(initial_open_entry);
}
while let Some(State {
g_score,
f_score,
position,
}) = heap.pop()
{
if goal_fn(position) {
let path_opt = get_path_from_parents(&parents, position);
return AStarSearchResults {
ops_used: max_ops - remaining_ops,
cost: Some(g_score),
incomplete: false,
path: path_opt.unwrap_or_else(|| Vec::new()),
};
}
if g_score >= max_cost {
continue;
}
if f_score < best_reached_f_score {
best_reached = position;
best_reached_f_score = f_score;
}
remaining_ops -= 1;
if remaining_ops == 0 {
break;
}
let neighbors: Vec<(T, O, O)> = neighbors_fn(position, g_score);
expand_neighbors(position, &neighbors, &mut heap, &mut parents);
}
let path_opt = get_path_from_parents(&parents, best_reached);
AStarSearchResults {
ops_used: max_ops - remaining_ops,
cost: Some(best_reached_f_score),
incomplete: true,
path: path_opt.unwrap_or_else(|| Vec::new()),
}
}
fn get_path_from_parents<T: AStarNode>(parents: &HashMap<T, T>, end: T) -> Option<Vec<T>> {
let mut path = Vec::new();
let mut current_pos = end;
path.push(end);
let mut parent_opt = parents.get(¤t_pos);
while parent_opt.is_some() {
let parent = parent_opt.unwrap();
path.push(*parent);
current_pos = *parent;
parent_opt = parents.get(¤t_pos);
}
Some(path.into_iter().rev().collect())
}
pub fn shortest_path_roomxy_single_goal<G>(
start: RoomXY,
goal: RoomXY,
cost_fn: G,
) -> AStarSearchResults<RoomXY, u32>
where
G: Fn(RoomXY) -> Option<u32>,
{
shortest_path_roomxy_multiple_goals(start, &[goal], cost_fn)
}
pub fn shortest_path_roomxy_multiple_goals<G>(
start: RoomXY,
goals: &[RoomXY],
cost_fn: G,
) -> AStarSearchResults<RoomXY, u32>
where
G: Fn(RoomXY) -> Option<u32>,
{
shortest_path_roomxy_multistart(
&[start],
&goal_exact_node_multigoal(goals),
cost_fn,
&heuristic_get_range_to_multigoal(goals),
)
}
pub fn shortest_path_roomxy<P, G, F>(
start: RoomXY,
goal_fn: &P,
cost_fn: G,
heuristic_fn: &F,
) -> AStarSearchResults<RoomXY, u32>
where
P: Fn(RoomXY) -> bool,
G: Fn(RoomXY) -> Option<u32>,
F: Fn(RoomXY) -> u32,
{
shortest_path_roomxy_multistart(&[start], goal_fn, cost_fn, heuristic_fn)
}
pub fn shortest_path_roomxy_multistart<P, G, F>(
start_nodes: &[RoomXY],
goal_fn: &P,
cost_fn: G,
heuristic_fn: &F,
) -> AStarSearchResults<RoomXY, u32>
where
P: Fn(RoomXY) -> bool,
G: Fn(RoomXY) -> Option<u32>,
F: Fn(RoomXY) -> u32,
{
let max_ops = 2000;
let max_cost = 2000;
let new_cost_fn = ignore_first_param_cost_fn(cost_fn);
shortest_path_generic_grid(
start_nodes,
goal_fn,
new_cost_fn,
heuristic_fn,
max_ops,
max_cost,
0,
)
}
pub fn shortest_path_position_single_goal<G>(
start: Position,
goal: Position,
cost_fn: G,
) -> AStarSearchResults<Position, u32>
where
G: Fn(Position) -> Option<u32>,
{
shortest_path_position_multiple_goals(start, &[goal], cost_fn)
}
pub fn shortest_path_position_multiple_goals<G>(
start: Position,
goals: &[Position],
cost_fn: G,
) -> AStarSearchResults<Position, u32>
where
G: Fn(Position) -> Option<u32>,
{
shortest_path_position_multistart(
&[start],
&goal_exact_node_multigoal(goals),
cost_fn,
&heuristic_get_range_to_multigoal(goals),
)
}
pub fn shortest_path_position<P, G, F>(
start: Position,
goal_fn: &P,
cost_fn: G,
heuristic_fn: &F,
) -> AStarSearchResults<Position, u32>
where
P: Fn(Position) -> bool,
G: Fn(Position) -> Option<u32>,
F: Fn(Position) -> u32,
{
shortest_path_position_multistart(&[start], goal_fn, cost_fn, heuristic_fn)
}
pub fn shortest_path_position_multistart<P, G, F>(
start_nodes: &[Position],
goal_fn: &P,
cost_fn: G,
heuristic_fn: &F,
) -> AStarSearchResults<Position, u32>
where
P: Fn(Position) -> bool,
G: Fn(Position) -> Option<u32>,
F: Fn(Position) -> u32,
{
let max_ops = 2000;
let max_cost = 2000;
let new_cost_fn = ignore_first_param_cost_fn(cost_fn);
shortest_path_generic_grid(
start_nodes,
goal_fn,
new_cost_fn,
heuristic_fn,
max_ops,
max_cost,
0,
)
}
fn ignore_first_param_cost_fn<G, T, O>(cost_fn: G) -> impl Fn(T, T) -> O
where
G: Fn(T) -> O,
{
move |_, p| cost_fn(p)
}
#[allow(dead_code)]
fn optionize_cost_fn_results<G, T, O>(cost_fn: G) -> impl Fn(T, T) -> Option<O>
where
G: Fn(T, T) -> O,
{
move |a, b| Some(cost_fn(a, b))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::heuristics::heuristic_get_range_to;
use screeps::constants::Terrain;
use screeps::local::{Position, RoomCoordinate, RoomName, RoomXY};
use crate::common::data::load_all_room_terrains_from_map;
use itertools::Itertools;
fn new_position(room_name: &str, x: u8, y: u8) -> Position {
Position::new(
RoomCoordinate::try_from(x).unwrap(),
RoomCoordinate::try_from(y).unwrap(),
room_name.parse().unwrap(),
)
}
fn all_tiles_are_plains_costs<T>(_prev: T, _node: T) -> Option<u32> {
Some(1)
}
fn all_tiles_are_swamps_costs<T>(_prev: T, _node: T) -> Option<u32> {
Some(5)
}
fn is_goal_fn<T: std::cmp::PartialEq>(goal: T) -> impl Fn(T) -> bool {
move |node: T| node == goal
}
fn roomxy_unreachable_tile_costs(_prev: RoomXY, node: RoomXY) -> Option<u32> {
if node.x.u8() == 10 && node.y.u8() == 12 {
None
} else {
Some(1)
}
}
fn position_unreachable_tile_costs(_prev: Position, node: Position) -> Option<u32> {
if node.x().u8() == 10 && node.y().u8() == 12 {
None
} else {
Some(1)
}
}
#[test]
fn compare_option_scores_orders_properly() {
let res = compare_option_scores::<u8>(None, None);
assert_eq!(res, Ordering::Equal);
for i in 0..u8::MAX {
let a = i;
let b = i + 1;
let res = compare_option_scores(Some(a), None);
assert_eq!(res, Ordering::Less);
let res = compare_option_scores(None, Some(a));
assert_eq!(res, Ordering::Greater);
let res = compare_option_scores(Some(a), Some(b));
assert_eq!(res, Ordering::Less);
let res = compare_option_scores(Some(b), Some(a));
assert_eq!(res, Ordering::Greater);
let res = compare_option_scores(Some(a), Some(a));
assert_eq!(res, Ordering::Equal);
}
}
#[test]
fn state_orders_comparisons_by_f_score_descending() {
let large_g_score: u8 = 100;
let small_g_score: u8 = 5;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, small_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, small_position),
(large_g_score, large_g_score, large_position, large_position),
];
for i in 0..u8::MAX {
let low_f_score = i;
let high_f_score = i + 1;
for (a_g_score, b_g_score, a_pos, b_pos) in irrelevant_score_orderings {
let a = State {
g_score: a_g_score,
f_score: low_f_score,
position: a_pos,
};
let b = State {
g_score: b_g_score,
f_score: high_f_score,
position: b_pos,
};
let res = a.cmp(&b);
assert_eq!(res, Ordering::Greater);
let res = b.cmp(&a);
assert_eq!(res, Ordering::Less);
}
}
}
#[test]
fn state_orders_comparisons_by_g_score_descending_for_equal_f_scores() {
let f_score: u8 = 50;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_position, large_position),
(large_position, small_position),
(large_position, large_position),
];
for i in 0..u8::MAX {
let low_g_score = i;
let high_g_score = i + 1;
for (a_pos, b_pos) in irrelevant_score_orderings {
let a = State {
g_score: low_g_score,
f_score: f_score,
position: a_pos,
};
let b = State {
g_score: high_g_score,
f_score: f_score,
position: b_pos,
};
let res = a.cmp(&b);
assert_eq!(res, Ordering::Less);
let res = b.cmp(&a);
assert_eq!(res, Ordering::Greater);
}
}
}
#[test]
fn state_orders_comparisons_by_position_descending_for_equal_f_and_g_scores() {
let f_score: u8 = 50;
let g_score: u8 = 5;
for i in 0..u8::MAX {
let low_pos = i;
let high_pos = i + 1;
let a = State {
g_score: g_score,
f_score: f_score,
position: low_pos,
};
let b = State {
g_score: g_score,
f_score: f_score,
position: high_pos,
};
let res = a.cmp(&b);
assert_eq!(res, Ordering::Less);
let res = b.cmp(&a);
assert_eq!(res, Ordering::Greater);
}
}
#[test]
fn gridstate_orders_comparisons_by_f_score_descending() {
let large_g_score: u8 = 100;
let small_g_score: u8 = 5;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, small_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, small_position),
(large_g_score, large_g_score, large_position, large_position),
];
for i in 0..u8::MAX {
let low_f_score = i;
let high_f_score = i + 1;
for (a_g_score, b_g_score, a_pos, b_pos) in irrelevant_score_orderings {
let a = GridState {
g_score: Some(a_g_score),
f_score: Some(low_f_score),
position: a_pos,
open_direction: None,
};
let b = GridState {
g_score: Some(b_g_score),
f_score: Some(high_f_score),
position: b_pos,
open_direction: None,
};
let res = a.cmp(&b);
assert_eq!(res, Ordering::Greater);
let res = b.cmp(&a);
assert_eq!(res, Ordering::Less);
}
}
}
#[test]
fn gridstate_orders_comparisons_by_g_score_descending_for_equal_f_scores() {
let f_score: u8 = 50;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_position, large_position),
(large_position, small_position),
(large_position, large_position),
];
for i in 0..u8::MAX {
let low_g_score = i;
let high_g_score = i + 1;
for (a_pos, b_pos) in irrelevant_score_orderings {
let a = GridState {
g_score: Some(low_g_score),
f_score: Some(f_score),
position: a_pos,
open_direction: None,
};
let b = GridState {
g_score: Some(high_g_score),
f_score: Some(f_score),
position: b_pos,
open_direction: None,
};
let res = a.cmp(&b);
assert_eq!(res, Ordering::Less);
let res = b.cmp(&a);
assert_eq!(res, Ordering::Greater);
}
}
}
#[test]
fn gridstate_orders_comparisons_by_position_ascending_for_equal_f_and_g_scores() {
let f_score: u8 = 50;
let g_score: u8 = 5;
for i in 0..u8::MAX {
let low_pos = i;
let high_pos = i + 1;
let a = GridState {
g_score: Some(g_score),
f_score: Some(f_score),
position: low_pos,
open_direction: None,
};
let b = GridState {
g_score: Some(g_score),
f_score: Some(f_score),
position: high_pos,
open_direction: None,
};
let res = a.cmp(&b);
assert_eq!(res, Ordering::Less);
let res = b.cmp(&a);
assert_eq!(res, Ordering::Greater);
}
}
#[test]
fn binary_heap_orders_state_by_f_score_descending() {
let large_g_score: u8 = 100;
let small_g_score: u8 = 5;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, small_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, small_position),
(large_g_score, large_g_score, large_position, large_position),
];
for i in 0..u8::MAX {
let low_f_score = i;
let high_f_score = i + 1;
for (a_g_score, b_g_score, a_pos, b_pos) in irrelevant_score_orderings {
let a = State {
g_score: a_g_score,
f_score: low_f_score,
position: a_pos,
};
let b = State {
g_score: b_g_score,
f_score: high_f_score,
position: b_pos,
};
let mut heap = BinaryHeap::from([a, b]);
assert_eq!(heap.pop(), Some(a));
assert_eq!(heap.pop(), Some(b));
}
}
}
#[test]
fn binary_heap_orders_state_by_g_score_descending_for_equal_f_scores() {
let f_score: u8 = 50;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_position, large_position),
(large_position, small_position),
(large_position, large_position),
];
for i in 0..u8::MAX {
let low_g_score = i;
let high_g_score = i + 1;
for (a_pos, b_pos) in irrelevant_score_orderings {
let a = State {
g_score: low_g_score,
f_score: f_score,
position: a_pos,
};
let b = State {
g_score: high_g_score,
f_score: f_score,
position: b_pos,
};
let mut heap = BinaryHeap::from([a, b]);
assert_eq!(heap.pop(), Some(b));
assert_eq!(heap.pop(), Some(a));
}
}
}
#[test]
fn binary_heap_orders_state_by_position_ascending_for_equal_f_and_g_scores() {
let f_score: u8 = 50;
let g_score: u8 = 5;
for i in 0..u8::MAX {
let low_pos = i;
let high_pos = i + 1;
let a = State {
g_score: g_score,
f_score: f_score,
position: low_pos,
};
let b = State {
g_score: g_score,
f_score: f_score,
position: high_pos,
};
let mut heap = BinaryHeap::from([a, b]);
assert_eq!(heap.pop(), Some(b));
assert_eq!(heap.pop(), Some(a));
}
}
#[test]
fn binary_heap_orders_gridstate_by_f_score_descending() {
let large_g_score: u8 = 100;
let small_g_score: u8 = 5;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, small_position, large_position),
(small_g_score, large_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, small_g_score, large_position, small_position),
(large_g_score, small_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, large_position),
(large_g_score, large_g_score, large_position, small_position),
(large_g_score, large_g_score, large_position, large_position),
];
for i in 0..u8::MAX {
let low_f_score = i;
let high_f_score = i + 1;
for (a_g_score, b_g_score, a_pos, b_pos) in irrelevant_score_orderings {
let a = GridState {
g_score: Some(a_g_score),
f_score: Some(low_f_score),
position: a_pos,
open_direction: None,
};
let b = GridState {
g_score: Some(b_g_score),
f_score: Some(high_f_score),
position: b_pos,
open_direction: None,
};
let mut heap = BinaryHeap::from([a, b]);
assert_eq!(heap.pop(), Some(a));
assert_eq!(heap.pop(), Some(b));
}
}
}
#[test]
fn binary_heap_orders_gridstate_by_g_score_descending_for_equal_f_scores() {
let f_score: u8 = 50;
let large_position: u8 = 40;
let small_position: u8 = 4;
let irrelevant_score_orderings = [
(small_position, large_position),
(large_position, small_position),
(large_position, large_position),
];
for i in 0..u8::MAX {
let low_g_score = i;
let high_g_score = i + 1;
for (a_pos, b_pos) in irrelevant_score_orderings {
let a = GridState {
g_score: Some(low_g_score),
f_score: Some(f_score),
position: a_pos,
open_direction: None,
};
let b = GridState {
g_score: Some(high_g_score),
f_score: Some(f_score),
position: b_pos,
open_direction: None,
};
let mut heap = BinaryHeap::from([a, b]);
assert_eq!(heap.pop(), Some(b));
assert_eq!(heap.pop(), Some(a));
}
}
}
#[test]
fn binary_heap_orders_gridstate_by_position_ascending_for_equal_f_and_g_scores() {
let f_score: u8 = 50;
let g_score: u8 = 5;
for i in 0..u8::MAX {
let low_pos = i;
let high_pos = i + 1;
let a = GridState {
g_score: Some(g_score),
f_score: Some(f_score),
position: low_pos,
open_direction: None,
};
let b = GridState {
g_score: Some(g_score),
f_score: Some(f_score),
position: high_pos,
open_direction: None,
};
let mut heap = BinaryHeap::from([a, b]);
assert_eq!(heap.pop(), Some(b));
assert_eq!(heap.pop(), Some(a));
}
}
#[test]
fn simple_linear_path_roomxy() {
let start = unsafe { RoomXY::unchecked_new(10, 10) };
let goal = unsafe { RoomXY::unchecked_new(10, 12) };
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_plains_costs,
heuristic_get_range_to(goal),
2000,
2000,
0,
);
assert_eq!(search_results.incomplete(), false);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap(), 2);
assert_eq!(search_results.ops() < 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 3);
let middle_node_1 = unsafe { RoomXY::unchecked_new(10, 11) };
let middle_node_2 = unsafe { RoomXY::unchecked_new(11, 11) };
let middle_node_3 = unsafe { RoomXY::unchecked_new(11, 10) };
assert_eq!(path.contains(&start), true);
assert_eq!(path.contains(&goal), true);
let contains_a_middle_node = path.contains(&middle_node_1)
| path.contains(&middle_node_2)
| path.contains(&middle_node_3);
assert_eq!(contains_a_middle_node, true);
}
#[test]
fn simple_linear_path_position() {
let room_name = "E5N6";
let start = new_position(room_name, 10, 10);
let goal = new_position(room_name, 10, 12);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_plains_costs,
heuristic_get_range_to(goal),
2000,
2000,
0,
);
assert_eq!(search_results.incomplete(), false);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap(), 2);
assert_eq!(search_results.ops() < 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 3);
let middle_node_1 = new_position(room_name, 10, 11);
let middle_node_2 = new_position(room_name, 11, 11);
let middle_node_3 = new_position(room_name, 11, 10);
assert_eq!(path.contains(&start), true);
assert_eq!(path.contains(&goal), true);
let contains_a_middle_node = path.contains(&middle_node_1)
| path.contains(&middle_node_2)
| path.contains(&middle_node_3);
assert_eq!(contains_a_middle_node, true);
}
#[test]
fn unreachable_target_roomxy() {
let start = unsafe { RoomXY::unchecked_new(10, 10) };
let goal = unsafe { RoomXY::unchecked_new(10, 12) };
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
roomxy_unreachable_tile_costs,
heuristic_get_range_to(goal),
2000,
2000,
0,
);
println!("{:?}", search_results);
assert_eq!(search_results.incomplete(), true);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap() > 0, true);
assert_eq!(search_results.ops() == 2000, true);
}
#[test]
fn unreachable_target_position() {
let room_name = "E5N6";
let start = new_position(room_name, 10, 10);
let goal = new_position(room_name, 10, 12);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
position_unreachable_tile_costs,
heuristic_get_range_to(goal),
2000,
2000,
0,
);
println!("{:?}", search_results);
assert_eq!(search_results.incomplete(), true);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap() > 0, true);
assert_eq!(search_results.ops() > 0, true);
}
#[test]
fn max_ops_halt_roomxy() {
let max_ops_failure = 5;
let max_ops_success = 100;
let start = unsafe { RoomXY::unchecked_new(10, 10) };
let goal = unsafe { RoomXY::unchecked_new(30, 30) };
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_plains_costs,
heuristic_get_range_to(goal),
max_ops_failure,
2000,
0,
);
assert_eq!(search_results.incomplete(), true);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap() > 0, true);
assert_eq!(search_results.ops() == max_ops_failure, true);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_plains_costs,
heuristic_get_range_to(goal),
max_ops_success,
2000,
0,
);
assert_eq!(search_results.incomplete(), false);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap() > 0, true);
assert_eq!(search_results.ops() < max_ops_success, true);
let path = search_results.path();
assert_eq!(path.len(), 21);
}
#[test]
fn max_ops_halt_position() {
let max_ops_failure = 5;
let max_ops_success = 100;
let room_name = "E5N6";
let start = new_position(room_name, 10, 10);
let goal = new_position(room_name, 30, 30);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_plains_costs,
heuristic_get_range_to(goal),
max_ops_failure,
2000,
0,
);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.cost().unwrap() > 0, true);
assert_eq!(search_results.ops() == max_ops_failure, true);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_plains_costs,
heuristic_get_range_to(goal),
max_ops_success,
2000,
0,
);
assert_eq!(search_results.incomplete(), false);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap() > 0, true);
assert_eq!(search_results.ops() < max_ops_success, true);
let path = search_results.path();
assert_eq!(path.len(), 21);
}
#[test]
fn max_cost_halt_roomxy() {
let max_cost_failure = 5;
let max_cost_success = 100;
let start = unsafe { RoomXY::unchecked_new(10, 10) };
let goal = unsafe { RoomXY::unchecked_new(10, 12) };
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_swamps_costs,
heuristic_get_range_to(goal),
2000,
max_cost_failure,
0,
);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.ops() < 2000, true);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_swamps_costs,
heuristic_get_range_to(goal),
2000,
max_cost_success,
0,
);
assert_eq!(search_results.incomplete(), false);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap() < max_cost_success, true);
assert_eq!(search_results.ops() < 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 3);
}
#[test]
fn max_cost_halt_position() {
let max_cost_failure = 5;
let max_cost_success = 100;
let room_name = "E5N6";
let start = new_position(room_name, 10, 10);
let goal = new_position(room_name, 10, 12);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_swamps_costs,
heuristic_get_range_to(goal),
2000,
max_cost_failure,
0,
);
println!("{:?}", search_results);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.ops() < 2000, true);
let search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
all_tiles_are_swamps_costs,
heuristic_get_range_to(goal),
2000,
max_cost_success,
0,
);
assert_eq!(search_results.incomplete(), false);
assert!(search_results.cost().is_some());
assert_eq!(search_results.cost().unwrap() < max_cost_success, true);
assert_eq!(search_results.ops() < 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 3);
}
#[test]
fn validate_path_is_optimal_for_mmo_shard3_W49N34() {
let terrains_map = load_all_room_terrains_from_map("test-rooms.json");
let room_name_str = "W49N34";
let room_name = RoomName::new(room_name_str).unwrap();
let terrain_data = terrains_map.get(&room_name).unwrap();
let start = unsafe { RoomXY::unchecked_new(6, 0) }; let goal = unsafe { RoomXY::unchecked_new(40, 49) };
let plain_cost = 1;
let swamp_cost = 5;
let costs = crate::utils::movement_costs::get_movement_cost_lcm_from_terrain(
&terrain_data,
plain_cost,
swamp_cost,
);
let costs_fn = crate::utils::movement_costs::movement_costs_from_lcm(&costs);
let neighbors_fn = crate::utils::neighbors::room_xy_neighbors;
let max_ops = 2000;
let max_cost = 2000;
let dijkstra_search_results = crate::algorithms::dijkstra::shortest_path_generic(
&[start],
&is_goal_fn(goal),
&costs_fn,
neighbors_fn,
max_ops,
max_cost,
);
assert_eq!(dijkstra_search_results.incomplete(), false);
let dijkstra_path = dijkstra_search_results.path();
let new_cost_fn = optionize_cost_fn_results(ignore_first_param_cost_fn(costs_fn));
let astar_search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
new_cost_fn,
heuristic_get_range_to(goal),
max_ops,
max_cost,
0,
);
assert_eq!(astar_search_results.incomplete(), false);
let astar_path = astar_search_results.path();
assert_eq!(
astar_search_results.cost().unwrap(),
dijkstra_search_results.cost()
);
}
#[test]
fn validate_path_is_optimal_for_mmo_shard3_W49N34_known_edge_case() {
let terrains_map = load_all_room_terrains_from_map("test-rooms.json");
let room_name_str = "W49N34";
let room_name = RoomName::new(room_name_str).unwrap();
let terrain_data = terrains_map.get(&room_name).unwrap();
let start = unsafe { RoomXY::unchecked_new(6, 0) };
let goal = unsafe { RoomXY::unchecked_new(32, 5) };
let plain_cost = 1;
let swamp_cost = 5;
let costs = crate::utils::movement_costs::get_movement_cost_lcm_from_terrain(
&terrain_data,
plain_cost,
swamp_cost,
);
let costs_fn = crate::utils::movement_costs::movement_costs_from_lcm(&costs);
let neighbors_fn = crate::utils::neighbors::room_xy_neighbors;
let max_ops = 2000;
let max_cost = 2000;
let dijkstra_search_results = crate::algorithms::dijkstra::shortest_path_generic(
&[start],
&is_goal_fn(goal),
&costs_fn,
neighbors_fn,
max_ops,
max_cost,
);
assert_eq!(dijkstra_search_results.incomplete(), false);
let dijkstra_path = dijkstra_search_results.path();
let new_cost_fn = optionize_cost_fn_results(ignore_first_param_cost_fn(costs_fn));
let astar_search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
new_cost_fn,
heuristic_get_range_to(goal),
max_ops,
max_cost,
0,
);
assert_eq!(astar_search_results.incomplete(), false);
let astar_path = astar_search_results.path();
assert_eq!(
astar_search_results.cost().unwrap(),
dijkstra_search_results.cost()
);
}
#[test]
#[ignore]
fn validate_path_is_optimal_for_mmo_shard3_arbitrary_rooms() {
let terrains_map = load_all_room_terrains_from_map("test-rooms.json");
let mut skipped_because_plains = 0;
let mut num_rooms = 0;
let room_name_str = "W49N34";
let room_name = RoomName::new(room_name_str).unwrap();
let tmp = vec![terrains_map.get(&room_name).unwrap()];
for terrain_data in tmp {
num_rooms += 1;
let plains_tiles: Vec<RoomXY> = (0..50)
.cartesian_product(0..50)
.map(|(y, x)| unsafe { RoomXY::unchecked_new(x, y) })
.filter(|pos| match terrain_data.get_xy(*pos) {
Terrain::Plain => true,
_ => false,
})
.collect();
if plains_tiles.is_empty() {
skipped_because_plains += 1;
continue;
}
let total_length = plains_tiles.len();
let second_half_start = total_length / 2;
let start_positions = &plains_tiles[0..second_half_start];
let goal_positions = &plains_tiles[second_half_start..total_length];
for (start_ref, goal_ref) in start_positions.iter().cartesian_product(goal_positions) {
let start = *start_ref;
let goal = *goal_ref;
if start == goal {
continue;
}
let plain_cost = 1;
let swamp_cost = 5;
let costs = crate::utils::movement_costs::get_movement_cost_lcm_from_terrain(
&terrain_data,
plain_cost,
swamp_cost,
);
let costs_fn = crate::utils::movement_costs::movement_costs_from_lcm(&costs);
let neighbors_fn = crate::utils::neighbors::room_xy_neighbors;
let max_ops = 2000;
let max_cost = 2000;
let dijkstra_search_results = crate::algorithms::dijkstra::shortest_path_generic(
&[start],
&is_goal_fn(goal),
&costs_fn,
neighbors_fn,
max_ops,
max_cost,
);
assert_eq!(dijkstra_search_results.incomplete(), false);
let dijkstra_path = dijkstra_search_results.path();
let new_cost_fn = optionize_cost_fn_results(ignore_first_param_cost_fn(costs_fn));
let always_one_heuristic = |_| 1;
let astar_search_results = shortest_path_generic_grid(
&[start],
&is_goal_fn(goal),
new_cost_fn,
always_one_heuristic,
max_ops,
max_cost,
0,
);
assert_eq!(astar_search_results.incomplete(), false);
let astar_path = astar_search_results.path();
assert_eq!(
astar_search_results.cost().unwrap(),
dijkstra_search_results.cost()
);
}
}
assert!(skipped_because_plains < num_rooms);
}
}