Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
16 changes: 15 additions & 1 deletion bracket-pathfinding/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
114 changes: 114 additions & 0 deletions bracket-pathfinding/examples/floyd_warshall/common.rs
Original file line number Diff line number Diff line change
@@ -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<char>,
}

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<usize> {
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)
}
}
42 changes: 42 additions & 0 deletions bracket-pathfinding/examples/floyd_warshall/main.rs
Original file line number Diff line number Diff line change
@@ -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();
}
172 changes: 172 additions & 0 deletions bracket-pathfinding/src/floyd_warshall.rs
Original file line number Diff line number Diff line change
@@ -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<f32>,
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<T>(size_x: T, size_y: T, map: &dyn BaseMap, max_depth: f32) -> FloydWarshallMap
where
T: TryInto<usize>,
{
let sz_x: usize = size_x.try_into().ok().unwrap();
let sz_y: usize = size_y.try_into().ok().unwrap();
Comment on lines +21 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

.ok().unwrap() discards the conversion error and panics opaquely.

If try_into fails, the resulting panic has no context. Prefer .expect(...) (or propagate) for a clearer failure message.

🛠️ Suggested change
-        let sz_x: usize = size_x.try_into().ok().unwrap();
-        let sz_y: usize = size_y.try_into().ok().unwrap();
+        let sz_x: usize = size_x.try_into().ok().expect("size_x must fit in usize");
+        let sz_y: usize = size_y.try_into().ok().expect("size_y must fit in usize");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let sz_x: usize = size_x.try_into().ok().unwrap();
let sz_y: usize = size_y.try_into().ok().unwrap();
let sz_x: usize = size_x.try_into().ok().expect("size_x must fit in usize");
let sz_y: usize = size_y.try_into().ok().expect("size_y must fit in usize");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bracket-pathfinding/src/floyd_warshall.rs` around lines 21 - 22, The current
conversion uses size_x.try_into().ok().unwrap() and
size_y.try_into().ok().unwrap(), which hides conversion errors and produces an
opaque panic; replace these with explicit error handling such as
size_x.try_into().expect("failed to convert size_x to usize") and
size_y.try_into().expect("failed to convert size_y to usize") or propagate the
Result from the function so failures carry context—update the bindings sz_x and
sz_y accordingly to use expect (or ? propagation) instead of .ok().unwrap().

let result: Vec<f32> = 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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

/// 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);
}
}
}
}
Comment on lines +41 to +69
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset depth_map before rebuilding.

build is public, but it never clears the previous matrix. Rebuilding after exits are removed or weights change will keep stale shorter paths and return wrong distances.

🛠️ Proposed fix
 pub fn build(fm: &mut FloydWarshallMap, map: &dyn BaseMap) {
     let mapsize: usize = fm.size_x * fm.size_y;
+    fm.depth_map.fill(f32::MAX);
 
     for start_idx in 0..mapsize {
         let idx = fm.idx_helper(start_idx, start_idx);
         fm.depth_map[idx] = 0.;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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);
}
}
}
}
pub fn build(fm: &mut FloydWarshallMap, map: &dyn BaseMap) {
let mapsize: usize = fm.size_x * fm.size_y;
fm.depth_map.fill(f32::MAX);
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);
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bracket-pathfinding/src/floyd_warshall.rs` around lines 41 - 69, The build
function must reset fm.depth_map before recomputing paths to avoid stale values;
in FloydWarshallMap::build, initialize fm.depth_map to the default "infinite"
distance for all pairs (e.g., fill with f32::INFINITY or a large sentinel) sized
to mapsize*mapsize (or call a reset/clear helper on fm.depth_map), then set the
diagonal entries to 0 (using idx_helper(start_idx, start_idx)) and populate
direct exits from map.get_available_exits as you already do, before running the
triple-loop that updates entries; this ensures removed edges or changed weights
don't leave old shorter distances in depth_map.


/// 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<usize> {
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)
Comment on lines +74 to +96
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

These exit helpers ignore the destination node.

Both helpers rank candidates with depth_map[position, exit], which is only the cost from the current tile to that neighbor. That picks the cheapest/most expensive immediate edge, not the step that moves toward or away from a chosen node as the docs claim. The API needs a target/source index and should compare each candidate exit against that node instead.

🛠️ Direction of fix
 pub fn find_lowest_exit(
     fm: &FloydWarshallMap,
     position: usize,
+    target: usize,
     map: &dyn BaseMap,
 ) -> Option<usize> {
     ...
     for (exit, _) in exits {
-        let pos = fm.idx_helper(position, exit);
+        let pos = fm.idx_helper(exit, target);
         ...
     }
 }

Apply the same signature/lookup change to find_highest_exit, and add a regression test with asymmetric edge weights so the helper must choose based on the destination rather than the local edge cost.

Also applies to: 103-125

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bracket-pathfinding/src/floyd_warshall.rs` around lines 74 - 96, The helpers
currently pick exits by local edge cost; update both find_lowest_exit and
find_highest_exit to accept an additional target/source index parameter (e.g.,
dest: usize) and use fm.idx_helper(exit, dest) to look up the distance in
fm.depth_map when ranking candidates instead of fm.idx_helper(position, exit).
Specifically, change signatures of find_lowest_exit and find_highest_exit to
include the destination/source, iterate exits from
map.get_available_exits(position), compute pos = fm.idx_helper(exit, dest) and
compare fm.depth_map[pos] to choose the min/max exit, and add a regression test
with asymmetric edge weights exercising both helpers so the choice depends on
distance-to-target rather than the immediate edge cost.

}

/// 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<usize> {
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));
}
}
2 changes: 2 additions & 0 deletions bracket-pathfinding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::*;

Expand Down
Loading