From af178e97ffc7e2469fd89757d48732ffed9ca1c3 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 17 Sep 2024 16:28:06 -0400 Subject: [PATCH 1/4] feat: breadth-first search algorithm --- crates/map/src/helpers/bfs.cairo | 301 +++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) 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..561d0fd --- /dev/null +++ b/crates/map/src/helpers/bfs.cairo @@ -0,0 +1,301 @@ +//! Breadth-First Search algorithm implementation for pathfinding. + +// Core imports +use core::dict::{Felt252Dict, Felt252DictTrait}; + +// Internal imports +use origami_map::helpers::bitmap::Bitmap; +use origami_map::types::node::{Node, NodeTrait}; +use origami_map::types::direction::Direction; + +// Custom Queue implementation +#[derive(Drop)] +struct Queue { + elements: Array, +} + +/// Trait defining the Queue operations +trait QueueTrait { + /// Creates a new empty queue + fn new() -> Queue; + /// Adds an element to the back of the queue + fn enqueue(ref self: Queue, value: T); + /// Removes and returns the front element of the queue + fn dequeue(ref self: Queue) -> Option; + + fn is_empty(self: @Queue) -> bool; +} + +/// Implementation of QueueTrait +impl QueueImpl> of QueueTrait { + fn new() -> Queue { + Queue { elements: ArrayTrait::new() } + } + + fn enqueue(ref self: Queue, value: T) { + self.elements.append(value); + } + + fn dequeue(ref self: Queue) -> Option { + if self.elements.is_empty() { + return Option::None; + } + Option::Some(self.elements.pop_front().unwrap()) + } + + fn is_empty(self: @Queue) -> bool { + self.elements.is_empty() + } +} + +/// 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: Queue = QueueTrait::new(); + let mut visited: Felt252Dict = Default::default(); + let mut parents: Felt252Dict = Default::default(); + + queue.enqueue(start); + visited.insert(start.position.into(), true); + + // [Compute] BFS until the target is reached or queue is empty + let mut path_found = false; + loop { + if queue.is_empty() { + break; + } + + let current = queue.dequeue().unwrap(); + + // [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 directions = array![ + Direction::North, Direction::East, Direction::South, Direction::West + ]; + let mut i = 0; + loop { + if i >= directions.len() { + break; + } + let direction = *directions.at(i); + if Self::check(grid, width, height, current.position, direction, ref visited) { + let neighbor_position = Self::get_neighbor_position( + current.position, direction, width + ); + parents.insert(neighbor_position.into(), current.position); + let neighbor = NodeTrait::new(neighbor_position, current.position, 0, 0); + queue.enqueue(neighbor); + visited.insert(neighbor_position.into(), true); + } + i += 1; + }; + }; + + // Reconstruct and return the path if found + if path_found { + Self::path(parents, start, target) + } else { + array![].span() + } + } + + /// Checks if a neighbor in the given direction is valid and unvisited + #[inline] + fn check( + grid: felt252, + width: u8, + height: u8, + position: u8, + direction: Direction, + ref visited: Felt252Dict + ) -> bool { + let (x, y) = (position % width, position / width); + match direction { + Direction::North => { + y < height + - 1 + && Bitmap::get(grid, position + width.into()) == 1 + && !visited.get((position + width.into()).into()) + }, + Direction::East => { + x < width + - 1 + && Bitmap::get(grid, position + 1) == 1 + && !visited.get((position + 1).into()) + }, + Direction::South => { + y > 0 + && Bitmap::get(grid, position - width.into()) == 1 + && !visited.get((position - width.into()).into()) + }, + Direction::West => { + x > 0 && Bitmap::get(grid, position - 1) == 1 && !visited.get((position - 1).into()) + }, + _ => false, + } + } + + /// Calculates the position of a neighbor in the given direction + #[inline] + fn get_neighbor_position(position: u8, direction: Direction, width: u8) -> u8 { + match direction { + Direction::North => position + width, + Direction::East => position + 1, + Direction::South => position - width, + Direction::West => position - 1, + _ => 0, + } + } + + /// 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 = 0x1F1F0F43; + 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, 17, 16, 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: + // 0 0 0 + // 0 0 0 + // 0 0 0 + let grid: felt252 = 0x0; + 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()); + } +} From ed720c596c985cd1892f57a5444c64c6d81c3238 Mon Sep 17 00:00:00 2001 From: Tony Stark Date: Tue, 17 Sep 2024 16:52:06 -0400 Subject: [PATCH 2/4] feat: separated the heap and added more comments --- crates/map/src/helpers/bfs.cairo | 40 +------------- crates/map/src/helpers/queue.cairo | 87 ++++++++++++++++++++++++++++++ crates/map/src/lib.cairo | 3 +- 3 files changed, 90 insertions(+), 40 deletions(-) create mode 100644 crates/map/src/helpers/queue.cairo diff --git a/crates/map/src/helpers/bfs.cairo b/crates/map/src/helpers/bfs.cairo index 561d0fd..d3d277f 100644 --- a/crates/map/src/helpers/bfs.cairo +++ b/crates/map/src/helpers/bfs.cairo @@ -4,49 +4,11 @@ use core::dict::{Felt252Dict, Felt252DictTrait}; // Internal imports +use origami_map::helpers::queue::{Queue, QueueTrait}; use origami_map::helpers::bitmap::Bitmap; use origami_map::types::node::{Node, NodeTrait}; use origami_map::types::direction::Direction; -// Custom Queue implementation -#[derive(Drop)] -struct Queue { - elements: Array, -} - -/// Trait defining the Queue operations -trait QueueTrait { - /// Creates a new empty queue - fn new() -> Queue; - /// Adds an element to the back of the queue - fn enqueue(ref self: Queue, value: T); - /// Removes and returns the front element of the queue - fn dequeue(ref self: Queue) -> Option; - - fn is_empty(self: @Queue) -> bool; -} - -/// Implementation of QueueTrait -impl QueueImpl> of QueueTrait { - fn new() -> Queue { - Queue { elements: ArrayTrait::new() } - } - - fn enqueue(ref self: Queue, value: T) { - self.elements.append(value); - } - - fn dequeue(ref self: Queue) -> Option { - if self.elements.is_empty() { - return Option::None; - } - Option::Some(self.elements.pop_front().unwrap()) - } - - fn is_empty(self: @Queue) -> bool { - self.elements.is_empty() - } -} /// BFS implementation for pathfinding #[generate_trait] diff --git a/crates/map/src/helpers/queue.cairo b/crates/map/src/helpers/queue.cairo new file mode 100644 index 0000000..d45861f --- /dev/null +++ b/crates/map/src/helpers/queue.cairo @@ -0,0 +1,87 @@ +//! Queue implementation. + +// Custom Queue implementation +#[derive(Drop)] +pub struct Queue { + /// The elements in the queue, stored in an Array. + elements: Array, +} + +/// Trait defining the Queue operations +pub trait QueueTrait { + /// Creates a new empty queue. + /// # Returns + /// * A new Queue + fn new() -> Queue; + + /// Adds an element to the back of the queue. + /// # Arguments + /// * `self` - The queue + /// * `value` - The value to be added + /// # Effects + /// * The value is appended to the end of the queue + fn enqueue(ref self: Queue, value: T); + + /// Removes and returns the front element of the queue. + /// # Arguments + /// * `self` - The queue + /// # Returns + /// * The front element if the queue is not empty, `None` otherwise + /// # Effects + /// * The front element is removed from the queue if it exists + fn dequeue(ref self: Queue) -> Option; + + /// Checks if the queue is empty. + /// # Arguments + /// * `self` - The queue + /// # Returns + /// * `true` if the queue is empty, `false` otherwise + fn is_empty(self: @Queue) -> bool; +} + +/// Implementation of QueueTrait +pub impl QueueImpl> of QueueTrait { + /// Creates a new empty queue. + /// # Returns + /// * A new Queue + #[inline] + fn new() -> Queue { + Queue { elements: ArrayTrait::new() } + } + + /// Adds an element to the back of the queue. + /// # Arguments + /// * `self` - The queue + /// * `value` - The value to be added + /// # Effects + /// * The value is appended to the end of the queue + #[inline] + fn enqueue(ref self: Queue, value: T) { + self.elements.append(value); + } + + /// Removes and returns the front element of the queue. + /// # Arguments + /// * `self` - The queue + /// # Returns + /// * The front element if the queue is not empty, `None` otherwise + /// # Effects + /// * The front element is removed from the queue if it exists + #[inline] + fn dequeue(ref self: Queue) -> Option { + if self.elements.is_empty() { + return Option::None; + } + Option::Some(self.elements.pop_front().unwrap()) + } + + /// Checks if the queue is empty. + /// # Arguments + /// * `self` - The queue + /// # Returns + /// * `true` if the queue is empty, `false` otherwise + #[inline] + fn is_empty(self: @Queue) -> bool { + self.elements.is_empty() + } +} diff --git a/crates/map/src/lib.cairo b/crates/map/src/lib.cairo index 5e957c7..0120a98 100644 --- a/crates/map/src/lib.cairo +++ b/crates/map/src/lib.cairo @@ -18,8 +18,9 @@ pub mod helpers { pub mod spreader; pub mod astar; pub mod heap; + pub mod bfs; + pub mod queue; #[cfg(target: "test")] pub mod printer; } - From 1fab8dd82b1b70a71b62a7b4e49f352d4ff17b75 Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Tue, 8 Oct 2024 10:59:28 +0200 Subject: [PATCH 3/4] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20Simplify=20the=20code?= =?UTF-8?q?=20and=20add=20pseudo=20random=20decision=20about=20directions?= =?UTF-8?q?=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/map/src/helpers/bfs.cairo | 93 +++++++----------------------- crates/map/src/helpers/queue.cairo | 87 ---------------------------- crates/map/src/lib.cairo | 1 - 3 files changed, 21 insertions(+), 160 deletions(-) delete mode 100644 crates/map/src/helpers/queue.cairo diff --git a/crates/map/src/helpers/bfs.cairo b/crates/map/src/helpers/bfs.cairo index d3d277f..4ab3277 100644 --- a/crates/map/src/helpers/bfs.cairo +++ b/crates/map/src/helpers/bfs.cairo @@ -4,10 +4,11 @@ use core::dict::{Felt252Dict, Felt252DictTrait}; // Internal imports -use origami_map::helpers::queue::{Queue, QueueTrait}; +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; +use origami_map::types::direction::{Direction, DirectionTrait}; /// BFS implementation for pathfinding @@ -36,93 +37,41 @@ pub impl BFS of BFSTrait { let target = NodeTrait::new(to, 0, 0, 0); // [Effect] Initialize the queue and the visited nodes - let mut queue: Queue = QueueTrait::new(); + let mut queue: Array = array![start]; let mut visited: Felt252Dict = Default::default(); let mut parents: Felt252Dict = Default::default(); - - queue.enqueue(start); visited.insert(start.position.into(), true); // [Compute] BFS until the target is reached or queue is empty let mut path_found = false; - loop { - if queue.is_empty() { - break; - } - - let current = queue.dequeue().unwrap(); - + 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 directions = array![ - Direction::North, Direction::East, Direction::South, Direction::West - ]; - let mut i = 0; - loop { - if i >= directions.len() { - break; - } - let direction = *directions.at(i); - if Self::check(grid, width, height, current.position, direction, ref visited) { + 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 = Self::get_neighbor_position( current.position, direction, width ); parents.insert(neighbor_position.into(), current.position); let neighbor = NodeTrait::new(neighbor_position, current.position, 0, 0); - queue.enqueue(neighbor); + queue.append(neighbor); visited.insert(neighbor_position.into(), true); } - i += 1; }; }; // Reconstruct and return the path if found - if path_found { - Self::path(parents, start, target) - } else { - array![].span() - } - } - - /// Checks if a neighbor in the given direction is valid and unvisited - #[inline] - fn check( - grid: felt252, - width: u8, - height: u8, - position: u8, - direction: Direction, - ref visited: Felt252Dict - ) -> bool { - let (x, y) = (position % width, position / width); - match direction { - Direction::North => { - y < height - - 1 - && Bitmap::get(grid, position + width.into()) == 1 - && !visited.get((position + width.into()).into()) - }, - Direction::East => { - x < width - - 1 - && Bitmap::get(grid, position + 1) == 1 - && !visited.get((position + 1).into()) - }, - Direction::South => { - y > 0 - && Bitmap::get(grid, position - width.into()) == 1 - && !visited.get((position - width.into()).into()) - }, - Direction::West => { - x > 0 && Bitmap::get(grid, position - 1) == 1 && !visited.get((position - 1).into()) - }, - _ => false, - } + if !path_found { + return array![].span(); + }; + Self::path(parents, start, target) } /// Calculates the position of a neighbor in the given direction @@ -200,7 +149,7 @@ mod test { 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()); + assert_eq!(path, array![14, 15, 11, 7, 6, 5, 1].span()); } #[test] @@ -224,13 +173,13 @@ mod test { // 0 1 1 1 0 // 0 0 0 1 0 // 1 1 1 1 s - let grid: felt252 = 0x1F1F0F43; + 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, 17, 16, 11, 6, 1].span()); + assert_eq!(path, array![19, 18, 13, 12, 11, 6, 1].span()); } #[test] @@ -249,10 +198,10 @@ mod test { #[test] fn test_bfs_all_obstacles() { // Grid representation: + // x 0 0 // 0 0 0 - // 0 0 0 - // 0 0 0 - let grid: felt252 = 0x0; + // 0 0 s + let grid: felt252 = 0x101; let width = 3; let height = 3; let from = 0; diff --git a/crates/map/src/helpers/queue.cairo b/crates/map/src/helpers/queue.cairo deleted file mode 100644 index d45861f..0000000 --- a/crates/map/src/helpers/queue.cairo +++ /dev/null @@ -1,87 +0,0 @@ -//! Queue implementation. - -// Custom Queue implementation -#[derive(Drop)] -pub struct Queue { - /// The elements in the queue, stored in an Array. - elements: Array, -} - -/// Trait defining the Queue operations -pub trait QueueTrait { - /// Creates a new empty queue. - /// # Returns - /// * A new Queue - fn new() -> Queue; - - /// Adds an element to the back of the queue. - /// # Arguments - /// * `self` - The queue - /// * `value` - The value to be added - /// # Effects - /// * The value is appended to the end of the queue - fn enqueue(ref self: Queue, value: T); - - /// Removes and returns the front element of the queue. - /// # Arguments - /// * `self` - The queue - /// # Returns - /// * The front element if the queue is not empty, `None` otherwise - /// # Effects - /// * The front element is removed from the queue if it exists - fn dequeue(ref self: Queue) -> Option; - - /// Checks if the queue is empty. - /// # Arguments - /// * `self` - The queue - /// # Returns - /// * `true` if the queue is empty, `false` otherwise - fn is_empty(self: @Queue) -> bool; -} - -/// Implementation of QueueTrait -pub impl QueueImpl> of QueueTrait { - /// Creates a new empty queue. - /// # Returns - /// * A new Queue - #[inline] - fn new() -> Queue { - Queue { elements: ArrayTrait::new() } - } - - /// Adds an element to the back of the queue. - /// # Arguments - /// * `self` - The queue - /// * `value` - The value to be added - /// # Effects - /// * The value is appended to the end of the queue - #[inline] - fn enqueue(ref self: Queue, value: T) { - self.elements.append(value); - } - - /// Removes and returns the front element of the queue. - /// # Arguments - /// * `self` - The queue - /// # Returns - /// * The front element if the queue is not empty, `None` otherwise - /// # Effects - /// * The front element is removed from the queue if it exists - #[inline] - fn dequeue(ref self: Queue) -> Option { - if self.elements.is_empty() { - return Option::None; - } - Option::Some(self.elements.pop_front().unwrap()) - } - - /// Checks if the queue is empty. - /// # Arguments - /// * `self` - The queue - /// # Returns - /// * `true` if the queue is empty, `false` otherwise - #[inline] - fn is_empty(self: @Queue) -> bool { - self.elements.is_empty() - } -} diff --git a/crates/map/src/lib.cairo b/crates/map/src/lib.cairo index 0120a98..9fbc7c8 100644 --- a/crates/map/src/lib.cairo +++ b/crates/map/src/lib.cairo @@ -19,7 +19,6 @@ pub mod helpers { pub mod astar; pub mod heap; pub mod bfs; - pub mod queue; #[cfg(target: "test")] pub mod printer; From 711c743c0f219b7f0d7c359942e42c495d164170 Mon Sep 17 00:00:00 2001 From: bal7hazar Date: Tue, 8 Oct 2024 11:15:45 +0200 Subject: [PATCH 4/4] =?UTF-8?q?=F0=9F=94=A5=20Use=20exisiting=20function?= =?UTF-8?q?=20for=20finding=20neighbor=20positions=20and=20remove=20extra?= =?UTF-8?q?=20code?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/map/src/helpers/bfs.cairo | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/crates/map/src/helpers/bfs.cairo b/crates/map/src/helpers/bfs.cairo index 4ab3277..df2ec89 100644 --- a/crates/map/src/helpers/bfs.cairo +++ b/crates/map/src/helpers/bfs.cairo @@ -56,9 +56,7 @@ pub impl BFS of BFSTrait { while directions != 0 { let direction = DirectionTrait::pop_front(ref directions); if Astar::check(grid, width, height, current.position, direction, ref visited) { - let neighbor_position = Self::get_neighbor_position( - current.position, direction, width - ); + 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); @@ -74,18 +72,6 @@ pub impl BFS of BFSTrait { Self::path(parents, start, target) } - /// Calculates the position of a neighbor in the given direction - #[inline] - fn get_neighbor_position(position: u8, direction: Direction, width: u8) -> u8 { - match direction { - Direction::North => position + width, - Direction::East => position + 1, - Direction::South => position - width, - Direction::West => position - 1, - _ => 0, - } - } - /// Reconstructs the path from start to target using the parents dictionary #[inline] fn path(mut parents: Felt252Dict, start: Node, target: Node) -> Span { @@ -149,7 +135,7 @@ mod test { 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, 1].span()); + assert_eq!(path, array![14, 15, 11, 7, 6, 5, 4].span()); } #[test]