Use A* rather than Dijkstra

The level generation algorithm now runs somewhere around 100x faster.
master
Alex Bethel 2022-01-01 18:19:55 -06:00
parent 00efc1dda4
commit b5ddf09bb9
5 changed files with 175 additions and 84 deletions

82
Cargo.lock generated
View File

@ -2,6 +2,12 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "cc"
version = "1.0.72"
@ -21,9 +27,22 @@ dependencies = [
"float-ord",
"grid",
"pancurses",
"pathfinding",
"rand",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "fixedbitset"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e"
[[package]]
name = "float-ord"
version = "0.3.2"
@ -50,6 +69,40 @@ dependencies = [
"no-std-compat",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "integer-sqrt"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770"
dependencies = [
"num-traits",
]
[[package]]
name = "itertools"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3"
dependencies = [
"either",
]
[[package]]
name = "libc"
version = "0.2.112"
@ -82,6 +135,15 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
[[package]]
name = "num-traits"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
dependencies = [
"autocfg",
]
[[package]]
name = "pancurses"
version = "0.17.0"
@ -95,6 +157,20 @@ dependencies = [
"winreg",
]
[[package]]
name = "pathfinding"
version = "3.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a64bfa665d8821a903701c7bb440e7f72b1f05387b390cc23f498cc23148099"
dependencies = [
"fixedbitset",
"indexmap",
"integer-sqrt",
"itertools",
"num-traits",
"rustc-hash",
]
[[package]]
name = "pdcurses-sys"
version = "0.7.1"
@ -157,6 +233,12 @@ dependencies = [
"rand_core",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"

View File

@ -10,3 +10,4 @@ pancurses = "0.17.0"
rand = "0.8.4"
grid = "0.6.0"
float-ord = "0.3.2"
pathfinding = "3.0.5"

View File

@ -3,6 +3,7 @@ use pancurses::{endwin, initscr};
mod game;
mod rooms;
mod util;
fn main() {
let window = initscr();

View File

@ -13,18 +13,16 @@
//! near them, and it has some randomness added to its weights to
//! discourage long, linear hallways.
use std::{
collections::{hash_map::Entry, HashMap, HashSet},
hash::Hash,
iter::successors,
ops::Range,
};
use std::ops::Range;
use float_ord::FloatOrd;
use grid::Grid;
use pathfinding::directed::astar::astar;
use rand::Rng;
use crate::game::{DungeonTile, LEVEL_SIZE};
use crate::{
game::{DungeonTile, LEVEL_SIZE},
util::NiceFloat,
};
/// The possible sizes of a room, on both the x and y axes.
const ROOM_SIZE_LIMITS: Range<usize> = 4..8;
@ -43,7 +41,7 @@ const ROOM_MARGIN: usize = 2;
/// through rooms, 1.0 is indifferent to the existence of rooms, and
/// higher values discourage traveling through rooms (hallways will
/// wrap around rooms rather than enter them).
const ROOM_WEIGHT: f64 = 0.5;
const ROOM_WEIGHT: f64 = 0.2;
/// Randomness factor to avoid straight lines in hallways.
const HALLWAY_RANDOMNESS: f64 = 0.6;
@ -185,10 +183,11 @@ fn cut_hallways(grid: &mut Grid<DungeonTile>, rooms: &[RoomBounds], rng: &mut im
// Make hallways between pairs of adjacent rooms.
for rooms in rooms.windows(2) {
let (from, to) = (&rooms[0], &rooms[1]);
let (from, to) = (&rooms[0].center(), &rooms[1].center());
let neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1)];
for (x, y) in pathfind(
let (path, _weight) = astar(
from,
|node| {
let (x, y) = (node.0 as isize, node.1 as isize);
neighbors
@ -198,91 +197,45 @@ fn cut_hallways(grid: &mut Grid<DungeonTile>, rooms: &[RoomBounds], rng: &mut im
if (0..size.0 as isize).contains(&x) && (0..size.1 as isize).contains(&y) {
Some((
(x as usize, y as usize),
match grid[y as usize][x as usize] {
NiceFloat(match grid[y as usize][x as usize] {
DungeonTile::Wall => stone_weights[y as usize][x as usize],
_ => ROOM_WEIGHT,
},
}),
))
} else {
None
}
})
},
from.center(),
to.center(),
|node| {
// For A* to work correctly, the heuristic here must
// be smaller than the actual cost to travel from
// `node` to `to`, which means we need to know the
// minimum possible cost from `node` to `to`.
// The minimum possible cost to travel through a
// single node if it's a wall is 1.0 -
// HALLWAY_RANDOMNESS, and if it's a hallway then it's
// ROOM_WEIGHT.
let min_node_cost = f64::min(1.0 - HALLWAY_RANDOMNESS, ROOM_WEIGHT);
// And since hallways don't travel diagonally, the
// minimum number of nodes to travel through is the
// sum of the horizontal and vertical distance.
let dx = node.0 as isize - to.0 as isize;
let dy = node.1 as isize - to.1 as isize;
let min_dist = dx.abs() + dy.abs();
NiceFloat(min_dist as f64 * min_node_cost)
},
|node| node == to,
)
.expect("graph is connected, must therefore be navigable")
{
.expect("Grid is connected therefore should be navigable");
for (x, y) in path {
if grid[y][x] == DungeonTile::Wall {
grid[y][x] = DungeonTile::Hallway;
}
}
}
}
/// Finds a route from the nodes `from` to `to` on a graph, where the
/// edges and weights connected to a particular node are given by the
/// `edge_weights` function. Returns a vector of the nodes visited.
///
/// At the moment this is a horribly unoptimized Dijkstra's algorithm.
/// Should definitely swap this out for something more efficient.
fn pathfind<Node, EdgeWeights>(
mut edge_weights: impl FnMut(Node) -> EdgeWeights,
from: Node,
to: Node,
) -> Option<Vec<Node>>
where
EdgeWeights: Iterator<Item = (Node, f64)>,
Node: Clone + Eq + Hash,
{
let mut distances = HashMap::<Node, FloatOrd<f64>>::new();
let mut visited = HashSet::<Node>::new();
let mut parents = HashMap::<Node, Node>::new();
distances.insert(from, FloatOrd(0.0));
loop {
// Next node to visit is the unvisited node with the lowest
// distance.
let (current, current_dist) = match distances
.iter()
.filter(|(node, _distance)| !visited.contains(node))
.min_by_key(|(_node, distance)| *distance)
{
Some((current, FloatOrd(current_dist))) => (current.to_owned(), *current_dist),
None => {
// Every reachable node has been visited and the
// target node hasn't been reached, therefore no route
// exists.
return None;
}
};
if current == to {
// We've reached the destination.
break;
}
// Find the most efficient routes to unexplored neighbors.
for (neighbor, weight) in edge_weights(current.to_owned()) {
let neighbor_dist = current_dist + weight;
match distances.entry(neighbor.to_owned()) {
Entry::Occupied(mut slot) => {
if neighbor_dist < slot.get().0 {
*slot.get_mut() = FloatOrd(neighbor_dist);
parents.insert(neighbor.clone(), current.clone());
}
}
Entry::Vacant(slot) => {
slot.insert(FloatOrd(weight + current_dist));
parents.insert(neighbor.clone(), current.clone());
}
}
}
visited.insert(current);
}
let mut nodes: Vec<_> = successors(Some(to), |last| parents.get(last).cloned()).collect();
nodes.reverse();
Some(nodes)
}

54
src/util.rs Normal file
View File

@ -0,0 +1,54 @@
//! Miscellanous utility functions and types used in other files.
use std::ops::Add;
use float_ord::FloatOrd;
use pathfinding::num_traits::Zero;
/// A somewhat more well-behaved floating point type, used in
/// pathfinding. Fully ordered, implements Eq, and has a defined zero
/// element.
#[derive(Clone, Copy)]
pub struct NiceFloat(pub f64);
impl PartialEq for NiceFloat {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for NiceFloat {}
impl PartialOrd for NiceFloat {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for NiceFloat {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
// TODO: This is the /only/ use of FloatOrd in the program at
// this point. It's kinda silly to bring in a dependency for
// this; we should probably just implement the logic here
// instead.
FloatOrd(self.0).cmp(&FloatOrd(other.0))
}
}
impl Add for NiceFloat {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0 + rhs.0)
}
}
impl Zero for NiceFloat {
fn zero() -> Self {
Self(0.0)
}
fn is_zero(&self) -> bool {
*self == Self::zero()
}
}