Apply experimental astar2 changes to the original impl

This commit is contained in:
Imbris 2023-04-18 04:15:45 -04:00
parent d1ca47da41
commit 92a42ced18
10 changed files with 170 additions and 333 deletions

View File

@ -4,12 +4,13 @@ use core::{
fmt,
hash::{BuildHasher, Hash},
};
use hashbrown::{HashMap, HashSet};
use hashbrown::HashMap;
use std::collections::BinaryHeap;
#[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> {
cost: f32,
// cost so far + heursitic
cost_estimate: f32,
node: S,
}
@ -23,12 +24,24 @@ impl<S: Eq> Ord for PathEntry<S> {
// This method implements reverse ordering, so that the lowest cost
// will be ordered first
fn cmp(&self, other: &PathEntry<S>) -> Ordering {
other.cost.partial_cmp(&self.cost).unwrap_or(Equal)
other
.cost_estimate
.partial_cmp(&self.cost_estimate)
.unwrap_or(Equal)
}
}
impl<S: Eq> PartialOrd for PathEntry<S> {
fn partial_cmp(&self, other: &PathEntry<S>) -> Option<Ordering> { Some(self.cmp(other)) }
// This is particularily hot in `BinaryHeap::pop`, so we provide this
// implementation.
//
// NOTE: This probably doesn't handle edge cases like `NaNs` in a consistent
// manner with `Ord`, but I don't think we need to care about that here(?)
//
// See note about reverse ordering above.
fn le(&self, other: &PathEntry<S>) -> bool { other.cost_estimate <= self.cost_estimate }
}
pub enum PathResult<T> {
@ -56,15 +69,20 @@ impl<T> PathResult<T> {
}
}
// If node entry exists, this was visited!
#[derive(Clone, Debug)]
struct NodeEntry<S> {
// if came_from == self this is the start node!
came_from: S,
cheapest_score: f32,
}
#[derive(Clone)]
pub struct Astar<S, Hasher> {
iter: usize,
max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<S>>,
came_from: HashMap<S, S, Hasher>,
cheapest_scores: HashMap<S, f32, Hasher>,
final_scores: HashMap<S, f32, Hasher>,
visited: HashSet<S, Hasher>,
potential_nodes: BinaryHeap<PathEntry<S>>, // cost, node pairs
visited_nodes: HashMap<S, NodeEntry<S>, Hasher>,
cheapest_node: Option<S>,
cheapest_cost: Option<f32>,
}
@ -76,10 +94,7 @@ impl<S: Clone + Eq + Hash + fmt::Debug, H: BuildHasher> fmt::Debug for Astar<S,
.field("iter", &self.iter)
.field("max_iters", &self.max_iters)
.field("potential_nodes", &self.potential_nodes)
.field("came_from", &self.came_from)
.field("cheapest_scores", &self.cheapest_scores)
.field("final_scores", &self.final_scores)
.field("visited", &self.visited)
.field("visited_nodes", &self.visited_nodes)
.field("cheapest_node", &self.cheapest_node)
.field("cheapest_cost", &self.cheapest_cost)
.finish()
@ -92,24 +107,16 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
max_iters,
iter: 0,
potential_nodes: core::iter::once(PathEntry {
cost: 0.0,
cost_estimate: 0.0,
node: start.clone(),
})
.collect(),
came_from: HashMap::with_hasher(hasher.clone()),
cheapest_scores: {
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
h.extend(core::iter::once((start.clone(), 0.0)));
h
},
final_scores: {
let mut h = HashMap::with_capacity_and_hasher(1, hasher.clone());
h.extend(core::iter::once((start.clone(), 0.0)));
h
},
visited: {
let mut s = HashSet::with_capacity_and_hasher(1, hasher);
s.extend(core::iter::once(start));
visited_nodes: {
let mut s = HashMap::with_capacity_and_hasher(1, hasher.clone());
s.extend(core::iter::once((start.clone(), NodeEntry {
came_from: start,
cheapest_score: 0.0,
})));
s
},
cheapest_node: None,
@ -120,13 +127,16 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn poll<I>(
&mut self,
iters: usize,
// Estimate how far we are from the target? but we are given two nodes...
// (current, previous)
mut heuristic: impl FnMut(&S, &S) -> f32,
// get neighboring nodes
mut neighbors: impl FnMut(&S) -> I,
mut transition: impl FnMut(&S, &S) -> f32,
// have we reached target?
mut satisfied: impl FnMut(&S) -> bool,
) -> PathResult<S>
where
I: Iterator<Item = S>,
I: Iterator<Item = (S, f32)>, // (node, transition cost)
{
let iter_limit = self.max_iters.min(self.iter + iters);
while self.iter < iter_limit {
@ -134,28 +144,48 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
if satisfied(&node) {
return PathResult::Path(self.reconstruct_path_to(node));
} else {
for neighbor in neighbors(&node) {
let node_cheapest = self.cheapest_scores.get(&node).unwrap_or(&f32::MAX);
let neighbor_cheapest =
self.cheapest_scores.get(&neighbor).unwrap_or(&f32::MAX);
let (node_cheapest, came_from) = self
.visited_nodes
.get(&node)
.map(|n| (n.cheapest_score, n.came_from.clone()))
.unwrap();
for (neighbor, transition) in neighbors(&node) {
if neighbor == came_from {
continue;
}
let neighbor_cheapest = self
.visited_nodes
.get(&neighbor)
.map_or(f32::MAX, |n| n.cheapest_score);
let cost = node_cheapest + transition(&node, &neighbor);
if cost < *neighbor_cheapest {
self.came_from.insert(neighbor.clone(), node.clone());
self.cheapest_scores.insert(neighbor.clone(), cost);
// compute cost to traverse to each neighbor
let cost = node_cheapest + transition;
if cost < neighbor_cheapest {
let previously_visited = self
.visited_nodes
.insert(neighbor.clone(), NodeEntry {
came_from: node.clone(),
cheapest_score: cost,
})
.is_some();
let h = heuristic(&neighbor, &node);
let neighbor_cost = cost + h;
self.final_scores.insert(neighbor.clone(), neighbor_cost);
// note that cheapest_scores does not include the heuristic
// priority queue does include heuristic
let cost_estimate = cost + h;
if self.cheapest_cost.map(|cc| h < cc).unwrap_or(true) {
self.cheapest_node = Some(node.clone());
self.cheapest_cost = Some(h);
};
if self.visited.insert(neighbor.clone()) {
// TODO: I think the if here should be removed
// if we hadn't already visted, add this to potential nodes, what about
// its neighbors, wouldn't they need to be revisted???
if !previously_visited {
self.potential_nodes.push(PathEntry {
cost_estimate,
node: neighbor,
cost: neighbor_cost,
});
}
}
@ -190,7 +220,12 @@ impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
fn reconstruct_path_to(&mut self, end: S) -> Path<S> {
let mut path = vec![end.clone()];
let mut cnode = &end;
while let Some(node) = self.came_from.get(cnode) {
while let Some(node) = self
.visited_nodes
.get(cnode)
.map(|n| &n.came_from)
.filter(|n| *n != cnode)
{
path.push(node.clone());
cnode = node;
}

View File

@ -1,227 +0,0 @@
#![allow(dead_code, unused_mut, unused_variables)]
use crate::path::Path;
use core::{
cmp::Ordering::{self, Equal},
fmt,
hash::{BuildHasher, Hash},
};
use hashbrown::HashMap;
use std::collections::BinaryHeap;
#[derive(Copy, Clone, Debug)]
pub struct PathEntry<S> {
// cost so far + heursitic
cost_estimate: f32,
node: S,
}
impl<S: Eq> PartialEq for PathEntry<S> {
fn eq(&self, other: &PathEntry<S>) -> bool { self.node.eq(&other.node) }
}
impl<S: Eq> Eq for PathEntry<S> {}
impl<S: Eq> Ord for PathEntry<S> {
// This method implements reverse ordering, so that the lowest cost
// will be ordered first
fn cmp(&self, other: &PathEntry<S>) -> Ordering {
other
.cost_estimate
.partial_cmp(&self.cost_estimate)
.unwrap_or(Equal)
}
}
impl<S: Eq> PartialOrd for PathEntry<S> {
fn partial_cmp(&self, other: &PathEntry<S>) -> Option<Ordering> { Some(self.cmp(other)) }
// This is particularily hot in `BinaryHeap::pop`, so we provide this
// implementation.
//
// NOTE: This probably doesn't handle edge cases like `NaNs` in a consistent
// manner with `Ord`, but I don't think we need to care about that here(?)
//
// See note about reverse ordering above.
fn le(&self, other: &PathEntry<S>) -> bool { other.cost_estimate <= self.cost_estimate }
}
pub enum PathResult<T> {
None(Path<T>),
Exhausted(Path<T>),
Path(Path<T>),
Pending,
}
impl<T> PathResult<T> {
pub fn into_path(self) -> Option<Path<T>> {
match self {
PathResult::Path(path) => Some(path),
_ => None,
}
}
pub fn map<U>(self, f: impl FnOnce(Path<T>) -> Path<U>) -> PathResult<U> {
match self {
PathResult::None(p) => PathResult::None(f(p)),
PathResult::Exhausted(p) => PathResult::Exhausted(f(p)),
PathResult::Path(p) => PathResult::Path(f(p)),
PathResult::Pending => PathResult::Pending,
}
}
}
// If node entry exists, this was visited!
#[derive(Clone, Debug)]
struct NodeEntry<S> {
// if came_from == self this is the start node!
came_from: S,
cheapest_score: f32,
}
#[derive(Clone)]
pub struct Astar<S, Hasher> {
iter: usize,
max_iters: usize,
potential_nodes: BinaryHeap<PathEntry<S>>, // cost, node pairs
visited_nodes: HashMap<S, NodeEntry<S>, Hasher>,
cheapest_node: Option<S>,
cheapest_cost: Option<f32>,
}
/// NOTE: Must manually derive since Hasher doesn't implement it.
impl<S: Clone + Eq + Hash + fmt::Debug, H: BuildHasher> fmt::Debug for Astar<S, H> {
fn fmt(&self, _f: &mut fmt::Formatter<'_>) -> fmt::Result { todo!() }
}
impl<S: Clone + Eq + Hash, H: BuildHasher + Clone> Astar<S, H> {
pub fn new(max_iters: usize, start: S, hasher: H) -> Self {
Self {
max_iters,
iter: 0,
potential_nodes: core::iter::once(PathEntry {
cost_estimate: 0.0,
node: start.clone(),
})
.collect(),
visited_nodes: {
let mut s = HashMap::with_capacity_and_hasher(1, hasher.clone());
s.extend(core::iter::once((start.clone(), NodeEntry {
came_from: start,
cheapest_score: 0.0,
})));
s
},
cheapest_node: None,
cheapest_cost: None,
}
}
pub fn poll<I>(
&mut self,
iters: usize,
// Estimate how far we are from the target? but we are given two nodes...
// (current, previous)
mut heuristic: impl FnMut(&S, &S) -> f32,
// get neighboring nodes
mut neighbors: impl FnMut(&S) -> I,
// have we reached target?
mut satisfied: impl FnMut(&S) -> bool,
) -> PathResult<S>
where
I: Iterator<Item = (S, f32)>, // (node, transition cost)
{
let iter_limit = self.max_iters.min(self.iter + iters);
while self.iter < iter_limit {
if let Some(PathEntry { node, .. }) = self.potential_nodes.pop() {
if satisfied(&node) {
return PathResult::Path(self.reconstruct_path_to(node));
} else {
// we have to fetch this even though it was put into the priority queue
let (node_cheapest, came_from) = self
.visited_nodes
.get(&node)
.map(|n| (n.cheapest_score, n.came_from.clone()))
.unwrap();
for (neighbor, transition) in neighbors(&node) {
if neighbor == came_from {
continue;
}
let neighbor_cheapest = self
.visited_nodes
.get(&neighbor)
.map_or(f32::MAX, |n| n.cheapest_score);
// compute cost to traverse to each neighbor
let cost = node_cheapest + transition;
if cost < neighbor_cheapest {
let previously_visited = self
.visited_nodes
.insert(neighbor.clone(), NodeEntry {
came_from: node.clone(),
cheapest_score: cost,
})
.is_some();
let h = heuristic(&neighbor, &node);
// note that cheapest_scores does not include the heuristic
// priority queue does include heuristic
let cost_estimate = cost + h;
if self.cheapest_cost.map(|cc| h < cc).unwrap_or(true) {
self.cheapest_node = Some(node.clone());
self.cheapest_cost = Some(h);
};
// TODO: I think the if here should be removed
// if we hadn't already visted, add this to potential nodes, what about
// its neighbors, wouldn't they need to be revisted???
if !previously_visited {
self.potential_nodes.push(PathEntry {
cost_estimate,
node: neighbor,
});
}
}
}
}
} else {
return PathResult::None(
self.cheapest_node
.clone()
.map(|lc| self.reconstruct_path_to(lc))
.unwrap_or_default(),
);
}
self.iter += 1
}
if self.iter >= self.max_iters {
PathResult::Exhausted(
self.cheapest_node
.clone()
.map(|lc| self.reconstruct_path_to(lc))
.unwrap_or_default(),
)
} else {
PathResult::Pending
}
}
pub fn get_cheapest_cost(&self) -> Option<f32> { self.cheapest_cost }
fn reconstruct_path_to(&mut self, end: S) -> Path<S> {
let mut path = vec![end.clone()];
let mut cnode = &end;
while let Some(node) = self
.visited_nodes
.get(cnode)
.map(|n| &n.came_from)
.filter(|n| *n != cnode)
{
path.push(node.clone());
cnode = node;
}
path.into_iter().rev().collect()
}
}

View File

@ -38,7 +38,6 @@ pub mod uid;
// NOTE: Comment out macro to get rustfmt to re-order these as needed.
cfg_if! { if #[cfg(not(target_arch = "wasm32"))] {
pub mod astar;
pub mod astar2;
pub mod calendar;
pub mod character;
pub mod clock;

View File

@ -537,6 +537,17 @@ where
};
let heuristic = |pos: &Vec3<i32>, _: &Vec3<i32>| (pos.distance_squared(end) as f32).sqrt();
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let crow_line = LineSegment2 {
start: startf.xy(),
end: endf.xy(),
};
// Modify the heuristic a little in order to prefer paths that take us on a
// straight line toward our target. This means we get smoother movement.
1.0 + crow_line.distance_to_point(b.xy().map(|e| e as f32)) * 0.025
+ (b.z - a.z - 1).max(0) as f32 * 10.0
};
let neighbors = |pos: &Vec3<i32>| {
let pos = *pos;
const DIRS: [Vec3<i32>; 17] = [
@ -616,7 +627,10 @@ where
.map(|b| !b.is_solid())
.unwrap_or(true)))
})
.map(move |(pos, dir)| pos + dir)
.map(|(pos, dir)| {
let destination = pos + dir;
(destination, transition(pos, destination))
})
// .chain(
// DIAGONALS
// .iter()
@ -627,17 +641,6 @@ where
// )
};
let transition = |a: &Vec3<i32>, b: &Vec3<i32>| {
let crow_line = LineSegment2 {
start: startf.xy(),
end: endf.xy(),
};
// Modify the heuristic a little in order to prefer paths that take us on a
// straight line toward our target. This means we get smoother movement.
1.0 + crow_line.distance_to_point(b.xy().map(|e| e as f32)) * 0.025
+ (b.z - a.z - 1).max(0) as f32 * 10.0
};
let satisfied = |pos: &Vec3<i32>| pos == &end;
let mut new_astar = match astar.take() {
@ -645,7 +648,7 @@ where
Some(astar) => astar,
};
let path_result = new_astar.poll(100, heuristic, neighbors, transition, satisfied);
let path_result = new_astar.poll(100, heuristic, neighbors, satisfied);
*astar = Some(new_astar);

View File

@ -942,6 +942,12 @@ pub fn handle_manipulate_loadout(
BuildHasherDefault::<FxHasher64>::default(),
);
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: Vec3<i32>, b: Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
};
// Neighbors are all neighboring blocks that are air
let neighbors = |pos: &Vec3<i32>| {
const DIRS: [Vec3<i32>; 6] = [
@ -953,24 +959,23 @@ pub fn handle_manipulate_loadout(
Vec3::new(0, 0, -1),
];
let pos = *pos;
DIRS.iter().map(move |dir| dir + pos).filter(|pos| {
data.terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
// Transition uses manhattan distance as the cost, with a slightly lower cost
// for z transitions
let transition = |a: &Vec3<i32>, b: &Vec3<i32>| {
let (a, b) = (a.map(|x| x as f32), b.map(|x| x as f32));
((a - b) * Vec3::new(1.0, 1.0, 0.9)).map(|e| e.abs()).sum()
DIRS.iter()
.map(move |dir| {
let dest = dir + pos;
(dest, transition(pos, dest))
})
.filter(|(pos, _)| {
data.terrain
.get(*pos)
.ok()
.map_or(false, |block| !block.is_filled())
})
};
// Pathing satisfied when it reaches the sprite position
let satisfied = |pos: &Vec3<i32>| *pos == sprite_pos;
let not_blocked_by_terrain = astar
.poll(iters, heuristic, neighbors, transition, satisfied)
.poll(iters, heuristic, neighbors, satisfied)
.into_path()
.is_some();

View File

@ -52,10 +52,10 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let heuristic = |tile: &Vec2<i32>, _: &Vec2<i32>| tile.as_::<f32>().distance(end.as_());
let mut astar = Astar::new(1000, start, BuildHasherDefault::<FxHasher64>::default());
let transition = |a: &Vec2<i32>, b: &Vec2<i32>| {
let transition = |a: Vec2<i32>, b: Vec2<i32>| {
let distance = a.as_::<f32>().distance(b.as_());
let a_tile = site.tiles.get(*a);
let b_tile = site.tiles.get(*b);
let a_tile = site.tiles.get(a);
let b_tile = site.tiles.get(b);
let terrain = match &b_tile.kind {
TileKind::Empty => 3.0,
@ -79,12 +79,12 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let building = if a_tile.is_building() && b_tile.is_road() {
a_tile
.plot
.and_then(|plot| is_door_tile(plot, *a).then_some(1.0))
.and_then(|plot| is_door_tile(plot, a).then_some(1.0))
.unwrap_or(10000.0)
} else if b_tile.is_building() && a_tile.is_road() {
b_tile
.plot
.and_then(|plot| is_door_tile(plot, *b).then_some(1.0))
.and_then(|plot| is_door_tile(plot, b).then_some(1.0))
.unwrap_or(10000.0)
} else if (a_tile.is_building() || b_tile.is_building()) && a_tile.plot != b_tile.plot {
10000.0
@ -97,10 +97,13 @@ fn path_in_site(start: Vec2<i32>, end: Vec2<i32>, site: &site2::Site) -> PathRes
let neighbors = |tile: &Vec2<i32>| {
let tile = *tile;
CARDINALS.iter().map(move |c| tile + *c)
CARDINALS.iter().map(move |c| {
let n = tile + *c;
(n, transition(tile, n))
})
};
astar.poll(1000, heuristic, neighbors, transition, |tile| {
astar.poll(1000, heuristic, neighbors, |tile| {
*tile == end || site.tiles.get_known(*tile).is_none()
})
}
@ -135,17 +138,22 @@ fn path_between_sites(
let mut astar = Astar::new(250, start, BuildHasherDefault::<FxHasher64>::default());
let neighbors = |site: &Id<civ::Site>| world.civs().neighbors(*site);
let transition = |a: &Id<civ::Site>, b: &Id<civ::Site>| {
let transition = |a: Id<civ::Site>, b: Id<civ::Site>| {
world
.civs()
.track_between(*a, *b)
.track_between(a, b)
.map(|(id, _)| world.civs().tracks.get(id).cost)
.unwrap_or(f32::INFINITY)
};
let neighbors = |site: &Id<civ::Site>| {
let site = *site;
world
.civs()
.neighbors(site)
.map(move |n| (n, transition(n, site)))
};
let path = astar.poll(250, heuristic, neighbors, transition, |site| *site == end);
let path = astar.poll(250, heuristic, neighbors, |site| *site == end);
path.map(|path| {
let path = path

View File

@ -681,9 +681,12 @@ impl Civs {
.distance_squared(self.sites.get(b).center) as f32)
.sqrt()
};
let neighbors = |p: &Id<Site>| self.neighbors(*p);
let transition = |a: &Id<Site>, b: &Id<Site>| {
self.tracks.get(self.track_between(*a, *b).unwrap().0).cost
let transition =
|a: Id<Site>, b: Id<Site>| self.tracks.get(self.track_between(a, b).unwrap().0).cost;
let neighbors = |p: &Id<Site>| {
let p = *p;
self.neighbors(p)
.map(move |neighbor| (neighbor, transition(p, neighbor)))
};
let satisfied = |p: &Id<Site>| *p == b;
// We use this hasher (FxHasher64) because
@ -692,7 +695,7 @@ impl Civs {
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = Astar::new(100, a, BuildHasherDefault::<FxHasher64>::default());
astar
.poll(100, heuristic, neighbors, transition, satisfied)
.poll(100, heuristic, neighbors, satisfied)
.into_path()
.and_then(|path| astar.get_cheapest_cost().map(|cost| (path, cost)))
}
@ -1349,7 +1352,7 @@ fn find_path(
// (1) we don't care about DDOS attacks (ruling out SipHash);
// (2) we care about determinism across computers (ruling out AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
let mut astar = common::astar2::Astar::new(
let mut astar = Astar::new(
MAX_PATH_ITERS,
a,
BuildHasherDefault::<FxHasher64>::default(),

View File

@ -1347,15 +1347,19 @@ impl Land {
&self,
origin: Vec2<i32>,
dest: Vec2<i32>,
mut path_cost_fn: impl FnMut(Option<&Tile>, Option<&Tile>) -> f32,
path_cost_fn: impl Fn(Option<&Tile>, Option<&Tile>) -> f32,
) -> Option<Path<Vec2<i32>>> {
let heuristic = |pos: &Vec2<i32>, _: &Vec2<i32>| (pos - dest).map(|e| e as f32).magnitude();
let transition =
|from: Vec2<i32>, to: Vec2<i32>| path_cost_fn(self.tile_at(from), self.tile_at(to));
let neighbors = |pos: &Vec2<i32>| {
let pos = *pos;
CARDINALS.iter().map(move |dir| pos + *dir)
let transition = &transition;
CARDINALS.iter().map(move |dir| {
let to = pos + *dir;
(to, transition(pos, to))
})
};
let transition =
|from: &Vec2<i32>, to: &Vec2<i32>| path_cost_fn(self.tile_at(*from), self.tile_at(*to));
let satisfied = |pos: &Vec2<i32>| *pos == dest;
// We use this hasher (FxHasher64) because
@ -1363,7 +1367,7 @@ impl Land {
// (2) we don't care about determinism across computers (we could use AAHash);
// (3) we have 8-byte keys (for which FxHash is fastest).
Astar::new(250, origin, BuildHasherDefault::<FxHasher64>::default())
.poll(250, heuristic, neighbors, transition, satisfied)
.poll(250, heuristic, neighbors, satisfied)
.into_path()
}

View File

@ -166,12 +166,17 @@ impl Site {
&heuristic,
|(tile, _)| {
let tile = *tile;
CARDINALS.iter().map(move |dir| (tile + *dir, *dir))
},
|(a, _), (b, _)| {
let alt_a = land.get_alt_approx(self.tile_center_wpos(*a));
let alt_b = land.get_alt_approx(self.tile_center_wpos(*b));
(alt_a - alt_b).abs() / TILE_SIZE as f32
let this = &self;
CARDINALS.iter().map(move |dir| {
let neighbor = (tile + *dir, *dir);
// Transition cost
let alt_a = land.get_alt_approx(this.tile_center_wpos(tile));
let alt_b = land.get_alt_approx(this.tile_center_wpos(neighbor.0));
let cost = (alt_a - alt_b).abs() / TILE_SIZE as f32;
(neighbor, cost)
})
},
|(tile, _)| *tile == b,
)

View File

@ -545,19 +545,22 @@ impl Floor {
fn create_route(&mut self, _ctx: &mut GenCtx<impl Rng>, a: Vec2<i32>, b: Vec2<i32>) {
let heuristic =
move |l: &Vec2<i32>, _: &Vec2<i32>| (l - b).map(|e| e.abs()).reduce_max() as f32;
let neighbors = |l: &Vec2<i32>| {
let l = *l;
CARDINALS
.iter()
.map(move |dir| l + dir)
.filter(|pos| self.tiles.get(*pos).is_some())
};
let transition = |_a: &Vec2<i32>, b: &Vec2<i32>| match self.tiles.get(*b) {
let transition = |_a: Vec2<i32>, b: Vec2<i32>| match self.tiles.get(b) {
Some(Tile::Room(_)) | Some(Tile::Tunnel) => 1.0,
Some(Tile::Solid) => 25.0,
Some(Tile::UpStair(_, _)) | Some(Tile::DownStair(_)) => 0.0,
_ => 100000.0,
};
let neighbors = |l: &Vec2<i32>| {
let l = *l;
CARDINALS
.iter()
.map(move |dir| {
let dest = l + dir;
(dest, transition(l, dest))
})
.filter(|&(pos, _)| self.tiles.get(pos).is_some())
};
let satisfied = |l: &Vec2<i32>| *l == b;
// We use this hasher (FxHasher64) because
// (1) we don't care about DDOS attacks (ruling out SipHash);
@ -569,7 +572,6 @@ impl Floor {
FLOOR_SIZE.product() as usize + 1,
heuristic,
neighbors,
transition,
satisfied,
)
.into_path()