Added exit modifier
This commit is contained in:
parent
d93d015206
commit
4cd00f62de
|
@ -33,7 +33,7 @@ If you want to check how the maps look like, then:
|
|||
* [ ] Voronoi hive
|
||||
* [ ] Wave Function Collapse
|
||||
* Map modifiers (filters)
|
||||
* [ ] Area exit point
|
||||
* [x] Area exit point
|
||||
* [x] Area starting point
|
||||
* [x] Cellular automata
|
||||
* [x] Cull unreachable areas
|
||||
|
@ -69,10 +69,12 @@ use mapgen::dungeon::{
|
|||
map::{Map, Point, TileType},
|
||||
cellular_automata::CellularAutomataGen,
|
||||
starting_point::{AreaStartingPosition, XStart, YStart},
|
||||
cull_unreachable::CullUnreachable,
|
||||
};
|
||||
|
||||
let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50)))
|
||||
.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER))
|
||||
.with(CullUnreachable::new())
|
||||
.build_map();
|
||||
```
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ use mapgen::dungeon::{
|
|||
cellular_automata::CellularAutomataGen,
|
||||
starting_point::{AreaStartingPosition, XStart, YStart},
|
||||
cull_unreachable::CullUnreachable,
|
||||
distant_exit::DistantExit,
|
||||
};
|
||||
|
||||
|
||||
|
@ -35,9 +36,11 @@ struct MapTiles ;
|
|||
impl Tile for MapTiles {
|
||||
fn sprite(&self, p: Point3<u32>, world: &World) -> Option<usize> {
|
||||
let map = world.read_resource::<Map>();
|
||||
let player_pos = Point::new(p.x as usize, p.y as usize);
|
||||
if map.starting_point == Some(player_pos) {
|
||||
let pos = Point::new(p.x as usize, p.y as usize);
|
||||
if map.starting_point == Some(pos) {
|
||||
Some(64)
|
||||
} else if map.exit_point == Some(pos) {
|
||||
Some(62)
|
||||
} else if map.at(p.x as usize, p.y as usize) == TileType::Wall {
|
||||
Some(35)
|
||||
} else {
|
||||
|
@ -47,8 +50,8 @@ impl Tile for MapTiles {
|
|||
|
||||
fn tint(&self, p: Point3<u32>, world: &World) -> Srgba {
|
||||
let map = world.read_resource::<Map>();
|
||||
let player_pos = Point::new(p.x as usize, p.y as usize);
|
||||
if map.starting_point == Some(player_pos) {
|
||||
let pos = Some(Point::new(p.x as usize, p.y as usize));
|
||||
if map.starting_point == pos || map.exit_point == pos {
|
||||
Srgba::new(1.0, 1.0, 0.0, 1.0)
|
||||
} else {
|
||||
Srgba::new(1.0, 1.0, 1.0, 1.0)
|
||||
|
@ -85,6 +88,7 @@ fn init_map(world: &mut World) {
|
|||
let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50)))
|
||||
.with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER))
|
||||
.with(CullUnreachable::new())
|
||||
.with(DistantExit::new())
|
||||
.build_map();
|
||||
world.insert(map);
|
||||
}
|
||||
|
|
|
@ -1,271 +0,0 @@
|
|||
use std::collections::VecDeque;
|
||||
use std::f32::MAX;
|
||||
|
||||
/// Representation of a Dijkstra flow map.
|
||||
/// map is a vector of floats, having a size equal to size_x * size_y (one per tile).
|
||||
/// size_x and size_y are stored for overflow avoidance.
|
||||
/// max_depth is the maximum number of iterations this search shall support.
|
||||
pub struct DijkstraMap {
|
||||
pub map: Vec<f32>,
|
||||
size_x: usize,
|
||||
size_y: usize,
|
||||
max_depth: f32,
|
||||
}
|
||||
|
||||
impl DijkstraMap {
|
||||
/// Construct a new Dijkstra map, ready to run. You must specify the map size, and link to an implementation
|
||||
/// of a BaseMap trait that can generate exits lists. It then builds the map, giving you a result.
|
||||
pub fn new<T>(
|
||||
size_x: T,
|
||||
size_y: T,
|
||||
starts: &[usize],
|
||||
map: &dyn BaseMap,
|
||||
max_depth: f32,
|
||||
) -> DijkstraMap
|
||||
where
|
||||
T: TryInto<usize>,
|
||||
{
|
||||
let sz_x: usize = size_x.try_into().ok().unwrap();
|
||||
let sz_y: usize = size_y.try_into().ok().unwrap();
|
||||
let result: Vec<f32> = vec![MAX; sz_x * sz_y];
|
||||
let mut d = DijkstraMap {
|
||||
map: result,
|
||||
size_x: sz_x,
|
||||
size_y: sz_y,
|
||||
max_depth,
|
||||
};
|
||||
DijkstraMap::build(&mut d, starts, map);
|
||||
d
|
||||
}
|
||||
|
||||
/// Creates an empty Dijkstra map node.
|
||||
pub fn new_empty<T>(size_x: T, size_y: T, max_depth: f32) -> DijkstraMap
|
||||
where
|
||||
T: TryInto<usize>,
|
||||
{
|
||||
let sz_x: usize = size_x.try_into().ok().unwrap();
|
||||
let sz_y: usize = size_y.try_into().ok().unwrap();
|
||||
let result: Vec<f32> = vec![MAX; sz_x * sz_y];
|
||||
DijkstraMap {
|
||||
map: result,
|
||||
size_x: sz_x,
|
||||
size_y: sz_y,
|
||||
max_depth,
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the Dijkstra map. Uses a parallel for each for performance.
|
||||
#[cfg(feature = "threaded")]
|
||||
pub fn clear(dm: &mut DijkstraMap) {
|
||||
dm.map.par_iter_mut().for_each(|x| *x = MAX);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "threaded"))]
|
||||
pub fn clear(dm: &mut DijkstraMap) {
|
||||
dm.map.iter_mut().for_each(|x| *x = MAX);
|
||||
}
|
||||
|
||||
#[cfg(feature = "threaded")]
|
||||
fn build_helper(dm: &mut DijkstraMap, starts: &[usize], map: &dyn BaseMap) -> RunThreaded {
|
||||
if starts.len() >= THREADED_REQUIRED_STARTS {
|
||||
DijkstraMap::build_parallel(dm, starts, map);
|
||||
return RunThreaded::True;
|
||||
}
|
||||
RunThreaded::False
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "threaded"))]
|
||||
fn build_helper(_dm: &mut DijkstraMap, _starts: &[usize], _map: &dyn BaseMap) -> RunThreaded {
|
||||
RunThreaded::False
|
||||
}
|
||||
|
||||
/// Builds the Dijkstra map: iterate from each starting point, to each exit provided by BaseMap's
|
||||
/// exits implementation. Each step adds cost to the current depth, and is discarded if the new
|
||||
/// depth is further than the current depth.
|
||||
/// WARNING: Will give incorrect results when used with non-uniform exit costs. Much slower
|
||||
/// algorithm required to support that.
|
||||
/// Automatically branches to a parallel version if you provide more than 4 starting points
|
||||
pub fn build(dm: &mut DijkstraMap, starts: &[usize], map: &dyn BaseMap) {
|
||||
let threaded = DijkstraMap::build_helper(dm, starts, map);
|
||||
if threaded == RunThreaded::True { return; }
|
||||
let mapsize: usize = (dm.size_x * dm.size_y) as usize;
|
||||
let mut open_list: VecDeque<(usize, f32)> = VecDeque::with_capacity(mapsize);
|
||||
|
||||
for start in starts {
|
||||
open_list.push_back((*start, 0.0));
|
||||
}
|
||||
|
||||
while let Some((tile_idx, depth)) = open_list.pop_front() {
|
||||
let exits = map.get_available_exits(tile_idx);
|
||||
for (new_idx, add_depth) in exits {
|
||||
let new_depth = depth + add_depth;
|
||||
let prev_depth = dm.map[new_idx];
|
||||
if new_depth >= prev_depth { continue; }
|
||||
if new_depth >= dm.max_depth { continue; }
|
||||
dm.map[new_idx] = new_depth;
|
||||
open_list.push_back((new_idx, new_depth));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of Parallel Dijkstra.
|
||||
#[cfg(feature = "threaded")]
|
||||
fn build_parallel(dm: &mut DijkstraMap, starts: &[usize], map: &dyn BaseMap) {
|
||||
let mapsize: usize = (dm.size_x * dm.size_y) as usize;
|
||||
let mut layers: Vec<ParallelDm> = Vec::with_capacity(starts.len());
|
||||
for start_chunk in starts.chunks(rayon::current_num_threads()) {
|
||||
let mut layer = ParallelDm {
|
||||
map: vec![MAX; mapsize],
|
||||
max_depth: dm.max_depth,
|
||||
starts: Vec::new(),
|
||||
};
|
||||
layer
|
||||
.starts
|
||||
.extend(start_chunk.iter().copied().map(|x| x as usize));
|
||||
layers.push(layer);
|
||||
}
|
||||
|
||||
let exits: Vec<SmallVec<[(usize, f32); 10]>> = (0..mapsize)
|
||||
.map(|idx| map.get_available_exits(idx))
|
||||
.collect();
|
||||
|
||||
// Run each map in parallel
|
||||
layers.par_iter_mut().for_each(|l| {
|
||||
let mut open_list: VecDeque<(usize, f32)> = VecDeque::with_capacity(mapsize);
|
||||
|
||||
for start in l.starts.iter().copied() {
|
||||
open_list.push_back((start, 0.0));
|
||||
}
|
||||
|
||||
while let Some((tile_idx, depth)) = open_list.pop_front() {
|
||||
let exits = &exits[tile_idx];
|
||||
for (new_idx, add_depth) in exits {
|
||||
let new_idx = *new_idx;
|
||||
let new_depth = depth + add_depth;
|
||||
let prev_depth = l.map[new_idx];
|
||||
if new_depth >= prev_depth { continue; }
|
||||
if new_depth >= l.max_depth { continue; }
|
||||
l.map[new_idx] = new_depth;
|
||||
open_list.push_back((new_idx, new_depth));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Recombine down to a single result
|
||||
for l in layers {
|
||||
for i in 0..mapsize {
|
||||
dm.map[i] = f32::min(dm.map[i], l.map[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for traversing maps as path-finding. Provides the index of the lowest available
|
||||
/// exit from the specified position index, or None if there isn't one.
|
||||
/// You would use this for pathing TOWARDS a starting node.
|
||||
#[cfg(feature = "threaded")]
|
||||
pub fn find_lowest_exit(dm: &DijkstraMap, position: usize, map: &dyn BaseMap) -> Option<usize> {
|
||||
let mut exits = map.get_available_exits(position);
|
||||
|
||||
if exits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
exits.par_sort_by(|a, b| {
|
||||
dm.map[a.0 as usize]
|
||||
.partial_cmp(&dm.map[b.0 as usize])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Some(exits[0].0)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "threaded"))]
|
||||
pub fn find_lowest_exit(dm: &DijkstraMap, position: usize, map: &dyn BaseMap) -> Option<usize> {
|
||||
let mut exits = map.get_available_exits(position);
|
||||
|
||||
if exits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
exits.sort_by(|a, b| {
|
||||
dm.map[a.0 as usize]
|
||||
.partial_cmp(&dm.map[b.0 as usize])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Some(exits[0].0)
|
||||
}
|
||||
|
||||
/// Helper for traversing maps as path-finding. Provides the index of the highest available
|
||||
/// exit from the specified position index, or None if there isn't one.
|
||||
/// You would use this for pathing AWAY from a starting node, for example if you are running
|
||||
/// away.
|
||||
#[cfg(feature = "threaded")]
|
||||
pub fn find_highest_exit(
|
||||
dm: &DijkstraMap,
|
||||
position: usize,
|
||||
map: &dyn BaseMap,
|
||||
) -> Option<usize> {
|
||||
let mut exits = map.get_available_exits(position);
|
||||
|
||||
if exits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
exits.par_sort_by(|a, b| {
|
||||
dm.map[b.0 as usize]
|
||||
.partial_cmp(&dm.map[a.0 as usize])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Some(exits[0].0)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "threaded"))]
|
||||
pub fn find_highest_exit(
|
||||
dm: &DijkstraMap,
|
||||
position: usize,
|
||||
map: &dyn BaseMap,
|
||||
) -> Option<usize> {
|
||||
let mut exits = map.get_available_exits(position);
|
||||
|
||||
if exits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
exits.sort_by(|a, b| {
|
||||
dm.map[b.0 as usize]
|
||||
.partial_cmp(&dm.map[a.0 as usize])
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
Some(exits[0].0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::prelude::*;
|
||||
use bracket_algorithm_traits::prelude::*;
|
||||
// 1 by 3 stripe of tiles
|
||||
struct MiniMap;
|
||||
impl BaseMap for MiniMap {
|
||||
fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> {
|
||||
match idx {
|
||||
0 => smallvec![(1, 1.)],
|
||||
2 => smallvec![(1, 1.)],
|
||||
_ => smallvec![(idx - 1, 1.), (idx + 1, 2.)],
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn test_highest_exit() {
|
||||
let map = MiniMap {};
|
||||
let exits_map = DijkstraMap::new(3, 1, &[0], &map, 10.);
|
||||
let target = DijkstraMap::find_highest_exit(&exits_map, 0, &map);
|
||||
assert_eq!(target, Some(1));
|
||||
let target = DijkstraMap::find_highest_exit(&exits_map, 1, &map);
|
||||
assert_eq!(target, Some(2));
|
||||
}
|
||||
}
|
|
@ -68,6 +68,8 @@ impl DijkstraMap {
|
|||
|
||||
if let Some(pos) = map.starting_point {
|
||||
open_list.push_back(((pos.x, pos.y), 0.0));
|
||||
let idx = self.xy_idx(pos.x, pos.y);
|
||||
self.tiles[idx] = 0.0;
|
||||
}
|
||||
|
||||
while let Some(((x, y), depth)) = open_list.pop_front() {
|
||||
|
@ -78,7 +80,7 @@ impl DijkstraMap {
|
|||
let prev_depth = self.tiles[idx];
|
||||
if new_depth >= prev_depth { continue; }
|
||||
if new_depth >= self.max_depth { continue; }
|
||||
self.tiles[idx] = depth;
|
||||
self.tiles[idx] = new_depth;
|
||||
open_list.push_back(((x, y), new_depth));
|
||||
}
|
||||
}
|
||||
|
@ -87,8 +89,6 @@ impl DijkstraMap {
|
|||
fn xy_idx(&self, x: usize, y: usize) -> usize {
|
||||
(y * self.size_x ) + x
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------------------------------------------
|
||||
|
@ -107,9 +107,11 @@ mod tests {
|
|||
##########
|
||||
";
|
||||
let mut map = Map::from_string(map_str);
|
||||
map.starting_point = Some(Point::new(9, 1));
|
||||
map.starting_point = Some(Point::new(8, 1));
|
||||
let dm = DijkstraMap::new(&map);
|
||||
|
||||
println!("{:?}", &dm.tiles.iter().map(|&v| if v == f32::MAX {9.0} else {v}).collect::<Vec<f32>>());
|
||||
|
||||
assert_eq!(dm.size_x, 10);
|
||||
assert_eq!(dm.size_y, 3);
|
||||
for i in 0..10 {
|
||||
|
@ -123,4 +125,44 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_2() {
|
||||
let map_str = "
|
||||
####
|
||||
# #
|
||||
# #
|
||||
####
|
||||
";
|
||||
let mut map = Map::from_string(map_str);
|
||||
map.starting_point = Some(Point::new(2, 2));
|
||||
let dm = DijkstraMap::new(&map);
|
||||
let expected = [MAX, MAX, MAX, MAX,
|
||||
MAX, 1.45, 1.0, MAX,
|
||||
MAX, 1.0, 0.0, MAX,
|
||||
MAX, MAX, MAX, MAX];
|
||||
|
||||
assert_eq!(dm.tiles, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_3() {
|
||||
let map_str = "
|
||||
##########
|
||||
# #
|
||||
# # #
|
||||
##########
|
||||
";
|
||||
let mut map = Map::from_string(map_str);
|
||||
map.starting_point = Some(Point::new(8, 2));
|
||||
let dm = DijkstraMap::new(&map);
|
||||
let expected = [MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX,
|
||||
MAX, 7.45, 6.45, 5.45, 4.45, 3.45, 2.45, 1.45, 1.0, MAX,
|
||||
MAX, 7.9, 6.9, MAX, 4.0, 3.0, 2.0, 1.0, 0.0, MAX,
|
||||
MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX, MAX];
|
||||
|
||||
for (v, e) in dm.tiles.iter().zip(expected.iter()) {
|
||||
assert!(f32::abs(v - e) <= 0.01);
|
||||
}
|
||||
}
|
||||
}
|
75
src/dungeon/distant_exit.rs
Normal file
75
src/dungeon/distant_exit.rs
Normal file
|
@ -0,0 +1,75 @@
|
|||
//! Add exit point to the map
|
||||
//!
|
||||
//! This modifier will try to add exit point as far as possible from the starting point.
|
||||
//! It means that starting point have to be set before this Modyfier will start.
|
||||
//!
|
||||
|
||||
use std::f32;
|
||||
use rand::prelude::StdRng;
|
||||
use super::{MapModifier};
|
||||
use super::map::{Map, Point};
|
||||
use super::dijkstra::DijkstraMap;
|
||||
|
||||
|
||||
/// Add exist position to the map based on the distance from the start point.
|
||||
pub struct DistantExit {}
|
||||
|
||||
impl MapModifier for DistantExit {
|
||||
fn modify_map(&self, _: &mut StdRng, map: &Map) -> Map {
|
||||
self.build(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl DistantExit {
|
||||
#[allow(dead_code)]
|
||||
pub fn new() -> Box<DistantExit> {
|
||||
Box::new(DistantExit{})
|
||||
}
|
||||
|
||||
fn build(&self, map: &Map) -> Map {
|
||||
let mut new_map = map.clone();
|
||||
|
||||
let mut best_idx = 0;
|
||||
let mut best_value = 0.0;
|
||||
let dijkstra_map = DijkstraMap::new(map);
|
||||
for (i, &value) in dijkstra_map.tiles.iter().enumerate() {
|
||||
if value < f32::MAX && value > best_value {
|
||||
best_value = value;
|
||||
best_idx = i;
|
||||
}
|
||||
}
|
||||
let x = best_idx % map.width;
|
||||
let y = best_idx / map.width;
|
||||
new_map.exit_point = Some(Point::new(x, y));
|
||||
new_map
|
||||
}
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------------------------------------------
|
||||
/// Module unit tests
|
||||
/// ------------------------------------------------------------------------------------------------
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rand::prelude::*;
|
||||
use super::*;
|
||||
use super::MapModifier;
|
||||
use crate::dungeon::map::{Point, Map};
|
||||
|
||||
#[test]
|
||||
fn test_exit() {
|
||||
let map_str = "
|
||||
##########
|
||||
# #
|
||||
# # #
|
||||
##########
|
||||
";
|
||||
let mut map = Map::from_string(map_str);
|
||||
map.starting_point = Some(Point::new(9, 2));
|
||||
|
||||
let modifier = DistantExit::new();
|
||||
let mut rng = StdRng::seed_from_u64(0);
|
||||
let new_map = modifier.modify_map(&mut rng, &map);
|
||||
|
||||
assert_eq!(new_map.exit_point, Some(Point::new(1, 2)));
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@
|
|||
pub mod map;
|
||||
pub mod cellular_automata;
|
||||
pub mod cull_unreachable;
|
||||
pub mod distant_exit;
|
||||
pub mod starting_point;
|
||||
mod dijkstra;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
//! This modifier will try to add starting point by finding the floor title closes
|
||||
//! to the given point.
|
||||
//!
|
||||
//! Example generator usage:
|
||||
//! Example modifier usage:
|
||||
//! ```
|
||||
//! use rand::prelude::*;
|
||||
//! use mapgen::dungeon::{
|
||||
|
|
Loading…
Reference in New Issue
Block a user