diff --git a/crates/map/src/helpers/astar.cairo b/crates/map/src/helpers/astar.cairo new file mode 100644 index 0000000..3835198 --- /dev/null +++ b/crates/map/src/helpers/astar.cairo @@ -0,0 +1,269 @@ +//! A* algorithm implementation for pathfinding. + +// Core imports + +use core::dict::{Felt252Dict, Felt252DictTrait}; + +// Internal imports + +use origami_map::helpers::heap::{Heap, HeapTrait}; +use origami_map::helpers::bitmap::Bitmap; +use origami_map::types::node::{Node, NodeTrait}; +use origami_map::types::direction::Direction; + +#[generate_trait] +pub impl Astar of AstarTrait { + /// Search for the shortest path from a start to a target position. + /// # Arguments + /// * `grid` - The grid to search (1 is walkable and 0 is not) + /// * `width` - The width of the grid + /// * `height` - The height of the grid + /// * `from` - The starting position + /// * `to` - The target position + /// # Returns + /// * The path from the target (incl.) to the start (excl.) + #[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 heap and the visited nodes + let mut heap: Heap = HeapTrait::new(); + let mut visited: Felt252Dict = Default::default(); + heap.add(start); + // [Compute] Evaluate the path until the target is reached + while !heap.is_empty() { + // [Compute] Get the less expensive node + let current: Node = heap.pop_front().unwrap(); + visited.insert(current.position.into(), true); + // [Check] Stop if we reached the target + if current.position == target.position { + break; + } + // [Compute] Evaluate the neighbors for all 4 directions + if Self::check(grid, width, height, current.position, Direction::North, ref visited) { + let neighbor_position = current.position + width; + Self::assess(width, neighbor_position, current, target, ref heap); + } + if Self::check(grid, width, height, current.position, Direction::East, ref visited) { + let neighbor_position = current.position + 1; + Self::assess(width, neighbor_position, current, target, ref heap); + } + if Self::check(grid, width, height, current.position, Direction::South, ref visited) { + let neighbor_position = current.position - width; + Self::assess(width, neighbor_position, current, target, ref heap); + } + if Self::check(grid, width, height, current.position, Direction::West, ref visited) { + let neighbor_position = current.position - 1; + Self::assess(width, neighbor_position, current, target, ref heap); + } + }; + + // [Return] The path from the start to the target + Self::path(ref heap, start, target) + } + + /// Check if the position can be visited in the specified direction. + /// # Arguments + /// * `grid` - The grid to search (1 is walkable and 0 is not) + /// * `width` - The width of the grid + /// * `height` - The height of the grid + /// * `position` - The current position + /// * `direction` - The direction to check + /// * `visited` - The visited nodes + /// # Returns + /// * Whether the position can be visited in the specified direction + #[inline] + fn check( + grid: felt252, + width: u8, + height: u8, + position: u8, + direction: Direction, + ref visisted: Felt252Dict + ) -> bool { + let (x, y) = (position % width, position / width); + match direction { + Direction::North => (y < height - 1) + && (Bitmap::get(grid, position + width) == 1) + && !visisted.get((position + width).into()), + Direction::East => (x < width - 1) + && (Bitmap::get(grid, position + 1) == 1) + && !visisted.get((position + 1).into()), + Direction::South => (y > 0) + && (Bitmap::get(grid, position - width) == 1) + && !visisted.get((position - width).into()), + Direction::West => (x > 0) + && (Bitmap::get(grid, position - 1) == 1) + && !visisted.get((position - 1).into()), + _ => false, + } + } + + /// Assess the neighbor node and update the heap. + /// # Arguments + /// * `width` - The width of the grid + /// * `neighbor_position` - The position of the neighbor + /// * `current` - The current node + /// * `target` - The target node + /// * `heap` - The heap of nodes + /// # Effects + /// * Update the heap with the neighbor node + #[inline] + fn assess( + width: u8, neighbor_position: u8, current: Node, target: Node, ref heap: Heap, + ) { + let distance = Self::heuristic(current.position, neighbor_position, width); + let neighbor_gcost = current.gcost + distance; + let neighbor_hcost = Self::heuristic(neighbor_position, target.position, width); + let mut neighbor = match heap.get(neighbor_position.into()) { + Option::Some(node) => node, + Option::None => NodeTrait::new( + neighbor_position, current.position, neighbor_gcost, neighbor_hcost + ), + }; + if neighbor_gcost < neighbor.gcost || !heap.contains(neighbor.position) { + neighbor.gcost = neighbor_gcost; + neighbor.source = current.position; + if !heap.contains(neighbor.position) { + return heap.add(neighbor); + } + return heap.update(neighbor); + } + } + + /// Compute the heuristic cost between two positions. + /// # Arguments + /// * `position` - The current position + /// * `target` - The target position + /// * `width` - The width of the grid + /// # Returns + /// * The heuristic cost between the two positions + #[inline] + fn heuristic(position: u8, target: u8, width: u8) -> u16 { + let (x1, y1) = (position % width, position / width); + let (x2, y2) = (target % width, target / width); + let dx = if x1 > x2 { + x1 - x2 + } else { + x2 - x1 + }; + let dy = if y1 > y2 { + y1 - y2 + } else { + y2 - y1 + }; + (dx + dy).into() + } + + /// Reconstruct the path from the target to the start. + /// # Arguments + /// * `heap` - The heap of nodes + /// * `start` - The starting node + /// * `target` - The target node + /// # Returns + /// * The span of positions from the target to the start + #[inline] + fn path(ref heap: Heap, start: Node, target: Node) -> Span { + // [Check] The heap contains the target + let mut path: Array = array![]; + match heap.get(target.position) { + Option::None => { path.span() }, + Option::Some(mut current) => { + // [Compute] Reconstruct the path from the target to the start + loop { + if current.position == start.position { + break; + } + path.append(current.position); + current = heap.at(current.source); + }; + // [Return] The path from the start to the target + path.span() + }, + } + } +} + +#[cfg(test)] +mod test { + // Local imports + + use super::{Astar, Node, NodeTrait}; + + #[test] + fn test_astar_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 mut path = Astar::search(grid, width, height, from, to); + assert_eq!(path, array![8, 7, 6, 3].span()); + } + + #[test] + fn test_astar_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 mut path = Astar::search(grid, width, height, from, to); + assert_eq!(path, array![].span()); + } + + #[test] + fn test_astar_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 mut path = Astar::search(grid, width, height, from, to); + assert_eq!(path, array![14, 15, 11, 7, 6, 5, 4].span()); + } + + #[test] + fn test_astar_search_large() { + // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + // 0 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 + // 0 0 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 + // 0 0 1 1 1 1 1 0 1 1 0 0 0 0 0 0 0 0 + // 0 0 0 1 1 1 1 ┌───x 0 0 0 0 0 0 0 0 + // 0 0 0 0 1 1 1 │ 0 0 0 1 0 0 1 0 0 0 + // 0 0 0 1 1 1 1 │ 0 0 0 1 1 1 1 1 0 0 + // 0 0 1 1 1 1 1 └───┐ 1 1 1 1 1 1 1 0 + // 0 0 0 1 1 1 1 0 1 │ 1 0 1 1 1 1 1 0 + // 0 0 0 0 1 1 1 1 1 └─┐ 1 1 1 1 1 1 0 + // 0 0 0 1 1 1 1 1 1 1 └───────────s 0 + // 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 + // 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 0 + // 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + let grid: felt252 = 0x7F003F800FB001FC003C481F1F0FFFE1EEF83FFE1FFF81FFE03FF80000; + let width = 18; + let height = 14; + let from = 55; + let to = 170; + let mut path = Astar::search(grid, width, height, from, to); + assert_eq!( + path, + array![170, 171, 172, 154, 136, 118, 117, 116, 98, 80, 79, 61, 60, 59, 58, 57, 56] + .span() + ); + } +} diff --git a/crates/map/src/helpers/heap.cairo b/crates/map/src/helpers/heap.cairo new file mode 100644 index 0000000..509a038 --- /dev/null +++ b/crates/map/src/helpers/heap.cairo @@ -0,0 +1,439 @@ +//! Heap implementation. + +// Core imports + +use core::dict::{Felt252Dict, Felt252DictTrait}; + +// Internal imports + +use origami_map::types::node::Node; + +// Constants + +const KEY_OFFSET: felt252 = 252; + +/// Traits. +pub trait HeapTrait { + fn new() -> Heap; + /// Create if the heap is empty. + /// # Arguments + /// * `self` - The heap + /// # Returns + /// * `true` if the heap is empty, `false` otherwise + fn is_empty(self: @Heap) -> bool; + /// Get an item from the heap if it exists. + /// # Arguments + /// * `self` - The heap + /// * `key` - The key of the item + /// # Returns + /// * The item if it exists, `None` otherwise + fn get(ref self: Heap, key: u8) -> Option; + /// Get an item from the heap. + /// # Arguments + /// * `self` - The heap + /// * `key` - The key of the item + /// # Returns + /// * The item + /// # Panics + /// * If the item does not exist + fn at(ref self: Heap, key: u8) -> T; + /// Check if the heap contains an item. + /// # Arguments + /// * `self` - The heap + /// * `key` - The key of the item + /// # Returns + /// * `true` if the item exists, `false` otherwise + fn contains(ref self: Heap, key: u8) -> bool; + /// Add an item to the heap. + /// # Arguments + /// * `self` - The heap + /// * `item` - The item to add + /// # Effects + /// * The item is added at the end of the heap and the heap is sorted up + fn add(ref self: Heap, item: T); + /// Update an item in the heap. + /// # Arguments + /// * `self` - The heap + /// * `item` - The item to update + /// # Effects + /// * The item is updated and the heap is sorted up since it cannot be updated with a lower + /// score in case of A* algorithm + fn update(ref self: Heap, item: T); + /// Pop the first item from the heap. + /// # Arguments + /// * `self` - The heap + /// # Returns + /// * The first item if the heap is not empty, `None` otherwise + fn pop_front(ref self: Heap) -> Option; + /// Sort an item up in the heap. + /// # Arguments + /// * `self` - The heap + /// * `item_key` - The key of the item to sort up + /// # Effects + /// * The items are swapped until the item is in the right place + fn sort_up(ref self: Heap, item_key: u8); + /// Sort an item down in the heap. + /// # Arguments + /// * `self` - The heap + /// * `item_key` - The key of the item to sort down + /// # Effects + /// * The items are swapped until the item is in the right place + fn sort_down(ref self: Heap, item_key: u8); + /// Swap two items in the heap. + /// # Arguments + /// * `self` - The heap + /// * `lhs` - The key of the first item + /// * `rhs` - The key of the second item + /// # Effects + /// * The items are swapped + fn swap(ref self: Heap, lhs: u8, rhs: u8); +} + +pub trait ItemTrait { + /// Get the key of the item. + /// # Arguments + /// * `self` - The item + /// # Returns + /// * The key of the item + fn key(self: T) -> u8; +} + +/// Types. +pub struct Heap { + /// The length of the heap. + pub len: u8, + /// The keys of the items in the heap and also the indexes of the items in the data. + /// Both information is stored in the same map to save gas. + pub keys: Felt252Dict, + /// The items. + pub data: Felt252Dict>, +} + +/// Implementations. +pub impl HeapImpl, +PartialOrd, +Copy, +Drop> of HeapTrait { + /// Create a new heap. + /// # Returns + /// * The heap + #[inline] + fn new() -> Heap { + Heap { len: 0, keys: Default::default(), data: Default::default(), } + } + + /// Check if the heap is empty. + /// # Arguments + /// * `self` - The heap + /// # Returns + /// * `true` if the heap is empty, `false` otherwise + #[inline] + fn is_empty(self: @Heap) -> bool { + *self.len == 0 + } + + /// Get an item from the heap if it exists. + /// # Arguments + /// * `self` - The heap + /// * `key` - The key of the item + /// # Returns + /// * The item if it exists, `None` otherwise + #[inline] + fn get(ref self: Heap, key: u8) -> Option { + let nullable: Nullable = self.data.get(key.into()); + if nullable.is_null() { + return Option::None; + } + Option::Some(nullable.deref()) + } + + /// Get an item from the heap. + /// # Arguments + /// * `self` - The heap + /// * `key` - The key of the item + /// # Returns + /// * The item + /// # Panics + /// * If the item does not exist + #[inline] + fn at(ref self: Heap, key: u8) -> T { + self.data.get(key.into()).deref() + } + + /// Check if the heap contains an item. + /// # Arguments + /// * `self` - The heap + /// * `key` - The key of the item + /// # Returns + /// * `true` if the item exists, `false` otherwise + #[inline] + fn contains(ref self: Heap, key: u8) -> bool { + let index = self.keys.get(key.into() + KEY_OFFSET); + let item_key = self.keys.get(index.into()); + index < self.len && item_key == key + } + + /// Add an item to the heap. + /// # Arguments + /// * `self` - The heap + /// * `item` - The item to add + /// # Effects + /// * The item is added at the end of the heap and the heap is sorted up + #[inline] + fn add(ref self: Heap, item: T) { + // [Effect] Update heap length + let key = item.key(); + let index = self.len; + self.len += 1; + // [Effect] Insert item at the end + self.data.insert(key.into(), NullableTrait::new(item)); + self.keys.insert(index.into(), key); + self.keys.insert(key.into() + KEY_OFFSET, index); + // [Effect] Sort up + self.sort_up(key); + } + + /// Update an item in the heap. + /// # Arguments + /// * `self` - The heap + /// * `item` - The item to update + /// # Effects + /// * The item is updated and the heap is sorted up + /// # Info + /// * The heap is only sorted up since it cannot be updated with a lower score in case of A* + /// algorithm + #[inline] + fn update(ref self: Heap, item: T) { + // [Effect] Update item + let key = item.key(); + self.data.insert(key.into(), NullableTrait::new(item)); + // [Effect] Sort up + self.sort_up(key); + } + + /// Pop the first item from the heap. + /// # Arguments + /// * `self` - The heap + /// # Returns + /// * The first item if the heap is not empty, `None` otherwise + /// # Effects + /// * The first item is removed, replaced by the last item and the heap is sorted down + #[inline] + fn pop_front(ref self: Heap) -> Option { + if self.is_empty() { + return Option::None; + } + self.len -= 1; + let first_key: u8 = self.keys.get(0); + let mut first: T = self.data.get(first_key.into()).deref(); + if self.len != 0 { + let last_key: u8 = self.keys.get(self.len.into()); + self.swap(first_key, last_key); + self.sort_down(last_key); + } + Option::Some(first) + } + + /// Sort an item up in the heap. + /// # Arguments + /// * `self` - The heap + /// * `item_key` - The key of the item to sort up + /// # Effects + /// * The items are swapped from bottom to top until the item is in the right place + #[inline] + fn sort_up(ref self: Heap, item_key: u8) { + // [Compute] Item + let item: T = self.data.get(item_key.into()).deref(); + let mut index = self.keys.get(item_key.into() + KEY_OFFSET); + // [Compute] Peform swaps until the item is in the right place + while index != 0 { + index = (index - 1) / 2; + let parent_key = self.keys.get(index.into()); + let mut parent: T = self.data.get(parent_key.into()).deref(); + if parent <= item { + break; + } + self.swap(parent_key, item_key); + } + } + + /// Sort an item down in the heap. + /// # Arguments + /// * `self` - The heap + /// * `item_key` - The key of the item to sort down + /// # Effects + /// * The items are swapped from top to bottom until the item is in the right place + #[inline] + fn sort_down(ref self: Heap, item_key: u8) { + // [Compute] Item + let item: T = self.data.get(item_key.into()).deref(); + let mut index: u8 = self.keys.get(item_key.into() + KEY_OFFSET); + // [Compute] Peform swaps until the item is in the right place + let mut lhs_index = index * 2 + 1; + while lhs_index < self.len { + // [Compute] Child to swap + index = lhs_index; + let mut child_key: u8 = self.keys.get(index.into()); + let mut child: T = self.data.get(child_key.into()).deref(); + // [Compute] Assess right child side + let rhs_index = index * 2 + 2; + if rhs_index < self.len { + let rhs_key: u8 = self.keys.get(rhs_index.into()); + let rhs: T = self.data.get(rhs_key.into()).deref(); + if rhs < child { + index = rhs_index; + child_key = rhs_key; + child = rhs; + }; + } + // [Effect] Swap if necessary + if item <= child { + break; + }; + self.swap(item_key, child_key); + // [Check] Stop criteria, assess left child side + lhs_index = index * 2 + 1; + } + } + + /// Swap two items in the heap. + /// # Arguments + /// * `self` - The heap + /// * `lhs` - The key of the first item + /// * `rhs` - The key of the second item + /// # Effects + /// * The items are swapped + #[inline] + fn swap(ref self: Heap, lhs: u8, rhs: u8) { + // [Effect] Swap keys + let lhs_index = self.keys.get(lhs.into() + KEY_OFFSET); + let rhs_index = self.keys.get(rhs.into() + KEY_OFFSET); + self.keys.insert(lhs.into() + KEY_OFFSET, rhs_index); + self.keys.insert(rhs.into() + KEY_OFFSET, lhs_index); + self.keys.insert(lhs_index.into(), rhs); + self.keys.insert(rhs_index.into(), lhs); + } +} + +impl DestructHeap> of Destruct> { + fn destruct(self: Heap) nopanic { + self.keys.squash(); + self.data.squash(); + } +} + +#[cfg(test)] +mod tests { + // Local imports + + use super::{Node, Heap, HeapTrait, ItemTrait}; + + #[test] + fn test_heap_new() { + let heap: Heap = HeapTrait::new(); + assert!(heap.is_empty()); + } + + #[test] + fn test_heap_add() { + let mut heap: Heap = HeapTrait::new(); + let node: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + heap.add(node); + assert!(!heap.is_empty()); + } + + #[test] + fn test_heap_contains() { + let mut heap: Heap = HeapTrait::new(); + let node: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + heap.add(node); + assert!(heap.contains(node.position)); + } + + #[test] + fn test_heap_not_contains() { + let mut heap: Heap = HeapTrait::new(); + let node: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + assert!(!heap.contains(node.position)); + } + + #[test] + fn test_heap_pop_front_sorted() { + let mut heap: Heap = HeapTrait::new(); + let first: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + let second: Node = Node { position: 2, source: 2, gcost: 2, hcost: 2, }; + let third: Node = Node { position: 3, source: 3, gcost: 3, hcost: 3, }; + heap.add(first); + heap.add(second); + heap.add(third); + let popped: Node = heap.pop_front().unwrap(); + assert_eq!(popped.gcost, 1); + assert_eq!(popped.hcost, 1); + } + + #[test] + fn test_heap_pop_front_reversed() { + let mut heap: Heap = HeapTrait::new(); + let first: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + let second: Node = Node { position: 2, source: 2, gcost: 2, hcost: 2, }; + let third: Node = Node { position: 3, source: 3, gcost: 3, hcost: 3, }; + heap.add(third); + heap.add(second); + heap.add(first); + let popped: Node = heap.pop_front().unwrap(); + assert_eq!(popped.gcost, 1); + assert_eq!(popped.hcost, 1); + } + + #[test] + fn test_heap_swap() { + let mut heap: Heap = HeapTrait::new(); + let first: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + let second: Node = Node { position: 2, source: 2, gcost: 2, hcost: 2, }; + heap.add(first); + heap.add(second); + heap.swap(first.key(), second.key()); + assert_eq!(first.position, 1); + assert_eq!(first.gcost, 1); + let popped: Node = heap.pop_front().unwrap(); + assert_eq!(popped.position, 2); + assert_eq!(popped.gcost, 2); + } + + #[test] + fn test_heap_get() { + let mut heap: Heap = HeapTrait::new(); + let first: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + let second: Node = Node { position: 2, source: 2, gcost: 2, hcost: 2, }; + heap.add(first); + heap.add(second); + assert_eq!(heap.get(first.position).unwrap().position, 1); + assert_eq!(heap.get(second.position).unwrap().position, 2); + heap.swap(first.key(), second.key()); + assert_eq!(heap.get(first.position).unwrap().position, 1); + assert_eq!(heap.get(second.position).unwrap().position, 2); + } + + #[test] + fn test_heap_at() { + let mut heap: Heap = HeapTrait::new(); + let first: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + let second: Node = Node { position: 2, source: 2, gcost: 2, hcost: 2, }; + heap.add(first); + heap.add(second); + assert_eq!(heap.at(first.position).position, 1); + assert_eq!(heap.at(second.position).position, 2); + heap.swap(first.key(), second.key()); + assert_eq!(heap.at(first.position).position, 1); + assert_eq!(heap.at(second.position).position, 2); + } + + #[test] + fn test_heap_add_pop_add() { + let mut heap: Heap = HeapTrait::new(); + let first: Node = Node { position: 1, source: 1, gcost: 1, hcost: 1, }; + let second: Node = Node { position: 2, source: 2, gcost: 2, hcost: 2, }; + heap.add(first); + heap.add(second); + heap.pop_front().unwrap(); + assert_eq!(heap.at(1).position, 1); + assert_eq!(heap.at(2).position, 2); + } +} diff --git a/crates/map/src/helpers/printer.cairo b/crates/map/src/helpers/printer.cairo new file mode 100644 index 0000000..4a23828 --- /dev/null +++ b/crates/map/src/helpers/printer.cairo @@ -0,0 +1,112 @@ +//! Module for printing maps. +use core::fmt::Formatter; + +#[generate_trait] +pub impl MapPrinter of MapPrinterTrait { + /// Prints the bitmap as a 2D grid where 1 is walkable and 0 is not. + /// + /// # Arguments + /// * `map` - The bitmap to print + /// * `width` - The width of the grid + /// * `height` - The height of the grid + fn print(map: felt252, width: u8, height: u8) { + println!(""); + + let mut x: u256 = map.into(); + let mut y = 0; + let mut lines: Array = array![]; + + while y < height { + let mut line: ByteArray = ""; + let mut x_pos = 0; + while x_pos < width { + if x % 2 == 1 { + line.append(@"1"); + } else { + line.append(@"0"); + } + x /= 2; + x_pos += 1; + }; + + // Reverse the line to have smaller x coordinates at the end of the string. + lines.append(line.rev()); + y += 1; + }; + + let mut lines = lines.span(); + // Reverse order to have smaller y coordinates at the bottom. + while let Option::Some(l) = lines.pop_back() { + println!("{}", l); + }; + + println!(""); + } + + /// Prints the bitmap as a 2D grid where 1 is walkable and 0 is not. + /// The path is printed as a series of * characters and the start and end are printed as S and E + /// respectively. + /// + /// # Arguments + /// * `map` - The bitmap to print + /// * `width` - The width of the grid + /// * `height` - The height of the grid + /// * `from` - The index of the starting position + /// * `path` - The path to print + fn print_with_path(map: felt252, width: u8, height: u8, from: u8, path: Span) { + if path.is_empty() { + return; + } + + println!(""); + + let mut x: u256 = map.into(); + let mut y = 0; + let mut lines: Array = array![]; + + let last_path_index: u8 = *path[0]; + + while y < height { + let mut line: ByteArray = ""; + let mut x_pos = 0; + while x_pos < width { + let current_index = y * width + x_pos; + + let mut path_span = path; + let mut found = false; + while let Option::Some(value) = path_span.pop_front() { + if *value == current_index { + found = true; + break; + } + }; + + if current_index == from { + line.append(@"S"); + } else if current_index == last_path_index { + line.append(@"E"); + } else if found { + line.append(@"*"); + } else if x % 2 == 1 { + line.append(@"1"); + } else { + line.append(@"0"); + } + x /= 2; + x_pos += 1; + }; + + // Reverse the line to have smaller x coordinates at the end of the string. + lines.append(line.rev()); + y += 1; + }; + + let mut lines = lines.span(); + // Reverse order to have smaller y coordinates at the bottom. + while let Option::Some(l) = lines.pop_back() { + println!("{}", l); + }; + + println!(""); + } +} diff --git a/crates/map/src/helpers/spreader.cairo b/crates/map/src/helpers/spreader.cairo index fe0f658..7a51c05 100644 --- a/crates/map/src/helpers/spreader.cairo +++ b/crates/map/src/helpers/spreader.cairo @@ -105,8 +105,8 @@ mod tests { let width = 18; let height = 14; let grid: felt252 = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; - let room = Spreader::generate(grid, width, height, 35, SEED); - assert_eq!(room, 0x802800084000A006408008401003008004308C0002410E01080200008120); + let map = Spreader::generate(grid, width, height, 35, SEED); + assert_eq!(map, 0x802800084000A006408008401003008004308C0002410E01080200008120); } #[test] @@ -121,8 +121,8 @@ mod tests { let width = 18; let height = 14; let grid: felt252 = 0x38000E000380; - let room = Spreader::generate(grid, width, height, 4, SEED); - assert_eq!(room, 0x180000000300); + let map = Spreader::generate(grid, width, height, 4, SEED); + assert_eq!(map, 0x180000000300); } } diff --git a/crates/map/src/lib.cairo b/crates/map/src/lib.cairo index 6b71440..5e957c7 100644 --- a/crates/map/src/lib.cairo +++ b/crates/map/src/lib.cairo @@ -1,8 +1,9 @@ pub mod hex; -pub mod room; +pub mod map; pub mod types { pub mod direction; + pub mod node; } pub mod helpers { @@ -15,5 +16,10 @@ pub mod helpers { pub mod caver; pub mod walker; pub mod spreader; + pub mod astar; + pub mod heap; + + #[cfg(target: "test")] + pub mod printer; } diff --git a/crates/map/src/room.cairo b/crates/map/src/map.cairo similarity index 52% rename from crates/map/src/room.cairo rename to crates/map/src/map.cairo index 0d23801..8f1b469 100644 --- a/crates/map/src/room.cairo +++ b/crates/map/src/map.cairo @@ -1,4 +1,4 @@ -//! Room struct and generation methods. +//! Map struct and generation methods. // Internal imports @@ -9,121 +9,147 @@ use origami_map::helpers::walker::Walker; use origami_map::helpers::caver::Caver; use origami_map::helpers::digger::Digger; use origami_map::helpers::spreader::Spreader; +use origami_map::helpers::astar::Astar; /// Types. #[derive(Copy, Drop)] -pub struct Room { +pub struct Map { pub width: u8, pub height: u8, pub grid: felt252, pub seed: felt252, } -/// Implementation of the `RoomTrait` trait for the `Room` struct. +/// Implementation of the `MapTrait` trait for the `Map` struct. #[generate_trait] -pub impl RoomImpl of RoomTrait { - /// Create an empty room. +pub impl MapImpl of MapTrait { + /// Create a map. /// # Arguments - /// * `width` - The width of the room - /// * `height` - The height of the room - /// * `seed` - The seed to generate the room + /// * `grid` - The grid of the map + /// * `width` - The width of the map + /// * `height` - The height of the map + /// * `seed` - The seed to generate the map /// # Returns - /// * The generated room + /// * The corresponding map #[inline] - fn new_empty(width: u8, height: u8, seed: felt252) -> Room { + fn new(grid: felt252, width: u8, height: u8, seed: felt252) -> Map { + Map { width, height, grid, seed } + } + + /// Create an empty map. + /// # Arguments + /// * `width` - The width of the map + /// * `height` - The height of the map + /// * `seed` - The seed to generate the map + /// # Returns + /// * The generated map + #[inline] + fn new_empty(width: u8, height: u8, seed: felt252) -> Map { // [Check] Valid dimensions Asserter::assert_valid_dimension(width, height); - // [Effect] Generate room according to the method + // [Effect] Generate map according to the method let grid = Private::empty(width, height); - // [Effect] Create room - Room { width, height, grid, seed } + // [Effect] Create map + Map { width, height, grid, seed } } - /// Create a room with a maze. + /// Create a map with a maze. /// # Arguments - /// * `width` - The width of the room - /// * `height` - The height of the room - /// * `seed` - The seed to generate the room + /// * `width` - The width of the map + /// * `height` - The height of the map + /// * `seed` - The seed to generate the map /// # Returns - /// * The generated room + /// * The generated map #[inline] - fn new_maze(width: u8, height: u8, seed: felt252) -> Room { + fn new_maze(width: u8, height: u8, seed: felt252) -> Map { let grid = Mazer::generate(width, height, seed); - Room { width, height, grid, seed } + Map { width, height, grid, seed } } - /// Create a room with a cave. + /// Create a map with a cave. /// # Arguments - /// * `width` - The width of the room - /// * `height` - The height of the room + /// * `width` - The width of the map + /// * `height` - The height of the map /// * `order` - The order of the cave - /// * `seed` - The seed to generate the room + /// * `seed` - The seed to generate the map /// # Returns - /// * The generated room + /// * The generated map #[inline] - fn new_cave(width: u8, height: u8, order: u8, seed: felt252) -> Room { + fn new_cave(width: u8, height: u8, order: u8, seed: felt252) -> Map { let grid = Caver::generate(width, height, order, seed); - Room { width, height, grid, seed } + Map { width, height, grid, seed } } - /// Create a room with a random walk. + /// Create a map with a random walk. /// # Arguments - /// * `width` - The width of the room - /// * `height` - The height of the room + /// * `width` - The width of the map + /// * `height` - The height of the map /// * `steps` - The number of steps to walk - /// * `seed` - The seed to generate the room + /// * `seed` - The seed to generate the map /// # Returns - /// * The generated room + /// * The generated map #[inline] - fn new_random_walk(width: u8, height: u8, steps: u16, seed: felt252) -> Room { + fn new_random_walk(width: u8, height: u8, steps: u16, seed: felt252) -> Map { let grid = Walker::generate(width, height, steps, seed); - Room { width, height, grid, seed } + Map { width, height, grid, seed } } - /// Open the room with a corridor. + /// Open the map with a corridor. /// # Arguments /// * `position` - The position of the corridor /// # Returns - /// * The room with the corridor + /// * The map with the corridor #[inline] - fn open_with_corridor(ref self: Room, position: u8) { - // [Effect] Add a corridor to open the room + fn open_with_corridor(ref self: Map, position: u8) { + // [Effect] Add a corridor to open the map self.grid = Digger::corridor(self.width, self.height, position, self.grid, self.seed); } - /// Open the room with a maze. + /// Open the map with a maze. /// # Arguments /// * `position` - The position of the maze /// # Returns - /// * The room with the maze + /// * The map with the maze #[inline] - fn open_with_maze(ref self: Room, position: u8) { - // [Effect] Add a maze to open the room + fn open_with_maze(ref self: Map, position: u8) { + // [Effect] Add a maze to open the map self.grid = Digger::maze(self.width, self.height, position, self.grid, self.seed); } - /// Compute a distribution of objects in the room. + /// Compute a distribution of objects in the map. /// # Arguments /// * `count` - The number of objects to distribute /// # Returns /// * The distribution of objects #[inline] - fn compute_distribution(ref self: Room, count: u8, seed: felt252) -> felt252 { + fn compute_distribution(self: Map, count: u8, seed: felt252) -> felt252 { Spreader::generate(self.grid, self.width, self.height, count, seed) } + + /// Search a path in the map. + /// # Arguments + /// * `from` - The starting position + /// * `to` - The target position + /// # Returns + /// * The path from the target (incl.) to the start (excl.) + /// * If the path is empty, the target is not reachable + #[inline] + fn search_path(self: Map, from: u8, to: u8) -> Span { + Astar::search(self.grid, self.width, self.height, from, to) + } } #[generate_trait] impl Private of PrivateTrait { - /// Generate an empty room. + /// Generate an empty map. /// # Arguments - /// * `width` - The width of the room - /// * `height` - The height of the room + /// * `width` - The width of the map + /// * `height` - The height of the map /// # Returns - /// * The generated empty room + /// * The generated empty map #[inline] fn empty(width: u8, height: u8) -> felt252 { - // [Effect] Generate empty room + // [Effect] Generate empty map let offset: u256 = TwoPower::pow(width); let row: felt252 = ((offset - 1) / 2).try_into().unwrap() - 1; // Remove head and tail bits let offset: felt252 = offset.try_into().unwrap(); @@ -145,7 +171,7 @@ impl Private of PrivateTrait { mod tests { // Local imports - use super::{Room, RoomTrait}; + use super::{Map, MapTrait}; use origami_map::helpers::seeder::Seeder; // Constants @@ -153,7 +179,19 @@ mod tests { const SEED: felt252 = 'S33D'; #[test] - fn test_room_new() { + fn test_map_new() { + let width = 18; + let height = 14; + let grid = 0x1FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF80002; + let map: Map = MapTrait::new(grid, width, height, SEED); + assert_eq!(map.width, width); + assert_eq!(map.height, height); + assert_eq!(map.grid, grid); + assert_eq!(map.seed, SEED); + } + + #[test] + fn test_map_new_empty() { // 000000000000000000 // 011111111111111110 // 011111111111111110 @@ -170,13 +208,13 @@ mod tests { // 000000000000000010 let width = 18; let height = 14; - let mut room: Room = RoomTrait::new_empty(width, height, SEED); - room.open_with_corridor(1); - assert_eq!(room.grid, 0x1FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF80002); + let mut map: Map = MapTrait::new_empty(width, height, SEED); + map.open_with_corridor(1); + assert_eq!(map.grid, 0x1FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF9FFFE7FFF80002); } #[test] - fn test_room_maze() { + fn test_map_maze() { // 000000000000000000 // 010111011111110110 // 011101101000011100 @@ -193,13 +231,13 @@ mod tests { // 000000000000000010 let width = 18; let height = 14; - let mut room: Room = RoomTrait::new_maze(width, height, SEED); - room.open_with_corridor(1); - assert_eq!(room.grid, 0x177F676870B6EA33279B9EA59D69BAD623668F6B6673116B5C77BD80002); + let mut map: Map = MapTrait::new_maze(width, height, SEED); + map.open_with_corridor(1); + assert_eq!(map.grid, 0x177F676870B6EA33279B9EA59D69BAD623668F6B6673116B5C77BD80002); } #[test] - fn test_room_cave() { + fn test_map_cave() { // 000000000000000000 // 001100001100000000 // 011111001100000000 @@ -218,13 +256,13 @@ mod tests { let height = 14; let order = 3; let seed: felt252 = Seeder::shuffle(SEED, SEED); - let mut room: Room = RoomTrait::new_cave(width, height, order, seed); - room.open_with_corridor(1); - assert_eq!(room.grid, 0xC3007CC01F1867E719F8C07E001FC007FC01FFC07FF98FFFE3FFF80002); + let mut map: Map = MapTrait::new_cave(width, height, order, seed); + map.open_with_corridor(1); + assert_eq!(map.grid, 0xC3007CC01F1867E719F8C07E001FC007FC01FFC07FF98FFFE3FFF80002); } #[test] - fn test_room_random_walk() { + fn test_map_random_walk() { // 010000000000000000 // 010000000011000000 // 011000000111001100 @@ -242,13 +280,13 @@ mod tests { let width = 18; let height = 14; let steps: u16 = 2 * width.into() * height.into(); - let mut room: Room = RoomTrait::new_random_walk(width, height, steps, SEED); - room.open_with_maze(250); - assert_eq!(room.grid, 0x4000100C060730D1FE6E3F8FFFE69FF8A77E6FFF93FFE4FFF9BFE037F800000); + let mut map: Map = MapTrait::new_random_walk(width, height, steps, SEED); + map.open_with_maze(250); + assert_eq!(map.grid, 0x4000100C060730D1FE6E3F8FFFE69FF8A77E6FFF93FFE4FFF9BFE037F800000); } #[test] - fn test_room_compute_distribution() { + fn test_map_compute_distribution() { // 000000000000000000 // 000000000011000000 // 000000000111001100 @@ -281,8 +319,32 @@ mod tests { let width = 18; let height = 14; let steps: u16 = 2 * width.into() * height.into(); - let mut room: Room = RoomTrait::new_random_walk(width, height, steps, SEED); - let distribution = room.compute_distribution(10, SEED); + let mut map: Map = MapTrait::new_random_walk(width, height, steps, SEED); + let distribution = map.compute_distribution(10, SEED); assert_eq!(distribution, 0x8021002008000000000001200100000000420000000000); } + + #[test] + fn test_map_search_path() { + // 000000000000000000 + // 000000000011000000 + // 000000000111001100 + // 000001000111111110 + // 000011100011111110 + // 000011111111111110 + // 0000100111x─┐11110 + // 000010011101│11110 + // 000011111111│11110 + // 000011111111│11110 + // 000011111111│11110 + // 000011111111│00000 + // 000001111111x00000 + // 000000000000000000 + let width = 18; + let height = 14; + let steps: u16 = 2 * width.into() * height.into(); + let mut map: Map = MapTrait::new_random_walk(width, height, steps, SEED); + let path = map.search_path(23, 133); + assert_eq!(path, array![133, 132, 131, 113, 95, 77, 59, 41].span()); + } } diff --git a/crates/map/src/types/direction.cairo b/crates/map/src/types/direction.cairo index 6648fcb..d6faf2a 100644 --- a/crates/map/src/types/direction.cairo +++ b/crates/map/src/types/direction.cairo @@ -29,8 +29,6 @@ pub impl DirectionImpl of DirectionTrait { /// * `seed` - The seed to generate the shuffled directions /// # Returns /// * The shuffled directions - /// # Info - /// * 0: North, 1: East, 2: South, 3: West #[inline] fn compute_shuffled_directions(seed: felt252) -> u32 { // [Compute] Random number @@ -69,6 +67,8 @@ pub impl DirectionImpl of DirectionTrait { /// * `directions` - The packed directions /// # Returns /// * The next direction + /// # Effects + /// * The packed directions is updated #[inline] fn pop_front(ref directions: u32) -> Direction { let direciton: u8 = (directions % DIRECTION_SIZE).try_into().unwrap(); diff --git a/crates/map/src/types/node.cairo b/crates/map/src/types/node.cairo new file mode 100644 index 0000000..99fec5e --- /dev/null +++ b/crates/map/src/types/node.cairo @@ -0,0 +1,74 @@ +// Internal imports + +use origami_map::helpers::heap::ItemTrait; + +// Types. +#[derive(Copy, Drop)] +pub struct Node { + pub position: u8, + pub source: u8, + pub gcost: u16, + pub hcost: u16, +} + +/// Implementations. +#[generate_trait] +pub impl NodeImpl of NodeTrait { + #[inline] + fn new(position: u8, source: u8, gcost: u16, hcost: u16) -> Node { + Node { position, source, gcost, hcost } + } +} + +pub impl ItemImpl of ItemTrait { + #[inline] + fn key(self: Node) -> u8 { + self.position + } +} + +pub impl NodePartialEq of PartialEq { + #[inline] + fn eq(lhs: @Node, rhs: @Node) -> bool { + lhs.position == rhs.position + } + + #[inline] + fn ne(lhs: @Node, rhs: @Node) -> bool { + lhs.position != rhs.position + } +} + +pub impl NodePartialOrd of PartialOrd { + #[inline] + fn lt(lhs: Node, rhs: Node) -> bool { + if lhs.gcost + lhs.hcost == rhs.gcost + rhs.hcost { + return lhs.hcost < rhs.hcost; + } + lhs.gcost + lhs.hcost < rhs.gcost + rhs.hcost + } + + #[inline] + fn le(lhs: Node, rhs: Node) -> bool { + if lhs.gcost + lhs.hcost == rhs.gcost + rhs.hcost { + return lhs.hcost <= rhs.hcost; + } + lhs.gcost + lhs.hcost <= rhs.gcost + rhs.hcost + } + + #[inline] + fn gt(lhs: Node, rhs: Node) -> bool { + if lhs.gcost + lhs.hcost == rhs.gcost + rhs.hcost { + return lhs.hcost > rhs.hcost; + } + lhs.gcost + lhs.hcost > rhs.gcost + rhs.hcost + } + + #[inline] + fn ge(lhs: Node, rhs: Node) -> bool { + if lhs.gcost + lhs.hcost == rhs.gcost + rhs.hcost { + return lhs.hcost >= rhs.hcost; + } + lhs.gcost + lhs.hcost >= rhs.gcost + rhs.hcost + } +}