Use A* rather than Dijkstra
The level generation algorithm now runs somewhere around 100x faster.
This commit is contained in:
parent
00efc1dda4
commit
b5ddf09bb9
82
Cargo.lock
generated
82
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -3,6 +3,7 @@ use pancurses::{endwin, initscr};
|
|||
|
||||
mod game;
|
||||
mod rooms;
|
||||
mod util;
|
||||
|
||||
fn main() {
|
||||
let window = initscr();
|
||||
|
|
121
src/rooms.rs
121
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<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
54
src/util.rs
Normal 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()
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue