use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
use std::hash::Hash;
use screeps::local::{Position, RoomXY};
pub trait DijkstraNode: Eq + Hash + Copy + Ord {}
impl<T> DijkstraNode for T where T: Eq + Hash + Copy + Ord {}
#[derive(Debug)]
pub struct DijkstraSearchResults<T>
where
T: DijkstraNode,
{
ops_used: u32,
cost: u32,
incomplete: bool,
path: Vec<T>,
}
impl<T: DijkstraNode> DijkstraSearchResults<T> {
pub fn ops(&self) -> u32 {
self.ops_used
}
pub fn cost(&self) -> u32 {
self.cost
}
pub fn incomplete(&self) -> bool {
self.incomplete
}
pub fn path(&self) -> &[T] {
&self.path
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct State<T>
where
T: Ord,
{
cost: u32,
position: T,
}
impl<T> Ord for State<T>
where
T: Ord,
{
fn cmp(&self, other: &Self) -> Ordering {
other
.cost
.cmp(&self.cost)
.then_with(|| self.position.cmp(&other.position))
}
}
impl<T> PartialOrd for State<T>
where
T: Ord,
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
pub fn shortest_path_generic<T: DijkstraNode, P, G, N, I>(
start: &[T],
goal_fn: &P,
cost_fn: G,
neighbors: N,
max_ops: u32,
max_cost: u32,
) -> DijkstraSearchResults<T>
where
P: Fn(T) -> bool,
G: Fn(T) -> u32,
N: Fn(T) -> I,
I: IntoIterator<Item = T, IntoIter: Iterator<Item = T>>,
{
let mut remaining_ops: u32 = max_ops;
let mut last_examined_cost: u32 = 0;
let mut dist: HashMap<T, u32> = HashMap::new();
let mut parents: HashMap<T, T> = HashMap::new();
let mut heap = BinaryHeap::new();
for s in start {
dist.insert(*s, 0);
heap.push(State {
cost: 0,
position: *s,
});
}
while let Some(State { cost, position }) = heap.pop() {
if goal_fn(position) {
let path_opt = get_path_from_parents(&parents, position);
return DijkstraSearchResults {
ops_used: max_ops - remaining_ops,
cost,
incomplete: path_opt.is_none(),
path: path_opt.unwrap_or_else(|| Vec::new()),
};
}
remaining_ops -= 1;
if remaining_ops == 0 {
break;
}
last_examined_cost = cost;
if cost >= max_cost {
break;
}
let current_cost = match dist.get(&position) {
Some(c) => c,
None => &u32::MAX,
};
if cost > *current_cost {
continue;
}
for p in neighbors(position) {
let next_tile_cost = cost_fn(p);
if next_tile_cost == u32::MAX {
continue;
}
let next = State {
cost: cost + next_tile_cost,
position: p,
};
let current_next_cost = match dist.get(&next.position) {
Some(c) => c,
None => &u32::MAX,
};
if next.cost < *current_next_cost {
heap.push(next);
if let Some(c) = dist.get_mut(&next.position) {
*c = next.cost;
} else {
dist.insert(next.position, next.cost);
}
if let Some(parent_node) = parents.get_mut(&next.position) {
*parent_node = position;
} else {
parents.insert(next.position, position);
}
}
}
}
DijkstraSearchResults {
ops_used: max_ops - remaining_ops,
cost: last_examined_cost,
incomplete: true,
path: Vec::new(),
}
}
fn get_path_from_parents<T: DijkstraNode>(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<P, G>(
start: RoomXY,
goal_fn: &P,
cost_fn: G,
) -> DijkstraSearchResults<RoomXY>
where
P: Fn(RoomXY) -> bool,
G: Fn(RoomXY) -> u32,
{
shortest_path_roomxy_multistart(&[start], goal_fn, cost_fn)
}
pub fn shortest_path_roomxy_multistart<P, G>(
start_nodes: &[RoomXY],
goal_fn: &P,
cost_fn: G,
) -> DijkstraSearchResults<RoomXY>
where
P: Fn(RoomXY) -> bool,
G: Fn(RoomXY) -> u32,
{
let neighbors_fn = crate::utils::neighbors::room_xy_neighbors;
let max_ops = 2000;
let max_cost = 2000;
shortest_path_generic(
start_nodes,
goal_fn,
cost_fn,
neighbors_fn,
max_ops,
max_cost,
)
}
pub fn shortest_path_position<P, G>(
start: Position,
goal_fn: &P,
cost_fn: G,
) -> DijkstraSearchResults<Position>
where
P: Fn(Position) -> bool,
G: Fn(Position) -> u32,
{
shortest_path_position_multistart(&[start], goal_fn, cost_fn)
}
pub fn shortest_path_position_multistart<P, G>(
start_nodes: &[Position],
goal_fn: &P,
cost_fn: G,
) -> DijkstraSearchResults<Position>
where
P: Fn(Position) -> bool,
G: Fn(Position) -> u32,
{
let neighbors_fn = crate::utils::neighbors::position_neighbors;
let max_ops = 2000;
let max_cost = 2000;
shortest_path_generic(
start_nodes,
goal_fn,
cost_fn,
neighbors_fn,
max_ops,
max_cost,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::goals::goal_exact_node;
use screeps::constants::Direction;
use screeps::local::{Position, RoomCoordinate, RoomXY};
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>(_node: T) -> u32 {
1
}
fn all_tiles_are_swamps_costs<T>(_node: T) -> u32 {
5
}
fn room_xy_neighbors(node: RoomXY) -> Vec<RoomXY> {
node.neighbors()
}
fn position_neighbors(node: Position) -> Vec<Position> {
Direction::iter()
.filter_map(|dir| node.checked_add_direction(*dir).ok())
.collect()
}
fn roomxy_unreachable_tile_costs(node: RoomXY) -> u32 {
if node.x.u8() == 10 && node.y.u8() == 12 {
u32::MAX
} else {
1
}
}
fn position_unreachable_tile_costs(node: Position) -> u32 {
if node.x().u8() == 10 && node.y().u8() == 12 {
u32::MAX
} else {
1
}
}
#[test]
fn simple_linear_path_roomxy() {
let start = unsafe { RoomXY::unchecked_new(10, 10) };
let goal = unsafe { RoomXY::unchecked_new(10, 12) };
let goal_fn = goal_exact_node(goal);
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_plains_costs,
room_xy_neighbors,
2000,
2000,
);
assert_eq!(search_results.incomplete(), false);
assert_eq!(search_results.cost(), 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 goal_fn = goal_exact_node(goal);
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_plains_costs,
position_neighbors,
2000,
2000,
);
assert_eq!(search_results.incomplete(), false);
assert_eq!(search_results.cost(), 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_fn = goal_exact_node(unsafe { RoomXY::unchecked_new(10, 12) });
let search_results = shortest_path_generic(
&[start],
&goal_fn,
roomxy_unreachable_tile_costs,
room_xy_neighbors,
2000,
2000,
);
println!("{:?}", search_results);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.cost() > 0, true);
assert_eq!(search_results.ops() == 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 0);
}
#[test]
fn unreachable_target_position() {
let room_name = "E5N6";
let start = new_position(room_name, 10, 10);
let goal_fn = goal_exact_node(new_position(room_name, 10, 12));
let search_results = shortest_path_generic(
&[start],
&goal_fn,
position_unreachable_tile_costs,
position_neighbors,
2000,
2000,
);
println!("{:?}", search_results);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.cost() > 0, true);
assert_eq!(search_results.ops() == 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 0);
}
#[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_fn = goal_exact_node(unsafe { RoomXY::unchecked_new(10, 12) });
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_plains_costs,
room_xy_neighbors,
max_ops_failure,
2000,
);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.cost() > 0, true);
assert_eq!(search_results.ops() == max_ops_failure, true);
let path = search_results.path();
assert_eq!(path.len(), 0);
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_plains_costs,
room_xy_neighbors,
max_ops_success,
2000,
);
assert_eq!(search_results.incomplete(), false);
assert_eq!(search_results.cost() > 0, true);
assert_eq!(search_results.ops() < max_ops_success, true);
let path = search_results.path();
assert_eq!(path.len(), 3);
}
#[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_fn = goal_exact_node(new_position(room_name, 10, 12));
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_plains_costs,
position_neighbors,
max_ops_failure,
2000,
);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.cost() > 0, true);
assert_eq!(search_results.ops() == max_ops_failure, true);
let path = search_results.path();
assert_eq!(path.len(), 0);
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_plains_costs,
position_neighbors,
max_ops_success,
2000,
);
assert_eq!(search_results.incomplete(), false);
assert_eq!(search_results.cost() > 0, true);
assert_eq!(search_results.ops() < max_ops_success, true);
let path = search_results.path();
assert_eq!(path.len(), 3);
}
#[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_fn = goal_exact_node(unsafe { RoomXY::unchecked_new(10, 12) });
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_swamps_costs,
room_xy_neighbors,
2000,
max_cost_failure,
);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.cost() >= max_cost_failure, true);
assert_eq!(search_results.ops() < 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 0);
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_swamps_costs,
room_xy_neighbors,
2000,
max_cost_success,
);
assert_eq!(search_results.incomplete(), false);
assert_eq!(search_results.cost() < 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_fn = goal_exact_node(new_position(room_name, 10, 12));
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_swamps_costs,
position_neighbors,
2000,
max_cost_failure,
);
assert_eq!(search_results.incomplete(), true);
assert_eq!(search_results.cost() >= max_cost_failure, true);
assert_eq!(search_results.ops() < 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 0);
let search_results = shortest_path_generic(
&[start],
&goal_fn,
all_tiles_are_swamps_costs,
position_neighbors,
2000,
max_cost_success,
);
assert_eq!(search_results.incomplete(), false);
assert_eq!(search_results.cost() < max_cost_success, true);
assert_eq!(search_results.ops() < 2000, true);
let path = search_results.path();
assert_eq!(path.len(), 3);
}
}