diff --git a/README.md b/README.md index 61beef5..093f73c 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ If you want to check how the maps look like, then: * [ ] Area exit point * [x] Area starting point * [x] Cellular automata - * [ ] Cull unreachable areas + * [x] Cull unreachable areas * [ ] Voronoi spawning @@ -61,6 +61,21 @@ let gen = CellularAutomataGen::new(80, 50); let map = gen.generate_map(&mut rng) ``` +Use MapBuilder for chaining map generator and modifiers + +```rust +use mapgen::dungeon::{ + MapBuilder, + map::{Map, Point, TileType}, + cellular_automata::CellularAutomataGen, + starting_point::{AreaStartingPosition, XStart, YStart}, +}; + +let map = MapBuilder::new(Box::new(CellularAutomataGen::new(80, 50))) + .with(AreaStartingPosition::new(XStart::CENTER, YStart::CENTER)) + .build_map(); +``` + For more information check the [doc](https://docs.rs/mapgen) # License diff --git a/demo/src/main.rs b/demo/src/main.rs index 5a4ef57..4d2548a 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -25,6 +25,7 @@ use mapgen::dungeon::{ map::{Map, Point, TileType}, cellular_automata::CellularAutomataGen, starting_point::{AreaStartingPosition, XStart, YStart}, + cull_unreachable::CullUnreachable, }; @@ -83,6 +84,7 @@ fn init_camera(world: &mut World, transform: Transform, camera: Camera) -> Entit 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()) .build_map(); world.insert(map); } diff --git a/src/dungeon/cull_unreachable.rs b/src/dungeon/cull_unreachable.rs new file mode 100644 index 0000000..efbd291 --- /dev/null +++ b/src/dungeon/cull_unreachable.rs @@ -0,0 +1,71 @@ +use rand::prelude::StdRng; +use super::MapModifier; +use super::map::{Map, TileType}; +use super::dijkstra::DijkstraMap; + + +pub struct CullUnreachable {} + +impl MapModifier for CullUnreachable { + fn modify_map(&self, _: &mut StdRng, map: &Map) -> Map { + self.build(map) + } +} + +impl CullUnreachable { + #[allow(dead_code)] + pub fn new() -> Box { + Box::new(CullUnreachable{}) + } + + fn build(&self, map: &Map) -> Map { + let mut new_map = map.clone(); + + let dijkstra_map = DijkstraMap::new(map); + for (i, tile) in new_map.tiles.iter_mut().enumerate() { + if *tile == TileType::Floor { + let distance_to_start = dijkstra_map.tiles[i]; + // We can't get to this tile - so we'll make it a wall + if distance_to_start == std::f32::MAX { + *tile = TileType::Wall; + } + } + } + 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_culling() { + let map_str = " + ########## + # # # + ########## + "; + let mut map = Map::from_string(map_str); + map.starting_point = Some(Point::new(9, 1)); + let expected_map_str = " + ########## + #### # + ########## + "; + let expected_map = Map::from_string(expected_map_str); + + + let modifier = CullUnreachable::new(); + let mut rng = StdRng::seed_from_u64(0); + let new_map = modifier.modify_map(&mut rng, &map); + + assert_eq!(new_map.tiles, expected_map.tiles); + } +} \ No newline at end of file diff --git a/src/dungeon/dijkstra copy.rs b/src/dungeon/dijkstra copy.rs new file mode 100644 index 0000000..fc206dd --- /dev/null +++ b/src/dungeon/dijkstra copy.rs @@ -0,0 +1,271 @@ +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, + 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( + size_x: T, + size_y: T, + starts: &[usize], + map: &dyn BaseMap, + max_depth: f32, + ) -> DijkstraMap + where + T: TryInto, + { + let sz_x: usize = size_x.try_into().ok().unwrap(); + let sz_y: usize = size_y.try_into().ok().unwrap(); + let result: Vec = 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(size_x: T, size_y: T, max_depth: f32) -> DijkstraMap + where + T: TryInto, + { + let sz_x: usize = size_x.try_into().ok().unwrap(); + let sz_y: usize = size_y.try_into().ok().unwrap(); + let result: Vec = 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 = 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> = (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 { + 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 { + 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 { + 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 { + 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)); + } +} \ No newline at end of file diff --git a/src/dungeon/dijkstra.rs b/src/dungeon/dijkstra.rs new file mode 100644 index 0000000..de94f52 --- /dev/null +++ b/src/dungeon/dijkstra.rs @@ -0,0 +1,126 @@ +//! Calculate Dijkstra influence map +//! +//! http://www.roguebasin.com/index.php?title=The_Incredible_Power_of_Dijkstra_Maps +//! +//! This algorithm calculates cost (distance) of moving from the given starting point +//! to the each point on the map. Point which are not reachable will get f32::MAX value. +//! +//! Example generator usage: +//! --- +//! use rand::prelude::*; +//! use mapgen::dungeon::{ +//! MapModifier, +//! map::{Map, Point, TileType}, +//! starting_point::{AreaStartingPosition, XStart, YStart} +//! }; +//! +//! let mut rng = StdRng::seed_from_u64(100); +//! let mut map = Map::new(80, 50); +//! map.set_tile(10, 10, TileType::Floor); +//! let modifier = AreaStartingPosition::new(XStart::LEFT, YStart::TOP); +//! let new_map = modifier.modify_map(&mut rng, &map); +//! +//! assert_eq!(new_map.starting_point, Some(Point::new(10, 10))); +//! --- +//! + +use std::collections::VecDeque; +use std::f32::MAX; +use super::map::Map; + + +/// 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 tiles: Vec, + 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(map: &Map) -> DijkstraMap { + let len = map.width * map.height; + let tiles = vec![MAX; len]; + let mut d = DijkstraMap { + tiles: tiles, + size_x: map.width, + size_y: map.height, + max_depth: len as f32, + }; + d.build(map); + d + } + + /// 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 + fn build(self: &mut DijkstraMap, map: &Map) { + let mapsize: usize = (self.size_x * self.size_y) as usize; + let mut open_list: VecDeque<((usize, usize), f32)> = VecDeque::with_capacity(mapsize); + + if let Some(pos) = map.starting_point { + open_list.push_back(((pos.x, pos.y), 0.0)); + } + + while let Some(((x, y), depth)) = open_list.pop_front() { + let exits = map.get_available_exits(x, y); + for (x, y, add_depth) in exits { + let idx = self.xy_idx(x, y); + let new_depth = depth + add_depth; + let prev_depth = self.tiles[idx]; + if new_depth >= prev_depth { continue; } + if new_depth >= self.max_depth { continue; } + self.tiles[idx] = depth; + open_list.push_back(((x, y), new_depth)); + } + } + } + + fn xy_idx(&self, x: usize, y: usize) -> usize { + (y * self.size_x ) + x + } + + +} + +/// ------------------------------------------------------------------------------------------------ +/// Module unit tests +/// ------------------------------------------------------------------------------------------------ +#[cfg(test)] +mod tests { + use super::*; + use crate::dungeon::map::{Point, Map}; + + #[test] + fn test_culling() { + let map_str = " + ########## + # # # + ########## + "; + let mut map = Map::from_string(map_str); + map.starting_point = Some(Point::new(9, 1)); + let dm = DijkstraMap::new(&map); + + assert_eq!(dm.size_x, 10); + assert_eq!(dm.size_y, 3); + for i in 0..10 { + assert_eq!(dm.tiles[i], MAX); + assert_eq!(dm.tiles[2*dm.size_x + i], MAX); + let idx = dm.size_x + i; + if i < 3 || i == 9 { + assert_eq!(dm.tiles[idx], MAX); + } else { + assert_eq!(dm.tiles[idx], (8 - i) as f32); + } + } + } +} \ No newline at end of file diff --git a/src/dungeon/map.rs b/src/dungeon/map.rs index 99f13c5..44c8f4d 100644 --- a/src/dungeon/map.rs +++ b/src/dungeon/map.rs @@ -7,11 +7,14 @@ //! specific game. //! +use std::fmt; + + /// Position on the map #[derive(PartialEq, Copy, Clone, Debug, Eq, Hash)] pub struct Point { - x: usize, - y: usize + pub x: usize, + pub y: usize } impl Point { @@ -58,12 +61,58 @@ impl Map { } } + /// Create map from given string + pub fn from_string(map_string: &str) -> Map { + let lines: Vec<&str> = map_string.split("\n") + .map(|l| l.trim()) + .filter(|l| l.len() > 0) + .collect(); + let cols = lines.iter().map(|l| l.len()).max().get_or_insert(1).to_owned(); + let rows = lines.len(); + let mut map = Map::new(cols, rows); + + for i in 0..rows { + let line = lines[i].as_bytes(); + for j in 0..line.len() { + if line[j] as char == ' ' { + map.set_tile(j, i, TileType::Floor); + } + } + } + map + } + /// Get TileType at the given location pub fn at(&self, x: usize, y: usize) -> TileType { let idx = y * self.width + x; self.tiles[idx] } + /// Get available exists from the given tile + pub fn get_available_exits(&self, x: usize, y: usize) -> Vec<(usize, usize, f32)> { + let mut exits = Vec::new(); + + // Cardinal directions + if self.is_exit_valid(x-1, y) { exits.push((x-1, y, 1.0)) }; + if self.is_exit_valid(x+1, y) { exits.push((x+1, y, 1.0)) }; + if self.is_exit_valid(x, y-1) { exits.push((x, y-1, 1.0)) }; + if self.is_exit_valid(x, y+1) { exits.push((x, y+1, 1.0)) }; + + // Diagonals + if self.is_exit_valid(x-1, y-1) { exits.push((x-1, y-1, 1.45)); } + if self.is_exit_valid(x+1, y-1) { exits.push((x+1, y-1, 1.45)); } + if self.is_exit_valid(x-1, y+1) { exits.push((x-1, y+1, 1.45)); } + if self.is_exit_valid(x+1, y+1) { exits.push((x+1, y+1, 1.45)); } + + exits + } + + // Check if given tile can be accessed + fn is_exit_valid(&self, x:usize, y:usize) -> bool { + if x < 1 || x > self.width-1 || y < 1 || y > self.height-1 { return false; } + self.at(x, y) == TileType::Floor + } + /// Modify tile at the given location pub fn set_tile(&mut self, x: usize, y: usize, tile: TileType) { let idx = y * self.width + x; @@ -71,6 +120,19 @@ impl Map { } } +impl fmt::Display for Map { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for y in 0..self.height { + let bytes: Vec = (0..self.width) + .map(|x| if self.at(x, y) == TileType::Wall {'#'} else {' '} as u8) + .collect(); + let line = String::from_utf8(bytes).expect("Can't convert map to string"); + let _ = write!(f, "{}\n", line); + } + Ok(()) + } +} + /// ------------------------------------------------------------------------------------------------ /// Module unit tests /// ------------------------------------------------------------------------------------------------ @@ -95,4 +157,40 @@ mod tests { } } } + + #[test] + fn test_from_string() { + let map_str = " + ########## + # # + ########## + "; + let map = Map::from_string(map_str); + + assert_eq!(map.width, 10); + assert_eq!(map.height, 3); + for i in 0..10 { + assert_eq!(map.at(i, 0), TileType::Wall); + assert_eq!(map.at(i, 2), TileType::Wall); + if i == 0 || i == 9 { + assert_eq!(map.at(i, 1), TileType::Wall); + } else { + assert_eq!(map.at(i, 1), TileType::Floor); + } + } + } + + #[test] + fn test_exists() { + let map_str = " + ########## + # # + # # + ########## + "; + let map = Map::from_string(map_str); + let exists = map.get_available_exits(1, 1); + let expected_exists = vec![(2, 1, 1.0), (1, 2, 1.0), (2, 2, 1.45)]; + assert_eq!(exists, expected_exists); + } } \ No newline at end of file diff --git a/src/dungeon/mod.rs b/src/dungeon/mod.rs index 4a6c9b4..3f9f692 100644 --- a/src/dungeon/mod.rs +++ b/src/dungeon/mod.rs @@ -27,7 +27,9 @@ pub mod map; pub mod cellular_automata; +pub mod cull_unreachable; pub mod starting_point; +mod dijkstra; use std::time::{SystemTime, UNIX_EPOCH}; use rand::prelude::*;