From b5ddf09bb97686579e928bd072602df121c5ab4c Mon Sep 17 00:00:00 2001 From: Alex Bethel Date: Sat, 1 Jan 2022 18:19:55 -0600 Subject: [PATCH] Use A* rather than Dijkstra The level generation algorithm now runs somewhere around 100x faster. --- Cargo.lock | 82 ++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/main.rs | 1 + src/rooms.rs | 121 ++++++++++++++++----------------------------------- src/util.rs | 54 +++++++++++++++++++++++ 5 files changed, 175 insertions(+), 84 deletions(-) create mode 100644 src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 2722c32..f168ab7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 5e0a20c..9fb3a7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/main.rs b/src/main.rs index 7aaf042..70cc2e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ use pancurses::{endwin, initscr}; mod game; mod rooms; +mod util; fn main() { let window = initscr(); diff --git a/src/rooms.rs b/src/rooms.rs index 50938e6..2abf466 100644 --- a/src/rooms.rs +++ b/src/rooms.rs @@ -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 = 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, 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, 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( - mut edge_weights: impl FnMut(Node) -> EdgeWeights, - from: Node, - to: Node, -) -> Option> -where - EdgeWeights: Iterator, - Node: Clone + Eq + Hash, -{ - let mut distances = HashMap::>::new(); - let mut visited = HashSet::::new(); - let mut parents = HashMap::::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) -} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..5791a3c --- /dev/null +++ b/src/util.rs @@ -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 { + 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() + } +}