From bbf69dc89f555e2512faf72fa59509ad527d72ba Mon Sep 17 00:00:00 2001 From: okhai <57156589+okhaimie-dev@users.noreply.github.com> Date: Tue, 8 Oct 2024 04:27:11 -0500 Subject: [PATCH] feat: breadth-first search algorithm (#102) Co-authored-by: bal7hazar --- crates/map/src/helpers/bfs.cairo | 198 +++++++++++++++++++++++++++++++ crates/map/src/lib.cairo | 2 +- 2 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 crates/map/src/helpers/bfs.cairo diff --git a/crates/map/src/helpers/bfs.cairo b/crates/map/src/helpers/bfs.cairo new file mode 100644 index 0000000..df2ec89 --- /dev/null +++ b/crates/map/src/helpers/bfs.cairo @@ -0,0 +1,198 @@ +//! Breadth-First Search algorithm implementation for pathfinding. + +// Core imports +use core::dict::{Felt252Dict, Felt252DictTrait}; + +// Internal imports +use origami_map::helpers::astar::Astar; +use origami_map::helpers::bitmap::Bitmap; +use origami_map::helpers::seeder::Seeder; +use origami_map::types::node::{Node, NodeTrait}; +use origami_map::types::direction::{Direction, DirectionTrait}; + + +/// BFS implementation for pathfinding +#[generate_trait] +pub impl BFS of BFSTrait { + /// Searches for a path from 'from' to 'to' on the given grid using BFS + /// + /// # Arguments + /// * `grid` - The grid represented as a felt252 + /// * `width` - The width of the grid + /// * `height` - The height of the grid + /// * `from` - The starting position + /// * `to` - The target position + /// + /// # Returns + /// A Span representing the path from 'from' to 'to', or an empty span if no path exists + #[inline] + fn search(grid: felt252, width: u8, height: u8, from: u8, to: u8) -> Span { + // [Check] The start and target are walkable + if Bitmap::get(grid, from) == 0 || Bitmap::get(grid, to) == 0 { + return array![].span(); + } + + // [Effect] Initialize the start and target nodes + let mut start = NodeTrait::new(from, 0, 0, 0); + let target = NodeTrait::new(to, 0, 0, 0); + + // [Effect] Initialize the queue and the visited nodes + let mut queue: Array = array![start]; + let mut visited: Felt252Dict = Default::default(); + let mut parents: Felt252Dict = Default::default(); + visited.insert(start.position.into(), true); + + // [Compute] BFS until the target is reached or queue is empty + let mut path_found = false; + while let Option::Some(current) = queue.pop_front() { + // [Check] Stop if we reached the target + if current.position == target.position { + path_found = true; + break; + } + // [Compute] Evaluate the neighbors for all 4 directions + let seed = Seeder::shuffle(grid, current.position.into()); + let mut directions = DirectionTrait::compute_shuffled_directions(seed); + while directions != 0 { + let direction = DirectionTrait::pop_front(ref directions); + if Astar::check(grid, width, height, current.position, direction, ref visited) { + let neighbor_position = direction.next(current.position, width); + parents.insert(neighbor_position.into(), current.position); + let neighbor = NodeTrait::new(neighbor_position, current.position, 0, 0); + queue.append(neighbor); + visited.insert(neighbor_position.into(), true); + } + }; + }; + + // Reconstruct and return the path if found + if !path_found { + return array![].span(); + }; + Self::path(parents, start, target) + } + + /// Reconstructs the path from start to target using the parents dictionary + #[inline] + fn path(mut parents: Felt252Dict, start: Node, target: Node) -> Span { + let mut path: Array = array![]; + let mut current = target.position; + + loop { + if current == start.position { + break; + } + path.append(current); + current = parents.get(current.into()); + }; + + path.span() + } +} + +#[cfg(test)] +mod test { + // Local imports + use super::BFS; + + #[test] + fn test_bfs_search_small() { + // x───┐ + // 1 0 │ + // 0 1 s + let grid: felt252 = 0x1EB; + let width = 3; + let height = 3; + let from = 0; + let to = 8; + let path = BFS::search(grid, width, height, from, to); + assert_eq!(path, array![8, 7, 6, 3].span()); + } + + #[test] + fn test_bfs_search_impossible() { + // x 1 0 + // 1 0 1 + // 0 1 s + let grid: felt252 = 0x1AB; + let width = 3; + let height = 3; + let from = 0; + let to = 8; + let path = BFS::search(grid, width, height, from, to); + assert_eq!(path, array![].span()); + } + + #[test] + fn test_bfs_search_medium() { + // ┌─x 0 0 + // │ 0 1 1 + // └─────┐ + // 1 1 1 s + let grid: felt252 = 0xCBFF; + let width = 4; + let height = 4; + let from = 0; + let to = 14; + let path = BFS::search(grid, width, height, from, to); + assert_eq!(path, array![14, 15, 11, 7, 6, 5, 4].span()); + } + + #[test] + fn test_bfs_single_cell_path() { + // Grid representation: + // x s + // 1 1 + let grid: felt252 = 0xF; + let width = 2; + let height = 2; + let from = 0; + let to = 1; + let path = BFS::search(grid, width, height, from, to); + assert_eq!(path, array![1].span()); + } + + #[test] + fn test_bfs_maze() { + // Grid representation: + // x 1 0 0 0 + // 0 1 1 1 0 + // 0 0 0 1 0 + // 1 1 1 1 s + let grid: felt252 = 0xC385F; + let width = 5; + let height = 4; + let from = 0; + let to = 19; + let path = BFS::search(grid, width, height, from, to); + assert_eq!(path, array![19, 18, 13, 12, 11, 6, 1].span()); + } + + #[test] + fn test_bfs_long_straight_path() { + // Grid representation: + // x 1 1 1 1 s + let grid: felt252 = 0x3F; + let width = 6; + let height = 1; + let from = 0; + let to = 5; + let path = BFS::search(grid, width, height, from, to); + assert_eq!(path, array![5, 4, 3, 2, 1].span()); + } + + #[test] + fn test_bfs_all_obstacles() { + // Grid representation: + // x 0 0 + // 0 0 0 + // 0 0 s + let grid: felt252 = 0x101; + let width = 3; + let height = 3; + let from = 0; + let to = 8; + let path = BFS::search(grid, width, height, from, to); + assert_eq!(path, array![].span()); + } +} diff --git a/crates/map/src/lib.cairo b/crates/map/src/lib.cairo index 5e957c7..9fbc7c8 100644 --- a/crates/map/src/lib.cairo +++ b/crates/map/src/lib.cairo @@ -18,8 +18,8 @@ pub mod helpers { pub mod spreader; pub mod astar; pub mod heap; + pub mod bfs; #[cfg(target: "test")] pub mod printer; } -