diff --git a/README.md b/README.md index 093f73c..13cbdb2 100644 --- a/README.md +++ b/README.md @@ -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(); ``` diff --git a/demo/src/main.rs b/demo/src/main.rs index 4d2548a..783e539 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -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, world: &World) -> Option { let map = world.read_resource::(); - 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, world: &World) -> Srgba { let map = world.read_resource::(); - 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); } diff --git a/src/dungeon/dijkstra copy.rs b/src/dungeon/dijkstra copy.rs deleted file mode 100644 index fc206dd..0000000 --- a/src/dungeon/dijkstra copy.rs +++ /dev/null @@ -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, - 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 index de94f52..f645977 100644 --- a/src/dungeon/dijkstra.rs +++ b/src/dungeon/dijkstra.rs @@ -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::>()); + 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); + } + } } \ No newline at end of file diff --git a/src/dungeon/distant_exit.rs b/src/dungeon/distant_exit.rs new file mode 100644 index 0000000..a22f272 --- /dev/null +++ b/src/dungeon/distant_exit.rs @@ -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 { + 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))); + } +} \ No newline at end of file diff --git a/src/dungeon/mod.rs b/src/dungeon/mod.rs index 3f9f692..7064e77 100644 --- a/src/dungeon/mod.rs +++ b/src/dungeon/mod.rs @@ -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; diff --git a/src/dungeon/starting_point.rs b/src/dungeon/starting_point.rs index 8cb73fe..1007887 100644 --- a/src/dungeon/starting_point.rs +++ b/src/dungeon/starting_point.rs @@ -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::{