diff --git a/Scarb.lock b/Scarb.lock index 326ddc5e..c9dc96f1 100644 --- a/Scarb.lock +++ b/Scarb.lock @@ -33,6 +33,15 @@ dependencies = [ "dojo", ] +[[package]] +name = "grid_map" +version = "0.0.0" +dependencies = [ + "cubit", + "dojo", + "origami", +] + [[package]] name = "hex_map" version = "0.0.0" diff --git a/Scarb.toml b/Scarb.toml index 54e06304..d1b54433 100644 --- a/Scarb.toml +++ b/Scarb.toml @@ -3,6 +3,7 @@ members = [ "crates", "examples/chess", "examples/hex_map", + "examples/grid_map", "examples/market", "examples/matchmaker", "examples/projectile", diff --git a/crates/src/lib.cairo b/crates/src/lib.cairo index a0719f2c..05f6f8d4 100644 --- a/crates/src/lib.cairo +++ b/crates/src/lib.cairo @@ -31,4 +31,8 @@ mod map { mod hex; mod types; } + mod grid { + mod grid; + mod types; + } } diff --git a/crates/src/map/grid/grid.cairo b/crates/src/map/grid/grid.cairo new file mode 100644 index 00000000..88dcfe02 --- /dev/null +++ b/crates/src/map/grid/grid.cairo @@ -0,0 +1,174 @@ +use core::array::ArrayTrait; +use origami::map::grid::{types::{GridTile, Direction, DirectionIntoFelt252}}; + +trait IGridTile { + fn new(col: u32, row: u32) -> GridTile; + fn neighbor(self: GridTile, direction: Direction) -> GridTile; + fn neighbors(self: GridTile) -> Array; + fn is_neighbor(self: GridTile, other: GridTile) -> bool; + fn tiles_within_range(self: GridTile, range: u32) -> Array; +} + +impl ImplGridTile of IGridTile { + fn new(col: u32, row: u32) -> GridTile { + GridTile { col, row } + } + + fn neighbor(self: GridTile, direction: Direction) -> GridTile { + match direction { + Direction::East(()) => GridTile { col: self.col + 1, row: self.row }, + Direction::North(()) => GridTile { col: self.col, row: self.row - 1 }, + Direction::West(()) => GridTile { col: self.col - 1, row: self.row }, + Direction::South(()) => GridTile { col: self.col, row: self.row + 1 }, + } + } + + fn neighbors(self: GridTile) -> Array { + return array![ + self.neighbor(Direction::East(())), + self.neighbor(Direction::North(())), + self.neighbor(Direction::West(())), + self.neighbor(Direction::South(())) + ]; + } + + fn is_neighbor(self: GridTile, other: GridTile) -> bool { + let mut neighbors = self.neighbors(); + + loop { + if (neighbors.len() == 0) { + break false; + } + + let curent_neighbor = neighbors.pop_front().unwrap(); + + if (curent_neighbor.col == other.col) { + if (curent_neighbor.row == other.row) { + break true; + } + }; + } + } + + fn tiles_within_range(self: GridTile, range: u32) -> Array { + let mut queue = array![self]; + let mut visited = array![self]; + let mut moves = 0; + + loop { + if moves == range { + break; + } + let mut next_queue = array![]; + loop { + if queue.len() == 0 { + break; + } + let tile = queue.pop_front().unwrap(); + let mut neighbors = tile.neighbors(); + + loop { + if neighbors.len() == 0 { + break; + } + let neighbor = neighbors.pop_front().unwrap(); + let mut is_visited = false; + let mut index = 0; + let visited_span = visited.span(); + + loop { + if index == visited_span.len() || is_visited == true { + break; + } + let curr = *visited_span.at(index); + if (curr.col == neighbor.col && curr.row == neighbor.row) { + is_visited = true; + } + index = index + 1; + }; + if !is_visited { + next_queue.append(neighbor); + visited.append(neighbor); + } + }; + }; + queue = next_queue.clone(); + moves = moves + 1; + }; + return visited; + } +} + + +// tests ----------------------------------------------------------------------- // + +#[cfg(test)] +mod tests { + use super::{IGridTile, ImplGridTile, Direction, GridTile}; + #[test] + fn test_row_col() { + let mut grid_tile = ImplGridTile::new(5, 5); + + assert(grid_tile.col == 5, 'col should be 5'); + assert(grid_tile.row == 5, 'row should be 5'); + } + + + #[test] + fn test_grid_tile_neighbors() { + let mut grid_tile = ImplGridTile::new(5, 5); + + let east_neighbor = grid_tile.neighbor(Direction::East(())); + + assert(east_neighbor.col == 6, 'col should be 6'); + assert(east_neighbor.row == 5, 'row should be 5'); + + let north_neighbor = grid_tile.neighbor(Direction::North(())); + + assert(north_neighbor.col == 5, 'col should be 5'); + assert(north_neighbor.row == 4, 'row should be 4'); + + let west_neighbor = grid_tile.neighbor(Direction::West(())); + + assert(west_neighbor.col == 4, 'col should be 4'); + assert(west_neighbor.row == 5, 'row should be 5'); + + let south_neighbor = grid_tile.neighbor(Direction::South(())); + + assert(south_neighbor.col == 5, 'col should be 4'); + assert(south_neighbor.row == 6, 'row should be 6'); + } + + #[test] + fn test_is_neighbor() { + let mut grid_tile = ImplGridTile::new(5, 5); + + assert( + grid_tile.is_neighbor(GridTile { col: grid_tile.col + 1, row: grid_tile.row }), 'east' + ); + + assert( + grid_tile.is_neighbor(GridTile { col: grid_tile.col, row: grid_tile.row + 1 }), 'south' + ); + + assert( + grid_tile.is_neighbor(GridTile { col: grid_tile.col, row: grid_tile.row - 1 }), 'north' + ); + + assert( + grid_tile.is_neighbor(GridTile { col: grid_tile.col - 1, row: grid_tile.row }), 'west' + ); + } + + #[test] + fn test_tiles_within_range() { + let grid_tile = ImplGridTile::new(5, 5); + let tiles_range_one = grid_tile.tiles_within_range(1); + let tiles_range_two = grid_tile.tiles_within_range(2); + let tiles_range_three = grid_tile.tiles_within_range(3); + // Including the center tile + assert_eq!(tiles_range_one.len(), 5, "should be 5"); + assert_eq!(tiles_range_two.len(), 13, "should be 13"); + assert_eq!(tiles_range_three.len(), 25, "should be 25"); + } +} diff --git a/crates/src/map/grid/types.cairo b/crates/src/map/grid/types.cairo new file mode 100644 index 00000000..9380a728 --- /dev/null +++ b/crates/src/map/grid/types.cairo @@ -0,0 +1,24 @@ +#[derive(Copy, Drop, Serde, Introspect)] +struct GridTile { + col: u32, + row: u32, +} + +#[derive(Drop, Copy, Serde)] +enum Direction { + East: (), + North: (), + West: (), + South: (), +} + +impl DirectionIntoFelt252 of Into { + fn into(self: Direction) -> felt252 { + match self { + Direction::East => 0, + Direction::North => 1, + Direction::West => 2, + Direction::South => 3, + } + } +} diff --git a/crates/src/map/hex/hex.cairo b/crates/src/map/hex/hex.cairo index 6e74f543..7bc1261c 100644 --- a/crates/src/map/hex/hex.cairo +++ b/crates/src/map/hex/hex.cairo @@ -150,7 +150,7 @@ mod tests { let east_neighbor = hex_tile.neighbor(Direction::East(())); - assert(east_neighbor.col == 6, 'col should be 7'); + assert(east_neighbor.col == 6, 'col should be 6'); assert(east_neighbor.row == 5, 'row should be 5'); let north_east_neighbor = hex_tile.neighbor(Direction::NorthEast(())); diff --git a/examples/grid_map/Scarb.toml b/examples/grid_map/Scarb.toml new file mode 100644 index 00000000..db51839f --- /dev/null +++ b/examples/grid_map/Scarb.toml @@ -0,0 +1,10 @@ +[package] +name = "grid_map" +version = "0.0.0" +description = "Example Grid Map using Arrays" +homepage = "https://github.com/dojoengine/origami/tree/examples/grid_map" + +[dependencies] +cubit.workspace = true +dojo.workspace = true +origami.workspace = true diff --git a/examples/grid_map/readme.md b/examples/grid_map/readme.md new file mode 100644 index 00000000..55912957 --- /dev/null +++ b/examples/grid_map/readme.md @@ -0,0 +1,2 @@ +## Grid map using Array + diff --git a/examples/grid_map/src/actions.cairo b/examples/grid_map/src/actions.cairo new file mode 100644 index 00000000..5d54c7b0 --- /dev/null +++ b/examples/grid_map/src/actions.cairo @@ -0,0 +1,168 @@ +// internal imports +use origami::map::grid::{types::Direction}; +use grid_map::models::Position; + +// define the interface +#[dojo::interface] +trait IActions { + fn init_map(game_id: u32, max_x: u32, max_y: u32); + fn spawn(game_id: u32, x: u32, y: u32); + fn move(game_id: u32, direction: Direction); +} + +#[dojo::interface] +trait IActionsComputed { + fn next_position(position: Position, direction: Direction) -> Position; +} + +// dojo decorator +#[dojo::contract] +mod actions { + use starknet::{ContractAddress, get_caller_address}; + use origami::map::grid::{grid::{IGridTile}, types::{Direction, DirectionIntoFelt252}}; + + use grid_map::models::{Position, Vec2, Map}; + + use super::{IActions, IActionsComputed}; + + // declaring custom event struct + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Moved: Moved, + Spawned: Spawned + } + + // declaring custom event struct + #[derive(Drop, starknet::Event)] + struct Moved { + player: ContractAddress, + direction: Direction + } + + #[derive(Drop, starknet::Event)] + struct Spawned { + player: ContractAddress, + vec: Vec2 + } + + #[abi(embed_v0)] + impl ActionsComputedImpl of IActionsComputed { + #[computed(Position)] + fn next_position(position: Position, direction: Direction) -> Position { + let mut new_position = position; + + // convert to Grid + let grid_tile = IGridTile::new(position.vec.x, position.vec.y); + + // get next tile + let next_grid = grid_tile.neighbor(direction); + + // convert back to Position + new_position.vec = Vec2 { x: next_grid.col, y: next_grid.row }; + + new_position + } + } + + #[abi(embed_v0)] + impl ActionsImpl of IActions { + // Init the map + fn init_map(game_id: u32, max_x: u32, max_y: u32) { + // Access the world dispatcher for reading. + let world = self.world_dispatcher.read(); + + let player = get_caller_address(); + + let mut players: Array = ArrayTrait::new(); + + let map = Map { game_id, max_x, max_y, players, }; + + // Update the world state with the new moves data and position. + set!(world, (map)); + } + + // ContractState is defined by system decorator expansion + fn spawn(game_id: u32, x: u32, y: u32) { + // Access the world dispatcher for reading. + let world = self.world_dispatcher.read(); + let player = get_caller_address(); + + // Predefined position for later use + let player_position = Position { player, vec: Vec2 { x, y }, }; + + let mut map = get!(world, game_id, (Map)); + assert!(map.max_x >= x, "Position x is out of bounds"); + assert!(map.max_y >= y, "Position y is out of bounds"); + let mut players = map.players; + if players.len() == 0 { + players.append(player_position); + } else { + let player_snapshot = @players; + let mut i = 0; + while i < players.len() { + let current_player: Position = *player_snapshot[i]; + assert!(current_player.player != player, "Player already exists"); + assert!(current_player.vec != player_position.vec, "Position already taken"); + if i == players.len() - 1 { + players.append(player_position); + } + i += 1; + } + } + + map.players = players; + set!(world, (player_position)); + set!(world, (new_map)); + emit!(world, (Event::Spawned(Spawned { player, vec: player_position.vec }))); + } + + // Moves player in the provided direction. + fn move(game_id: u32, direction: Direction) { + let world = self.world_dispatcher.read(); + let player = get_caller_address(); + let current_position = get!(world, player, (Position)); + let new_vec: Vec2 = { + match direction { + Direction::East => Vec2 { + x: current_position.vec.x + 1, y: current_position.vec.y + }, + Direction::North => Vec2 { + x: current_position.vec.x, y: current_position.vec.y - 1 + }, + Direction::West => Vec2 { + x: current_position.vec.x - 1, y: current_position.vec.y + }, + Direction::South => Vec2 { + x: current_position.vec.x, y: current_position.vec.y + 1 + }, + } + }; + + // Predefined position for later use + let player_position = Position { player, vec: new_vec }; + let mut new_players = array![player_position]; + + let mut map = get!(world, game_id, (Map)); + let mut players = map.players; + assert!(players.len() > 0, "No players in the game"); + let player_snapshot = @players; + let mut i = 0; + while i < players.len() { + let current_player: Position = *player_snapshot[i]; + assert!(current_player.vec != new_vec, "Position already taken"); + // when the code reach this line, it means that the new position is valid + new_players.append(current_player); + i += 1; + }; + + map.players = new_players; + set!(world, (map)); + delete!(world, (current_position)); + set!(world, (player_position)); + emit!(world, (Event::Spawned(Spawned { player, vec: player_position.vec }))); + } + } +} + +// will write test later \ No newline at end of file diff --git a/examples/grid_map/src/lib.cairo b/examples/grid_map/src/lib.cairo new file mode 100644 index 00000000..8cade2dd --- /dev/null +++ b/examples/grid_map/src/lib.cairo @@ -0,0 +1,2 @@ +mod models; +mod actions; diff --git a/examples/grid_map/src/models.cairo b/examples/grid_map/src/models.cairo new file mode 100644 index 00000000..a146f5ca --- /dev/null +++ b/examples/grid_map/src/models.cairo @@ -0,0 +1,65 @@ +use starknet::ContractAddress; +use origami::map::grid::{ + grid::{GridTile}, + types::{Direction} +}; + +#[derive(Copy, Drop, Serde, Introspect, PartialEq)] +struct Vec2 { + x: u32, + y: u32 +} + +#[dojo::model] +#[derive(Copy, Drop, Serde, Introspect, PartialEq)] +struct Position { + #[key] + player: ContractAddress, + vec: Vec2, +} + +#[dojo::model] +#[derive(Drop, Serde)] +struct Map { + #[key] + game_id: u32, + max_x: u32, + max_y: u32, + players: Array, +} + + +trait Vec2Trait { + fn is_zero(self: Vec2) -> bool; + fn is_equal(self: Vec2, b: Vec2) -> bool; +} + +impl Vec2Impl of Vec2Trait { + fn is_zero(self: Vec2) -> bool { + if self.x - self.y == 0 { + return true; + } + false + } + + fn is_equal(self: Vec2, b: Vec2) -> bool { + self.x == b.x && self.y == b.y + } +} +// #[cfg(test)] +// mod tests { +// use super::{Position, Vec2, Vec2Trait}; + +// #[test] +// fn test_vec_is_zero() { +// assert(Vec2Trait::is_zero(Vec2 { x: 0, y: 0 }), 'not zero'); +// } + +// #[test] +// fn test_vec_is_equal() { +// let position = Vec2 { x: 420, y: 0 }; +// assert(position.is_equal(Vec2 { x: 420, y: 0 }), 'not equal'); +// } +// } + +