diff --git a/bracket-pathfinding/Cargo.toml b/bracket-pathfinding/Cargo.toml index 59b8df99..8a670013 100755 --- a/bracket-pathfinding/Cargo.toml +++ b/bracket-pathfinding/Cargo.toml @@ -4,7 +4,7 @@ version = "0.8.7" authors = ["Herbert Wolverson "] edition = "2024" publish = true -description = "Pathfinding and field-of view utilities. A Star, Dijkstra. Part of the bracket-lib family." +description = "Pathfinding and field-of view utilities. A Star, BFS, Dijkstra. Part of the bracket-lib family." homepage = "https://github.com/thebracket/bracket-lib" repository = "https://github.com/thebracket/bracket-lib" readme = "README.md" diff --git a/bracket-pathfinding/README.md b/bracket-pathfinding/README.md index 3223623e..d955e387 100755 --- a/bracket-pathfinding/README.md +++ b/bracket-pathfinding/README.md @@ -1,6 +1,6 @@ # bracket-pathfinding -This crate is part of the overall `bracket-lib` system, and (in conjunction with `bracket-algorithm-traits`) provides pathfinding functionality. A-Star (A*) and Dijkstra are supported. It also provides field of view (FOV) functionality. +This crate is part of the overall `bracket-lib` system, and (in conjunction with `bracket-algorithm-traits`) provides pathfinding functionality. A-Star (A*), breadth-first search (BFS), and Dijkstra are supported. It also provides field of view (FOV) functionality. ## Trait Implementation @@ -26,7 +26,7 @@ impl BaseMap for Map { } ``` -Dijkstra and A-Star need to know what exits are valid from a tile, and the "cost" of moving to that tile (most of the time you can use `1.0`). For example: +BFS, Dijkstra and A-Star need to know what exits are valid from a tile. Dijkstra and A-Star use the "cost" of moving to that tile (most of the time you can use `1.0`), while BFS treats each exit as one step. For example: ```rust impl BaseMap for Map { @@ -88,6 +88,23 @@ let path = a_star_search( The example `astar` demonstrates this. +## BFS Mapping + +Bracket-lib includes BFS maps for unweighted flow mapping. BFS can include as many search targets as you want, and treats each valid exit as one step regardless of the exit cost. + +To generate a BFS map, you need a vector of target tile indices. You can then make the map: + +```rust +let mut search_targets : Vec = Vec::new(); +search_targets.push(map.point2d_to_index(START_POINT)); +search_targets.push(map.point2d_to_index(END_POINT)); +let flow_map = BfsMap::new(MAP_WIDTH, MAP_HEIGHT, &search_targets, &map, 1024.0); +``` + +Once you have the map, you can access individual distances at `flow_map.map` - or you can use helper functions such as `find_highest_exit` and `find_lowest_exit` to help with path-finding. + +The example `bfs` demonstrates this. + ## Dijkstra Mapping Bracket-lib also includes Dijkstra maps, that can include as many search targets as you want. See [The Incredible Power of Dijkstra Maps](http://www.roguebasin.com/index.php?title=The_Incredible_Power_of_Dijkstra_Maps) for some ideas as to what you can do with this. @@ -101,7 +118,7 @@ search_targets.push(map.point2d_to_index(END_POINT)); let flow_map = DijkstraMap::new(MAP_WIDTH, MAP_HEIGHT, &search_targets, &map, 1024.0); ``` -Once you have the map, you can access individual distances at `flow_map.map` - or you can use various helper functions such as `find_highest_exist` and `find_lowest_exit` to help with path-finding. +Once you have the map, you can access individual distances at `flow_map.map` - or you can use helper functions such as `find_highest_exit` and `find_lowest_exit` to help with path-finding. The example `dijkstra` demonstrates this. @@ -117,14 +134,17 @@ You can see this in action with the example `fov`. ## Feature Flags -If you enable the `threaded` feature, some Dijkstra functions will use a multi-threaded algorithm. +If you enable the `threaded` feature, some BFS and Dijkstra functions will use a multi-threaded algorithm. ## Examples -There are three examples (ignore `common.rs` - it's shared code): +There are six examples (ignore `common.rs` - it's shared code): * `astar` (`cargo run --example astar`), demonstrating A-Star pathing across a random map. +* `astar_manhattan` (`cargo run --example astar_manhattan`), demonstrating A-Star pathing with Manhattan distance. +* `bfs` (`cargo run --example bfs`), demonstrating BFS mapping to two targets. * `dijkstra` (`cargo run --example dijkstra`), demonstrating Dijkstra mapping to two targets. +* `dijkstra_weighted` (`cargo run --example dijkstra_weighted`), demonstrating weighted Dijkstra mapping. * `fov` (`cargo run --example fov`), demonstrating field-of-view generation. These use `crossterm` for rendering to your terminal. diff --git a/bracket-pathfinding/examples/bfs/main.rs b/bracket-pathfinding/examples/bfs/main.rs new file mode 100644 index 00000000..a9f062b2 --- /dev/null +++ b/bracket-pathfinding/examples/bfs/main.rs @@ -0,0 +1,47 @@ +#[path = "../dijkstra/common.rs"] +mod common; + +use bracket_color::prelude::*; +use bracket_pathfinding::prelude::*; +use common::*; + +fn main() { + let map = Map::new(); + + // Perform the search + let search_targets: Vec = vec![ + map.point2d_to_index(START_POINT), + map.point2d_to_index(END_POINT), + ]; + let flow_map = BfsMap::new(MAP_WIDTH, MAP_HEIGHT, &search_targets, &map, 1024.0); + + // Draw the result + for y in 0..MAP_HEIGHT { + let base_idx = map.point2d_to_index(Point::new(0, y)); + for x in 0..MAP_WIDTH { + let idx = base_idx + x; + + let tile = map.tiles[idx]; + let color = match tile { + '#' => RGB::named(YELLOW), + _ => { + if flow_map.map[idx] < f32::MAX { + RGB::from_u8( + 0, + 255 - { + let n = flow_map.map[idx] * 6.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/bfs.rs b/bracket-pathfinding/src/bfs.rs new file mode 100644 index 00000000..ce5878fc --- /dev/null +++ b/bracket-pathfinding/src/bfs.rs @@ -0,0 +1,324 @@ +use bracket_algorithm_traits::prelude::BaseMap; +#[cfg(feature = "threaded")] +use rayon::prelude::*; +#[allow(unused_imports)] +use smallvec::SmallVec; +use std::collections::VecDeque; +use std::convert::TryInto; + +/// Representation of a breadth-first 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 BfsMap { + pub map: Vec, + size_x: usize, + size_y: usize, + max_depth: f32, +} + +/// Used internally when constructing maps in parallel +#[cfg(feature = "threaded")] +struct ParallelBfs { + map: Vec, + max_depth: f32, + starts: Vec, +} + +// This is chosen arbitrarily. Whether it's better to +// run threaded or not would depend on map structure, +// map size, number of starts, and probably several +// other parameters. Might want to make this choice +// an explicit part of the API? +#[allow(dead_code)] +const THREADED_REQUIRED_STARTS: usize = 4; + +#[derive(PartialEq)] +enum RunThreaded { + True, + False, +} + +#[allow(dead_code)] +impl BfsMap { + /// Construct a new BFS 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, + ) -> BfsMap + 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]; + let mut bfs = BfsMap { + map: result, + size_x: sz_x, + size_y: sz_y, + max_depth, + }; + BfsMap::build(&mut bfs, starts, map); + bfs + } + + /// Construct a new BFS 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. + /// Starts is provided as a set of tuples, two per tile. The first is the tile index, the second the starting + /// depth (defaults to 0.0 on new). + pub fn new_weighted( + size_x: T, + size_y: T, + starts: &[(usize, f32)], + map: &dyn BaseMap, + max_depth: f32, + ) -> BfsMap + 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]; + let mut bfs = BfsMap { + map: result, + size_x: sz_x, + size_y: sz_y, + max_depth, + }; + BfsMap::build_weighted(&mut bfs, starts, map); + bfs + } + + /// Creates an empty BFS map node. + pub fn new_empty(size_x: T, size_y: T, max_depth: f32) -> BfsMap + 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]; + BfsMap { + map: result, + size_x: sz_x, + size_y: sz_y, + max_depth, + } + } + + /// Clears the BFS map. Uses a parallel for each for performance. + #[cfg(feature = "threaded")] + pub fn clear(bfs: &mut BfsMap) { + bfs.map.par_iter_mut().for_each(|x| *x = f32::MAX); + } + + #[cfg(not(feature = "threaded"))] + pub fn clear(bfs: &mut BfsMap) { + bfs.map.iter_mut().for_each(|x| *x = f32::MAX); + } + + #[cfg(feature = "threaded")] + fn build_helper(bfs: &mut BfsMap, starts: &[usize], map: &dyn BaseMap) -> RunThreaded { + if starts.len() >= THREADED_REQUIRED_STARTS { + BfsMap::build_parallel(bfs, starts, map); + return RunThreaded::True; + } + RunThreaded::False + } + + #[cfg(not(feature = "threaded"))] + fn build_helper(_bfs: &mut BfsMap, _starts: &[usize], _map: &dyn BaseMap) -> RunThreaded { + RunThreaded::False + } + + /// Builds the BFS map: iterate from each starting point, to each exit provided by BaseMap's + /// exits implementation. Each step adds one to the current depth and ignores exit costs. + /// Automatically branches to a parallel version if you provide more than 4 starting points. + pub fn build(bfs: &mut BfsMap, starts: &[usize], map: &dyn BaseMap) { + let threaded = BfsMap::build_helper(bfs, starts, map); + if threaded == RunThreaded::True { + return; + } + + let weighted_starts: Vec<(usize, f32)> = starts.iter().map(|start| (*start, 0.0)).collect(); + BfsMap::build_weighted(bfs, &weighted_starts, map); + } + + /// Builds the BFS map: iterate from each starting point, to each exit provided by BaseMap's + /// exits implementation. Each step adds one to the current depth and ignores exit costs. + pub fn build_weighted(bfs: &mut BfsMap, starts: &[(usize, f32)], map: &dyn BaseMap) { + let mapsize: usize = bfs.size_x * bfs.size_y; + let mut open_list: VecDeque<(usize, f32)> = VecDeque::with_capacity(mapsize); + + for (start, depth) in starts.iter().copied() { + if depth >= bfs.map[start] || depth >= bfs.max_depth { + continue; + } + bfs.map[start] = depth; + open_list.push_back((start, depth)); + } + + while let Some((tile_idx, depth)) = open_list.pop_front() { + let exits = map.get_available_exits(tile_idx); + for (new_idx, _) in exits { + let new_depth = depth + 1.0; + let prev_depth = bfs.map[new_idx]; + if new_depth >= prev_depth { + continue; + } + if new_depth >= bfs.max_depth { + continue; + } + bfs.map[new_idx] = new_depth; + open_list.push_back((new_idx, new_depth)); + } + } + } + + /// Implementation of Parallel BFS. + #[cfg(feature = "threaded")] + fn build_parallel(bfs: &mut BfsMap, starts: &[usize], map: &dyn BaseMap) { + let mapsize: usize = bfs.size_x * bfs.size_y; + let mut layers: Vec = Vec::with_capacity(starts.len()); + for start_chunk in starts.chunks(rayon::current_num_threads()) { + let mut layer = ParallelBfs { + map: vec![f32::MAX; mapsize], + max_depth: bfs.max_depth, + starts: Vec::new(), + }; + layer.starts.extend(start_chunk.iter().copied()); + 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() { + if 0.0 >= l.map[start] || 0.0 >= l.max_depth { + continue; + } + l.map[start] = 0.0; + 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, _) in exits { + let new_idx = *new_idx; + let new_depth = depth + 1.0; + 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 { + bfs.map[i] = f32::min(bfs.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(bfs: &BfsMap, 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| bfs.map[a.0].partial_cmp(&bfs.map[b.0]).unwrap()); + + Some(exits[0].0) + } + + #[cfg(not(feature = "threaded"))] + pub fn find_lowest_exit(bfs: &BfsMap, 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| bfs.map[a.0].partial_cmp(&bfs.map[b.0]).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(bfs: &BfsMap, 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| bfs.map[b.0].partial_cmp(&bfs.map[a.0]).unwrap()); + + Some(exits[0].0) + } + + #[cfg(not(feature = "threaded"))] + pub fn find_highest_exit(bfs: &BfsMap, 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| bfs.map[b.0].partial_cmp(&bfs.map[a.0]).unwrap()); + + Some(exits[0].0) + } +} + +#[cfg(test)] +mod test { + use crate::prelude::*; + use bracket_algorithm_traits::prelude::*; + + struct WeightedShortcutMap; + + impl BaseMap for WeightedShortcutMap { + fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> { + match idx { + 0 => smallvec![(1, 10.0), (2, 1.0)], + 2 => smallvec![(1, 1.0)], + _ => smallvec![], + } + } + } + + #[test] + fn bfs_counts_edges_not_costs() { + let map = WeightedShortcutMap; + let bfs = BfsMap::new(3, 1, &[0], &map, 10.0); + + assert_eq!(bfs.map[0], 0.0); + assert_eq!(bfs.map[1], 1.0); + assert_eq!(bfs.map[2], 1.0); + } +} diff --git a/bracket-pathfinding/src/dijkstra.rs b/bracket-pathfinding/src/dijkstra.rs index 6a2c41f0..4bc0fa37 100755 --- a/bracket-pathfinding/src/dijkstra.rs +++ b/bracket-pathfinding/src/dijkstra.rs @@ -3,7 +3,8 @@ use bracket_algorithm_traits::prelude::BaseMap; use rayon::prelude::*; #[allow(unused_imports)] use smallvec::SmallVec; -use std::collections::VecDeque; +use std::cmp::Ordering; +use std::collections::BinaryHeap; use std::convert::TryInto; /// Representation of a Dijkstra flow map. @@ -25,6 +26,36 @@ struct ParallelDm { starts: Vec, } +#[derive(Copy, Clone, Debug)] +struct DijkstraNode { + idx: usize, + cost: f32, +} + +impl PartialEq for DijkstraNode { + fn eq(&self, other: &Self) -> bool { + self.idx == other.idx && self.cost == other.cost + } +} + +impl Eq for DijkstraNode {} + +impl Ord for DijkstraNode { + fn cmp(&self, other: &Self) -> Ordering { + other + .cost + .partial_cmp(&self.cost) + .unwrap() + .then_with(|| self.idx.cmp(&other.idx)) + } +} + +impl PartialOrd for DijkstraNode { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + // This is chosen arbitrarily. Whether it's better to // run threaded or not would depend on map structure, // map size, number of starts, and probably several @@ -137,58 +168,40 @@ impl DijkstraMap { /// 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; - let mut open_list: VecDeque<(usize, f32)> = VecDeque::with_capacity(mapsize); - - for start in starts { - dm.map[*start] = f32::min(dm.map[*start], 0.0); - 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)); - } - } + let weighted_starts: Vec<(usize, f32)> = starts.iter().map(|start| (*start, 0.0)).collect(); + DijkstraMap::build_weighted(dm, &weighted_starts, map); } /// 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_weighted(dm: &mut DijkstraMap, starts: &[(usize, f32)], map: &dyn BaseMap) { let mapsize: usize = dm.size_x * dm.size_y; - let mut open_list: VecDeque<(usize, f32)> = VecDeque::with_capacity(mapsize); + let mut open_list: BinaryHeap = BinaryHeap::with_capacity(mapsize); - for start in starts { - dm.map[start.0] = f32::min(dm.map[start.0], start.1); - open_list.push_back(*start); + for (start, cost) in starts.iter().copied() { + if cost >= dm.map[start] || cost >= dm.max_depth { + continue; + } + dm.map[start] = cost; + open_list.push(DijkstraNode { idx: start, cost }); } - while let Some((tile_idx, depth)) = open_list.pop_front() { - let exits = map.get_available_exits(tile_idx); + while let Some(node) = open_list.pop() { + if node.cost > dm.map[node.idx] { + continue; + } + + let exits = map.get_available_exits(node.idx); for (new_idx, add_depth) in exits { - let new_depth = depth + add_depth; + let new_depth = node.cost + add_depth; let prev_depth = dm.map[new_idx]; if new_depth >= prev_depth { continue; @@ -197,7 +210,10 @@ impl DijkstraMap { continue; } dm.map[new_idx] = new_depth; - open_list.push_back((new_idx, new_depth)); + open_list.push(DijkstraNode { + idx: new_idx, + cost: new_depth, + }); } } } @@ -205,7 +221,7 @@ impl DijkstraMap { /// 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 mapsize: usize = dm.size_x * dm.size_y; let mut layers: Vec = Vec::with_capacity(starts.len()); for start_chunk in starts.chunks(rayon::current_num_threads()) { let mut layer = ParallelDm { @@ -213,9 +229,7 @@ impl DijkstraMap { max_depth: dm.max_depth, starts: Vec::new(), }; - layer - .starts - .extend(start_chunk.iter().copied().map(|x| x as usize)); + layer.starts.extend(start_chunk.iter().copied()); layers.push(layer); } @@ -225,18 +239,28 @@ impl DijkstraMap { // Run each map in parallel layers.par_iter_mut().for_each(|l| { - let mut open_list: VecDeque<(usize, f32)> = VecDeque::with_capacity(mapsize); + let mut open_list: BinaryHeap = BinaryHeap::with_capacity(mapsize); for start in l.starts.iter().copied() { - l.map[start] = f32::min(l.map[start], 0.0); - open_list.push_back((start, 0.0)); + if 0.0 >= l.map[start] || 0.0 >= l.max_depth { + continue; + } + l.map[start] = 0.0; + open_list.push(DijkstraNode { + idx: start, + cost: 0.0, + }); } - while let Some((tile_idx, depth)) = open_list.pop_front() { - let exits = &exits[tile_idx]; + while let Some(node) = open_list.pop() { + if node.cost > l.map[node.idx] { + continue; + } + + let exits = &exits[node.idx]; for (new_idx, add_depth) in exits { let new_idx = *new_idx; - let new_depth = depth + add_depth; + let new_depth = node.cost + add_depth; let prev_depth = l.map[new_idx]; if new_depth >= prev_depth { continue; @@ -245,7 +269,10 @@ impl DijkstraMap { continue; } l.map[new_idx] = new_depth; - open_list.push_back((new_idx, new_depth)); + open_list.push(DijkstraNode { + idx: new_idx, + cost: new_depth, + }); } } }); @@ -352,6 +379,18 @@ mod test { } } } + + struct WeightedShortcutMap; + impl BaseMap for WeightedShortcutMap { + fn get_available_exits(&self, idx: usize) -> SmallVec<[(usize, f32); 10]> { + match idx { + 0 => smallvec![(1, 10.0), (2, 1.0)], + 2 => smallvec![(1, 1.0)], + _ => smallvec![], + } + } + } + #[test] fn test_new() { let map = MiniMap {}; @@ -415,4 +454,14 @@ mod test { let target = DijkstraMap::find_highest_exit(&exits_map, 1, &map); assert_eq!(target, Some(2)); } + + #[test] + fn dijkstra_follows_lowest_total_cost() { + let map = WeightedShortcutMap; + let dijkstra = DijkstraMap::new(3, 1, &[0], &map, 10.0); + + assert_eq!(dijkstra.map[0], 0.0); + assert_eq!(dijkstra.map[1], 2.0); + assert_eq!(dijkstra.map[2], 1.0); + } } diff --git a/bracket-pathfinding/src/lib.rs b/bracket-pathfinding/src/lib.rs index a1956b38..11d4cc2c 100755 --- a/bracket-pathfinding/src/lib.rs +++ b/bracket-pathfinding/src/lib.rs @@ -1,11 +1,13 @@ #![allow(clippy::multiple_crate_versions)] mod astar; +mod bfs; mod dijkstra; mod field_of_view; pub mod prelude { pub use crate::astar::*; + pub use crate::bfs::*; pub use crate::dijkstra::*; pub use crate::field_of_view::*; pub use bracket_algorithm_traits::prelude::*; diff --git a/manual/src/ex_path.md b/manual/src/ex_path.md index 22fa62d2..47cdb6d8 100644 --- a/manual/src/ex_path.md +++ b/manual/src/ex_path.md @@ -16,6 +16,12 @@ Demonstrates using Manhattan distances in A-Star, rather than regular Pythagoras ![](./ex_path_astar_manhattan.jpg) +### bfs + +[Source Code](https://github.com/amethyst/bracket-lib/tree/master/bracket-pathfinding/examples/bfs) + +Demonstrates the usage of BFS maps for unweighted flow mapping to multiple starting points. + ### dijkstra [Source Code](https://github.com/amethyst/bracket-lib/tree/master/bracket-pathfinding/examples/dijkstra) @@ -30,4 +36,4 @@ Demonstrates the usage of the Dijkstra maps feature by brightening areas close t Demonstrates the Field-of-View functionality. -![](./ex_path_fov.jpg) \ No newline at end of file +![](./ex_path_fov.jpg) diff --git a/manual/src/individual_parts.md b/manual/src/individual_parts.md index a1048b9d..53b1a6db 100644 --- a/manual/src/individual_parts.md +++ b/manual/src/individual_parts.md @@ -6,7 +6,7 @@ * `bracket-color` defines how the library handles color, and includes functions for grayscale, RGB/RGBA/HSV conversion, a lot of named colors, and general color management support. * `bracket-geometry` provides points, lines, rectangles and circle support. * `bracket-noise` provides Perlin, Simplex, White and other noise functions useful for randomly generating things. -* `bracket-pathfinding` provides an A-Star and a Dijkstra mapping solution. +* `bracket-pathfinding` provides A-Star, BFS and Dijkstra mapping solutions. * `bracket-random` provides an easy-to-use wrapper to a random number generator. * `bracket-terminal` provides console rendering and support. diff --git a/src/lib.rs b/src/lib.rs index 1aff16da..0d487b02 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -37,7 +37,7 @@ pub mod noise { pub use bracket_noise::prelude::*; } /// bracket-pathfinding (in conjunction with bracket-algorithm-traits) provides -/// pathfinding functionality. A-Star (A*) and Dijkstra are supported. It also +/// pathfinding functionality. A-Star (A*), BFS and Dijkstra are supported. It also // provides field of view (FOV) functionality. pub mod pathfinding { pub use bracket_pathfinding::prelude::*;