diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ed4fa3e..441ef83 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -70,7 +70,7 @@ flowchart LR | `rltk` | Backward-compatible facade that maps legacy names onto `bracket-lib` | `bracket-lib`, tutorial/legacy codebases | | `bracket-bevy` | Bevy-oriented CP437/ASCII integration entry point | `bevy`, `bracket-color`, `bracket-geometry`, `bracket-random` | | `bracket-terminal` | Terminal runtime, frame loop, input, and backend-specific rendering | `bracket-color`, `bracket-geometry`, `bracket-rex`, `bracket-embedding` | -| `bracket-pathfinding` | A\*, Dijkstra, and FOV over user-provided map traits | `bracket-algorithm-traits`, `bracket-geometry` | +| `bracket-pathfinding` | A\*, Dijkstra, Floyd-Warshall and FOV over user-provided map traits | `bracket-algorithm-traits`, `bracket-geometry` | | `bracket-algorithm-traits` | Abstractions (`Algorithm2D/3D`, `BaseMap`) that decouple algorithms from storage layout | `bracket-geometry`, `bracket-pathfinding` | | `bracket-noise` | Noise generation utilities (FastNoise-style) | `bracket-random` | | `bracket-geometry` | Points, rectangles, lines, circles, and distance helpers | `bracket-algorithm-traits`, `bracket-pathfinding`, `bracket-terminal` | diff --git a/bracket-pathfinding/README.md b/bracket-pathfinding/README.md index 3223623..ac4ed4c 100755 --- a/bracket-pathfinding/README.md +++ b/bracket-pathfinding/README.md @@ -105,6 +105,19 @@ Once you have the map, you can access individual distances at `flow_map.map` - o The example `dijkstra` demonstrates this. +## Floyd-Warshall Mapping + +Bracket-lib also includes Floyd-Warshall maps. Unlike Dijkstra maps, Floyd-Warshall computes the shortest path between every pair of points on the map. + +```rust +let flow_map = FloydWarshallMap::new(MAP_WIDTH, MAP_HEIGHT, &map, 1024.0); +``` + +Once you have the map, you can use helper functions such as `find_highest_exit` and `find_lowest_exit` to assist with path-finding, similarly to Dijkstra maps. + +Warning: Floyd-Warshall has a significantly higher computational cost than Dijkstra mapping. Large maps may take a long time to generate and can consume a considerable amount of memory. + + ## Field of View (2D only for now) With `is_opaque` defined for your `BaseMap` trait, obtaining a set of all visible tiles is easy: @@ -121,10 +134,11 @@ If you enable the `threaded` feature, some Dijkstra functions will use a multi-t ## Examples -There are three examples (ignore `common.rs` - it's shared code): +There are four examples (ignore `common.rs` - it's shared code): * `astar` (`cargo run --example astar`), demonstrating A-Star pathing across a random map. * `dijkstra` (`cargo run --example dijkstra`), demonstrating Dijkstra mapping to two targets. +* `floyd warshall` (`cargo run --example floyd_warshall`), demonstrating all-pairs shortest-path mapping on a random map. * `fov` (`cargo run --example fov`), demonstrating field-of-view generation. These use `crossterm` for rendering to your terminal. diff --git a/bracket-pathfinding/examples/floyd_warshall/common.rs b/bracket-pathfinding/examples/floyd_warshall/common.rs new file mode 100644 index 0000000..26a8365 --- /dev/null +++ b/bracket-pathfinding/examples/floyd_warshall/common.rs @@ -0,0 +1,114 @@ +use bracket_color::prelude::*; +use bracket_pathfinding::prelude::*; +use crossterm::queue; +use crossterm::style::{Color::Rgb, Print, SetForegroundColor}; +use std::io::{Write, stdout}; + +// Console Support + +pub fn print_color(color: RGB, text: &str) { + queue!( + stdout(), + SetForegroundColor(Rgb { + r: (color.r * 255.0) as u8, + g: (color.g * 255.0) as u8, + b: (color.b * 255.0) as u8, + }) + ) + .expect("Command Fail"); + queue!(stdout(), Print(text)).expect("Command fail"); +} + +pub fn flush_console() { + stdout().flush().expect("Flush Fail"); +} + +// Map + +pub const MAP_WIDTH: usize = 80; +pub const MAP_HEIGHT: usize = 20; +pub const MAP_TILES: usize = MAP_WIDTH * MAP_HEIGHT; +pub const START_POINT: Point = Point::constant(2, 2); + +pub struct Map { + pub tiles: Vec, +} + +impl Map { + pub fn new() -> Self { + let mut tiles = Self { + tiles: vec!['.'; MAP_TILES], + }; + + // Add walls + for i in 0..15 { + tiles.tiles[10 + i * MAP_WIDTH] = '#'; + tiles.tiles[18 + (i + 5) * MAP_WIDTH] = '#'; + } + + tiles + } + + fn valid_exit(&self, loc: Point, delta: Point) -> Option { + let destination = loc + delta; + if self.in_bounds(destination) { + let idx = self.point2d_to_index(destination); + if self.tiles[idx] != '#' { + Some(idx) + } else { + None + } + } else { + None + } + } +} + +impl BaseMap for Map { + fn is_opaque(&self, idx: usize) -> bool { + self.tiles[idx] == '#' + } + + fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> { + let mut exits = SmallVec::new(); + let location = self.index_to_point2d(idx); + + if let Some(idx) = self.valid_exit(location, Point::new(-1, 0)) { + exits.push((idx, 1.0)) + } + if let Some(idx) = self.valid_exit(location, Point::new(1, 0)) { + exits.push((idx, 1.0)) + } + if let Some(idx) = self.valid_exit(location, Point::new(0, -1)) { + exits.push((idx, 1.0)) + } + if let Some(idx) = self.valid_exit(location, Point::new(0, 1)) { + exits.push((idx, 1.0)) + } + + if let Some(idx) = self.valid_exit(location, Point::new(-1, -1)) { + exits.push((idx, 1.4)) + } + if let Some(idx) = self.valid_exit(location, Point::new(-1, 1)) { + exits.push((idx, 1.4)) + } + if let Some(idx) = self.valid_exit(location, Point::new(1, -1)) { + exits.push((idx, 1.4)) + } + if let Some(idx) = self.valid_exit(location, Point::new(1, 1)) { + exits.push((idx, 1.4)) + } + + exits + } + + fn get_pathing_distance(&self, idx1: usize, idx2: usize) -> f32 { + DistanceAlg::Pythagoras.distance2d(self.index_to_point2d(idx1), self.index_to_point2d(idx2)) + } +} + +impl Algorithm2D for Map { + fn dimensions(&self) -> Point { + Point::new(MAP_WIDTH, MAP_HEIGHT) + } +} diff --git a/bracket-pathfinding/examples/floyd_warshall/main.rs b/bracket-pathfinding/examples/floyd_warshall/main.rs new file mode 100644 index 0000000..9a731ae --- /dev/null +++ b/bracket-pathfinding/examples/floyd_warshall/main.rs @@ -0,0 +1,42 @@ +mod common; +use bracket_color::prelude::*; +use bracket_pathfinding::prelude::*; +use common::*; + +fn main() { + let map = Map::new(); + + // Perform the search + let flow_map = FloydWarshallMap::new(MAP_WIDTH, MAP_HEIGHT, &map, 1024.0); + let start_idx = map.point2d_to_index(START_POINT); + + // Draw the result + for y in 0..MAP_HEIGHT { + for x in 0..MAP_WIDTH { + let idx = y * MAP_WIDTH + x; + let depth_map_idx = flow_map.idx_helper(start_idx, idx); + + let tile = map.tiles[idx]; + let color = match tile { + '#' => RGB::named(YELLOW), + _ => { + if flow_map.depth_map[depth_map_idx] < f32::MAX { + RGB::from_u8( + 0, + 255 - { + let n = flow_map.depth_map[depth_map_idx] * 12.0; + if n > 255.0 { 255.0 } else { n } + } as u8, + 0, + ) + } else { + RGB::named(CHOCOLATE) + } + } + }; + print_color(color, &tile.to_string()); + } + print_color(RGB::named(WHITE), "\n"); + } + flush_console(); +} diff --git a/bracket-pathfinding/src/floyd_warshall.rs b/bracket-pathfinding/src/floyd_warshall.rs new file mode 100644 index 0000000..793d787 --- /dev/null +++ b/bracket-pathfinding/src/floyd_warshall.rs @@ -0,0 +1,172 @@ +use bracket_algorithm_traits::prelude::BaseMap; +#[allow(unused_imports)] +use smallvec::SmallVec; +use std::convert::TryInto; + +pub struct FloydWarshallMap { + pub depth_map: Vec, + size_x: usize, + size_y: usize, + max_depth: f32, +} + +#[allow(dead_code)] +impl FloydWarshallMap { + /// Construct a new FloydWarshall 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, map: &dyn BaseMap, max_depth: f32) -> FloydWarshallMap + 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![f32::MAX; (sz_x * sz_y) * (sz_x * sz_y)]; + let mut f = FloydWarshallMap { + depth_map: result, + size_x: sz_x, + size_y: sz_y, + max_depth, + }; + FloydWarshallMap::build(&mut f, map); + f + } + + /// Helper for indexing the Floyd-Warshall distance map. + /// Converts a start node index and end node index into a + /// single index within the flattened depth_map array. + pub fn idx_helper(&self, start_idx: usize, end_idx: usize) -> usize { + start_idx * (self.size_x * self.size_y) + end_idx + } + + pub fn build(fm: &mut FloydWarshallMap, map: &dyn BaseMap) { + let mapsize: usize = fm.size_x * fm.size_y; + + for start_idx in 0..mapsize { + let idx = fm.idx_helper(start_idx, start_idx); + fm.depth_map[idx] = 0.; + } + + for start_idx in 0..mapsize { + for (end_idx, depth) in map.get_available_exits(start_idx) { + let ste_idx = fm.idx_helper(start_idx, end_idx); + fm.depth_map[ste_idx] = depth; + } + } + + for mid_idx in 0..mapsize { + for start_idx in 0..mapsize { + let stm_idx = fm.idx_helper(start_idx, mid_idx); + for end_idx in 0..mapsize { + let ste_idx = fm.idx_helper(start_idx, end_idx); + let mte_idx = fm.idx_helper(mid_idx, end_idx); + let new_depth = fm.depth_map[stm_idx] + fm.depth_map[mte_idx]; + let prev_depth = fm.depth_map[ste_idx]; + + fm.depth_map[ste_idx] = f32::min(new_depth, prev_depth); + } + } + } + } + + /// 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. + pub fn find_lowest_exit( + fm: &FloydWarshallMap, + position: usize, + map: &dyn BaseMap, + ) -> Option { + let exits = map.get_available_exits(position); + + if exits.is_empty() { + return None; + } + + let mut lowest_depth = fm.max_depth; + let mut lowest_exit = 0; + + for (exit, _) in exits { + let pos = fm.idx_helper(position, exit); + if lowest_depth > fm.depth_map[pos] { + lowest_exit = exit; + lowest_depth = fm.depth_map[pos] + } + } + + Some(lowest_exit) + } + + /// 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. + pub fn find_highest_exit( + fm: &FloydWarshallMap, + position: usize, + map: &dyn BaseMap, + ) -> Option { + let exits = map.get_available_exits(position); + + if exits.is_empty() { + return None; + } + + let mut highest_depth = f32::MIN; + let mut highest_exit = 0; + + for (exit, _) in exits { + let pos = fm.idx_helper(position, exit); + if highest_depth < fm.depth_map[pos] { + highest_exit = exit; + highest_depth = fm.depth_map[pos] + } + } + + Some(highest_exit) + } +} + +#[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_new() { + let map = MiniMap {}; + + let test_map = FloydWarshallMap::new(3, 1, &map, 10.); + assert_eq!(test_map.depth_map, vec![0., 1., 3., 1., 0., 2., 2., 1., 0.]); + } + + #[test] + fn test_lowest_exit() { + let map = MiniMap {}; + let exits_map = FloydWarshallMap::new(3, 1, &map, 10.); + let target = FloydWarshallMap::find_lowest_exit(&exits_map, 0, &map); + assert_eq!(target, Some(1)); + let target = FloydWarshallMap::find_lowest_exit(&exits_map, 1, &map); + assert_eq!(target, Some(0)); + } + + #[test] + fn test_highest_exit() { + let map = MiniMap {}; + let exits_map = FloydWarshallMap::new(3, 1, &map, 10.); + let target = FloydWarshallMap::find_highest_exit(&exits_map, 0, &map); + assert_eq!(target, Some(1)); + let target = FloydWarshallMap::find_highest_exit(&exits_map, 1, &map); + assert_eq!(target, Some(2)); + } +} diff --git a/bracket-pathfinding/src/lib.rs b/bracket-pathfinding/src/lib.rs index a1956b3..7e0b959 100755 --- a/bracket-pathfinding/src/lib.rs +++ b/bracket-pathfinding/src/lib.rs @@ -3,11 +3,13 @@ mod astar; mod dijkstra; mod field_of_view; +mod floyd_warshall; pub mod prelude { pub use crate::astar::*; pub use crate::dijkstra::*; pub use crate::field_of_view::*; + pub use crate::floyd_warshall::*; pub use bracket_algorithm_traits::prelude::*; pub use bracket_geometry::prelude::*;