├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── DejaVuSans.ttf ├── bin ├── astar.rs ├── bfs.rs └── dijkstra.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | 13 | # Added by cargo 14 | /target 15 | 16 | # testing images 17 | /*.png 18 | 19 | *.swp 20 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-pathfinding" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | pathfinding = "3.0.12" 10 | image = "0.24.1" 11 | imageproc = "0.23.0" 12 | rusttype = "0.9.2" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Greg Stoll 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-pathfinding 2 | Examples of pathfinding in Rust 3 | 4 | This repo is the example code for the article [Pathfinding in Rust: A tutorial with examples](https://blog.logrocket.com/pathfinding-rust-tutorial-examples/) on the [LogRocket blog](https://blog.logrocket.com/). It shows examples of doing breadth-first search, Dijkstra's algorithm, and A* search. 5 | 6 | To run these, use: 7 | - `cargo run --bin bfs` to run the breadth-first search example - this will output bfs.png in the root directory. 8 | - `cargo run --bin dijkstra` to run the Dijkstra's algorithm example - this will output dijkstra.png in the root directory. 9 | - `cargo run --bin astar` to run the A* search example - this will output astar.png in the root directory. 10 | 11 | The code uses the [pathfinding](https://crates.io/crates/pathfinding) crate to do the searches. The `Board` struct uses the [imageproc](https://crates.io/crates/imageproc) crate to draw the board and paths. 12 | 13 | The `Board` struct is defined in [lib.rs](https://github.com/gregstoll/rust-pathfinding/blob/main/src/lib.rs), and there are some unit tests in that file as well. 14 | -------------------------------------------------------------------------------- /src/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gregstoll/rust-pathfinding/162acd89f49c116bf9ffdebe59d0b447d58b1060/src/DejaVuSans.ttf -------------------------------------------------------------------------------- /src/bin/astar.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use pathfinding::prelude::astar; 4 | use rust_pathfinding::{Board, Pos}; 5 | 6 | fn main() { 7 | let board = Board::new(vec![ 8 | "21397X2", 9 | "1X19452", 10 | "62251X1", 11 | "1612179", 12 | "1348512", 13 | "61453X1", 14 | "7861243"], false); 15 | let start = Pos(0,1); 16 | let goal = Pos(6,2); 17 | let result = astar( 18 | &start, 19 | |p| board.get_successors(p).iter().map(|s| (s.pos, s.cost)).collect::>(), 20 | |p| ((p.0 - goal.0).abs() + (p.1 - goal.1).abs()) as u32, 21 | |p| *p==goal); 22 | let result = result.expect("No path found"); 23 | println!("total cost: {:}", result.1); 24 | board.draw_to_image(Path::new("astar.png"), Some(&result.0)); 25 | } -------------------------------------------------------------------------------- /src/bin/bfs.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use pathfinding::prelude::bfs; 4 | use rust_pathfinding::{Board, Pos}; 5 | 6 | fn main() { 7 | let board = Board::new(vec![ 8 | "1111X", 9 | "1X1X1", 10 | "1X111", 11 | "1X1X1", 12 | "11111"], false); 13 | let start = Pos(0,1); 14 | let goal = Pos(4,1); 15 | let result = bfs( 16 | &start, 17 | |p| board.get_successors(p).iter().map(|successor| successor.pos).collect::>(), 18 | |p| *p==goal); 19 | let result = result.expect("No path found"); 20 | board.draw_to_image(Path::new("bfs.png"), Some(&result)); 21 | } 22 | -------------------------------------------------------------------------------- /src/bin/dijkstra.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use pathfinding::prelude::dijkstra; 4 | use rust_pathfinding::{Board, Pos}; 5 | 6 | fn main() { 7 | let board = Board::new(vec![ 8 | "89341", 9 | "1X534", 10 | "62891", 11 | "17214", 12 | "13285"], false); 13 | let start = Pos(0,1); 14 | let goal = Pos(4,2); 15 | let result = dijkstra( 16 | &start, 17 | |p| board.get_successors(p).iter().map(|s| (s.pos, s.cost)).collect::>(), 18 | |p| *p==goal); 19 | let result = result.expect("No path found"); 20 | println!("total cost: {:}", result.1); 21 | board.draw_to_image(Path::new("dijkstra.png"), Some(&result.0)); 22 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | use image::{Rgb, RgbImage}; 3 | use imageproc::drawing::{draw_line_segment_mut, draw_text_mut, draw_filled_rect_mut, draw_polygon_mut}; 4 | use imageproc::point::Point; 5 | use imageproc::rect::Rect; 6 | use rusttype::{Font, Scale}; 7 | 8 | #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 9 | pub struct Pos(pub i16, pub i16); 10 | 11 | pub struct Board { 12 | pub width: u8, 13 | pub height: u8, 14 | pub data: Vec>>, 15 | pub allow_diagonal: bool 16 | } 17 | 18 | impl Board { 19 | pub fn new(board_lines: Vec<&str>, allow_diagonal: bool) -> Board { 20 | let width = board_lines[0].len() as u8; 21 | let height = board_lines.len() as u8; 22 | let mut data = Vec::new(); 23 | for board_line in board_lines { 24 | let mut row: Vec> = Vec::new(); 25 | for c in board_line.chars() { 26 | match c { 27 | 'X' => row.push(None), 28 | '1'..='9' => row.push(Some(c as u8 - b'0')), 29 | _ => panic!("invalid character") 30 | } 31 | } 32 | data.push(row); 33 | } 34 | Board {width, height, data, allow_diagonal} 35 | } 36 | 37 | pub fn get_successors(&self, position: &Pos) -> Vec { 38 | let mut successors = Vec::new(); 39 | for dx in -1i16..=1 { 40 | for dy in -1i16..=1 { 41 | if self.allow_diagonal { 42 | if dx == 0 && dy == 0 { 43 | continue; 44 | } 45 | } 46 | else { 47 | // Omit diagonal moves (and moving to the same position) 48 | if (dx + dy).abs() != 1 { 49 | continue; 50 | } 51 | } 52 | let new_position = Pos(position.0 + dx, position.1 + dy); 53 | if new_position.0 < 0 || new_position.0 >= self.width.into() || new_position.1 < 0 || new_position.1 >= self.height.into() { 54 | continue; 55 | } 56 | let board_value = self.data[new_position.1 as usize][new_position.0 as usize]; 57 | if let Some(board_value) = board_value { 58 | successors.push(Successor { pos: new_position, cost: board_value as u32}); 59 | } 60 | } 61 | } 62 | 63 | successors 64 | } 65 | 66 | pub fn draw_to_image(&self, file_path: &Path, pos_path: Option<&Vec>) { 67 | const CELL_WIDTH: u32 = 100; 68 | const CELL_HEIGHT: u32 = 100; 69 | const CELL_SHADING: Option = Some(10); 70 | let mut image = RgbImage::new(self.width as u32 * CELL_WIDTH, self.height as u32 * CELL_HEIGHT); 71 | image.fill(255u8); 72 | const BLACK: Rgb = Rgb([0u8, 0u8, 0u8]); 73 | const DODGER_BLUE: Rgb = Rgb([30u8, 144u8, 255u8]); 74 | const LIME_GREEN: Rgb = Rgb([50u8, 205u8, 50u8]); 75 | const LIGHT_GRAY: Rgb = Rgb([150u8, 150u8, 150u8]); 76 | 77 | // draw inner border lines 78 | for i in 1u8..self.width { 79 | draw_line_segment_mut(&mut image, (i as f32 * CELL_WIDTH as f32, 0.0), (i as f32 * CELL_WIDTH as f32, self.height as f32 * CELL_HEIGHT as f32), BLACK); 80 | } 81 | for i in 1u8..self.height { 82 | draw_line_segment_mut(&mut image, (0.0, i as f32 * CELL_HEIGHT as f32), (self.width as f32 * CELL_WIDTH as f32, i as f32 * CELL_HEIGHT as f32), BLACK); 83 | } 84 | 85 | let font = Vec::from(include_bytes!("DejaVuSans.ttf") as &[u8]); 86 | let font = Font::try_from_vec(font).unwrap(); 87 | let height = 48.0; 88 | let scale = Scale { 89 | x: height * 2.0, 90 | y: height, 91 | }; 92 | let no_costs = self.data.iter().all(|row| row.iter().all(|cell| cell.is_none() || cell.unwrap() == 1)); 93 | let start_pos = pos_path.and_then(|v| v.first()); 94 | let end_pos = pos_path.and_then(|v| v.last()); 95 | fn get_cell_background_color(board_value: u8) -> Option> { 96 | CELL_SHADING.map(|shading| { 97 | Rgb([255u8, 255u8 - (board_value - 1) * shading, 255u8 - (board_value - 1) * shading]) 98 | }) 99 | } 100 | // draw the numbers/walls (with start and end positions) 101 | for y in 0..self.height { 102 | for x in 0..self.width { 103 | let board_value = self.data[y as usize][x as usize]; 104 | let cur_pos = Pos(x as i16, y as i16); 105 | let mut cur_color: &Rgb = &BLACK; 106 | // This would be a nice place to use is_some_and(), but it's still unstable 107 | // https://github.com/rust-lang/rust/issues/93050 108 | if let Some(start_pos_real) = start_pos { 109 | if start_pos_real == &cur_pos { 110 | cur_color = &DODGER_BLUE; 111 | } 112 | } 113 | if let Some(end_pos_real) = end_pos { 114 | if end_pos_real == &cur_pos { 115 | cur_color = &LIME_GREEN; 116 | } 117 | } 118 | match board_value { 119 | Some(board_value) => { 120 | if !no_costs { 121 | if let Some(cell_background_color) = get_cell_background_color(board_value) { 122 | // cells on the left/top border need an extra pixel at the left/top 123 | let start_x = if x == 0 { 0 } else {x as i32 * CELL_WIDTH as i32 + 1}; 124 | let x_size = if x == 0 { CELL_WIDTH } else { CELL_WIDTH - 1}; 125 | let start_y = if y == 0 { 0 } else {y as i32 * CELL_HEIGHT as i32 + 1}; 126 | let y_size = if y == 0 { CELL_HEIGHT } else { CELL_HEIGHT - 1}; 127 | draw_filled_rect_mut(&mut image, 128 | Rect::at(start_x, start_y).of_size(x_size, y_size), 129 | cell_background_color); 130 | } 131 | draw_text_mut(&mut image, 132 | *cur_color, 133 | x as i32 * CELL_WIDTH as i32 + 26, 134 | y as i32 * CELL_HEIGHT as i32 + 26, 135 | scale, 136 | &font, 137 | &format!("{}", board_value)); 138 | } 139 | else { 140 | // draw a rectangle for the start and end positions 141 | if cur_color != &BLACK { 142 | draw_filled_rect_mut(&mut image, 143 | Rect::at(x as i32 * CELL_WIDTH as i32 + 30, y as i32 * CELL_HEIGHT as i32 + 30).of_size(CELL_WIDTH - 30 * 2, CELL_HEIGHT - 30 * 2), 144 | *cur_color); 145 | } 146 | } 147 | } 148 | None => { 149 | draw_filled_rect_mut(&mut image, Rect::at(x as i32 * CELL_WIDTH as i32, y as i32 * CELL_HEIGHT as i32).of_size(CELL_WIDTH, CELL_HEIGHT), *cur_color); 150 | } 151 | } 152 | } 153 | } 154 | 155 | fn get_line_endpoint(start: &Pos, end: &Pos) -> (f32, f32) { 156 | let x_delta = 20.0 * match end.0.cmp(&start.0) { 157 | std::cmp::Ordering::Equal => 0, 158 | std::cmp::Ordering::Less => -1, 159 | std::cmp::Ordering::Greater => 1 160 | } as f32; 161 | let y_delta = 20.0 * match end.1.cmp(&start.1) { 162 | std::cmp::Ordering::Equal => 0, 163 | std::cmp::Ordering::Less => -1, 164 | std::cmp::Ordering::Greater => 1 165 | } as f32; 166 | 167 | ((start.0 as f32 + 0.5) * CELL_WIDTH as f32 + x_delta, (start.1 as f32 + 0.5) * CELL_HEIGHT as f32 + y_delta) 168 | } 169 | fn get_points_for_rectangle_around_line(start: &(f32, f32), end: &(f32, f32), width: f32, space_for_arrow: f32) -> Vec> { 170 | let (x1, y1) = start; 171 | let (x2, y2) = end; 172 | let x_delta = x2 - x1; 173 | let y_delta = y2 - y1; 174 | let x_delta_norm = x_delta / x_delta.hypot(y_delta); 175 | let y_delta_norm = y_delta / x_delta.hypot(y_delta); 176 | 177 | vec![ 178 | Point::new((x1 - y_delta_norm * (width / 2.0)) as i32, (y1 + x_delta_norm * (width / 2.0)) as i32), 179 | Point::new((x1 + y_delta_norm * (width / 2.0)) as i32, (y1 - x_delta_norm * (width / 2.0)) as i32), 180 | Point::new((x2 + y_delta_norm * (width / 2.0) - x_delta_norm * space_for_arrow) as i32, (y2 - x_delta_norm * (width / 2.0) - y_delta_norm * space_for_arrow) as i32), 181 | Point::new((x2 - y_delta_norm * (width / 2.0) - x_delta_norm * space_for_arrow) as i32, (y2 + x_delta_norm * (width / 2.0) - y_delta_norm * space_for_arrow) as i32), 182 | ] 183 | } 184 | fn get_points_for_arrowhead(start: &(f32, f32), end: &(f32, f32), width: f32, length: f32) -> Vec> { 185 | // 186 | // start 187 | // *** 188 | // * * 189 | // * * 190 | // ******* <- midpoint of this line is arrow_middle 191 | // *** 192 | // end 193 | 194 | let (x1, y1) = start; 195 | let (x2, y2) = end; 196 | let x_delta = x2 - x1; 197 | let y_delta = y2 - y1; 198 | let x_delta_norm = x_delta / x_delta.hypot(y_delta); 199 | let y_delta_norm = y_delta / x_delta.hypot(y_delta); 200 | let arrow_middle_x = x2 - x_delta_norm * length; 201 | let arrow_middle_y = y2 - y_delta_norm * length; 202 | 203 | vec![ 204 | Point::new(*x2 as i32, *y2 as i32), 205 | Point::new((arrow_middle_x - y_delta_norm * width) as i32, (arrow_middle_y + x_delta_norm * width) as i32), 206 | Point::new((arrow_middle_x + y_delta_norm * width) as i32, (arrow_middle_y - x_delta_norm * width) as i32), 207 | ] 208 | } 209 | // Draw the path 210 | if let Some(pos_path) = pos_path { 211 | pos_path.windows(2).for_each(|pair| { 212 | let start_pos = &pair[0]; 213 | let end_pos = &pair[1]; 214 | let start_line_endpoint = get_line_endpoint(start_pos, end_pos); 215 | let end_line_endpoint = get_line_endpoint(end_pos, start_pos); 216 | draw_polygon_mut(&mut image, &get_points_for_rectangle_around_line(&start_line_endpoint, &end_line_endpoint, 10.0, 24.0), LIGHT_GRAY); 217 | draw_polygon_mut(&mut image, &get_points_for_arrowhead(&start_line_endpoint, &end_line_endpoint, 14.0, 24.0), LIGHT_GRAY); 218 | }); 219 | } 220 | 221 | image.save(file_path).unwrap(); 222 | } 223 | } 224 | 225 | #[derive(Clone, Copy, Debug, Eq, PartialEq, PartialOrd)] 226 | pub struct Successor { 227 | pub pos: Pos, 228 | pub cost: u32, 229 | } 230 | // Used to make writing tests easier 231 | impl PartialEq<(Pos, u32)> for Successor { 232 | fn eq(&self, other: &(Pos, u32)) -> bool { 233 | self.pos == other.0 && self.cost == other.1 234 | } 235 | } 236 | 237 | #[cfg(test)] 238 | mod tests { 239 | use super::*; 240 | 241 | #[test] 242 | fn test_onebyoneboard_nosuccessors() { 243 | let board = Board::new(vec!["1"], false); 244 | let result = board.get_successors(&Pos(0, 0)); 245 | assert_eq!(result.len(), 0); 246 | } 247 | 248 | #[test] 249 | fn test_twobytwoboardwithobstacle() { 250 | let board = Board::new(vec![ 251 | "21", 252 | "1X"], false); 253 | let result = board.get_successors(&Pos(0, 1)); 254 | assert_eq!(result, vec![(Pos(0, 0), 2)]); 255 | } 256 | 257 | #[test] 258 | fn test_twobytwoboardwithobstacleanddiagonal() { 259 | let board = Board::new(vec![ 260 | "21", 261 | "1X"], true); 262 | let result = board.get_successors(&Pos(0, 1)); 263 | assert_eq!(result, vec![(Pos(0, 0), 2), (Pos(1, 0), 1)]); 264 | } 265 | 266 | #[test] 267 | fn test_bigboardmovingfrommiddle() { 268 | let board = Board::new(vec![ 269 | "21941", 270 | "1X587", 271 | "238X1", 272 | "18285", 273 | "13485"], false); 274 | let result = board.get_successors(&Pos(2, 2)); 275 | assert_eq!(result, vec![(Pos(1, 2), 3), (Pos(2, 1), 5), (Pos(2, 3), 2)]); 276 | } 277 | 278 | #[test] 279 | fn test_surroundedbywalls() { 280 | let board = Board::new(vec![ 281 | "21941", 282 | "1XX87", 283 | "2X8X1", 284 | "18X85", 285 | "13485"], false); 286 | let result = board.get_successors(&Pos(2, 2)); 287 | assert_eq!(result.len(), 0); 288 | } 289 | } --------------------------------------------------------------------------------