diff --git a/src/SUMMARY.md b/src/SUMMARY.md index 5802d4be..717591dd 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -80,9 +80,8 @@ - [Onchain Chess](./tutorial/onchain-chess/README.md) - [0. Setup](./tutorial/onchain-chess/0-setup.md) - [1. Initiate](./tutorial/onchain-chess/1-action.md) - - [2. Check Legal Move](./tutorial/onchain-chess/2-legal.md) - - [3. Move](./tutorial/onchain-chess/3-move.md) - - [4. Test Chess](./tutorial/onchain-chess/4-test.md) + - [2. Move](./tutorial/onchain-chess/2-move.md) + - [3. Test Chess](./tutorial/onchain-chess/3-test.md) - [Deploy using Slot](./tutorial/deploy-using-slot/main.md) --- diff --git a/src/tutorial/onchain-chess/0-setup.md b/src/tutorial/onchain-chess/0-setup.md index e8e40407..8e16981b 100644 --- a/src/tutorial/onchain-chess/0-setup.md +++ b/src/tutorial/onchain-chess/0-setup.md @@ -7,13 +7,13 @@ _Before starting recommend following the [`hello-dojo`](../../cairo/hello-dojo.m Create a new Dojo project folder. You can name your project what you want. ```sh -mkdir dojo-chess +mkdir chess ``` Open the project folder. ```sh -cd dojo-chess +cd chess ``` And initialize the project using sozo init. @@ -24,30 +24,61 @@ sozo init ## Cleaning Up the Boilerplate -The project comes with a lot of boilerplate codes. Clear it all. Make sure both `actions.cairo`, `models.cairo` and `utils.cairo` files are empty. Then create a new empty `tests.cairo` file in your `/src` directory. +The project comes with a lot of boilerplate codes. Clear it all. Make sure your directory looks like this + +```shell +├── README.md +├── Scarb.toml +└── src + ├── actions.cairo + ├── lib.cairo + ├── models + │ ├── game.cairo + │ ├── piece.cairo + │ └── player.cairo + ├── models.cairo + ├── tests + │ ├── integration.cairo + │ └── units.cairo + └── tests.cairo +``` -Remodel your`lib.cairo`, to look like this : +Remodel your `lib.cairo`, to look like this : ```rust,ignore mod actions; mod models; -mod utils; mod tests; ``` +Remodel your `models.cairo`, to look like this : + +```rust,ignore +mod game; +mod piece; +mod player; +``` + +Remodel your `tests.cairo`, to look like this : + +```rust,ignore +mod integration; +mod units; +``` + Make sure your `Scarb.toml` looks like this: ```toml [package] -cairo-version = "2.3.0" -name = "dojo_chess" -version = "0.3.15" +cairo-version = "2.4.0" +name = "chess" +version = "0.4.0" [cairo] sierra-replace-ids = true [dependencies] -dojo = { git = "https://github.com/dojoengine/dojo", version = "0.3.15" } +dojo = { git = "https://github.com/dojoengine/dojo", version = "0.4.2" } [[target.dojo]] @@ -72,37 +103,84 @@ sozo build While there are many ways to design a chess game using the ECS model, we'll follow this approach: -> Every square of the chess board (e.g., A1) will be treated as an entity. If a piece exists on a square, the square entity will hold that piece. +> Every square of the chess board (e.g., A1) will be treated as an entity. If a piece exists on a square position, that position will hold that piece. -First, add this basic model to `models.cairo` file. If you are not familar with model syntax in Dojo engine, go back to this [chapter](../../cairo/models.md). +First, add this basic `player` model to `models/player.cairo` file. If you are not familar with model syntax in Dojo engine, go back to this [chapter](../../cairo/models.md). ```rust,ignore +use starknet::ContractAddress; + #[derive(Model, Drop, Serde)] -struct Square { +struct Player { #[key] game_id: u32, #[key] - x: u32, + address: ContractAddress, + color: Color +} + +#[derive(Serde, Drop, Copy, PartialEq, Introspect)] +enum Color { + White, + Black, + None, +} +``` + +Second, we do the same for `game` model. Edit your `models/player.cairo` file and add this content. + +```rust,ignore +use chess::models::player::Color; +use starknet::ContractAddress; + +#[derive(Model, Drop, Serde)] +struct Game { + #[key] + game_id: u32, + winner: Color, + white: ContractAddress, + black: ContractAddress +} + +#[derive(Model, Drop, Serde)] +struct GameTurn { + #[key] + game_id: u32, + player_color: Color +} +``` + +Lastly we create `piece` model in our `models/player.cairo` file. + +```rust,ignore +use chess::models::player::Color; +use starknet::ContractAddress; + +#[derive(Model, Drop, Serde)] +struct Piece { + #[key] + game_id: u32, #[key] - y: u32, - piece: PieceType, + position: Vec2, + color: Color, + piece_type: PieceType, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct Vec2 { + x: u32, + y: u32 } #[derive(Serde, Drop, Copy, PartialEq, Introspect)] enum PieceType { - WhitePawn: (), - WhiteKnight: (), - WhiteBishop: (), - WhiteRook: (), - WhiteQueen: (), - WhiteKing: (), - BlackPawn: (), - BlackKnight: (), - BlackBishop: (), - BlackRook: (), - BlackQueen: (), - BlackKing: (), - None: (), + Pawn, + Knight, + Bishop, + Rook, + Queen, + King, + None, } ``` @@ -124,82 +202,133 @@ It should be noted that Systems function are contract methods, by implication, r Now try `sozo build` to build. -Complied? Great! then let's move on. If not fix try issues, so that you can run the `sozo build` command successfully. +Complied? Great! then let's move on. If not fix the issues, so that you can run the `sozo build` command successfully. -## Add more models +## Implement Traits for models -Before you move on, add more models so we can use them in the next chapter when creating the action contract. +Before you move on, implement traits for models so we can use them in the next chapter when creating the action contract. ### Requirements -- `Color` enum with values: White,Black & None +Firt we have to define the following traits for `Game`, `Player`, `Piece` models respectively. + +```rust,ignore +trait GameTurnTrait { + fn next_turn(self: @GameTurn) -> Color; +} -- `Game` model with fields: game_id, winner, white, black. +trait PlayerTrait { +fn is_not_my_piece(self: @Player, piece_color: Color) -> bool; +} -- `GameTurn` model with fields: game_id, turn. +trait PieceTrait { +fn is_out_of_board(next_position: Vec2) -> bool; +fn is_right_piece_move(self: @Piece, next_position: Vec2) -> bool; +} +``` Try to implement this code by yourself, Otherwise
Click to see full `models.cairo` code -```rust,ignore -use starknet::ContractAddress; - -#[derive(Model, Drop, Serde)] -struct Square { - #[key] - game_id: u32, - #[key] - x: u32, - #[key] - y: u32, - piece: PieceType, +```c +// code for player.cairo file +trait PlayerTrait { + fn is_not_my_piece(self: @Player, piece_color: Color) -> bool; } -#[derive(Serde, Drop, Copy, PartialEq, Introspect)] -enum PieceType { - WhitePawn: (), - WhiteKnight: (), - WhiteBishop: (), - WhiteRook: (), - WhiteQueen: (), - WhiteKing: (), - BlackPawn: (), - BlackKnight: (), - BlackBishop: (), - BlackRook: (), - BlackQueen: (), - BlackKing: (), - None: (), +impl PalyerImpl of PlayerTrait { + fn is_not_my_piece(self: @Player, piece_color: Color) -> bool { + *self.color != piece_color + } } -#[derive(Serde, Drop, Copy, PartialEq, Introspect)] -enum Color { - White: (), - Black: (), - None: (), +// code for game.cairo file +trait GameTurnTrait { + fn next_turn(self: @GameTurn) -> Color; +} +impl GameTurnImpl of GameTurnTrait { + fn next_turn(self: @GameTurn) -> Color { + match self.player_color { + Color::White => Color::Black, + Color::Black => Color::White, + Color::None => panic(array!['Illegal turn']) + } + } } -#[derive(Model, Drop, Serde)] -struct Game { - #[key] - game_id: u32, - winner: Color, - white: ContractAddress, - black: ContractAddress +// code for piece.cairo file +trait PieceTrait { + fn is_out_of_board(next_position: Vec2) -> bool; + fn is_right_piece_move(self: @Piece, next_position: Vec2) -> bool; } -#[derive(Model, Drop, Serde)] -struct GameTurn { - #[key] - game_id: u32, - turn: Color +impl PieceImpl of PieceTrait { + fn is_out_of_board(next_position: Vec2) -> bool { + next_position.x > 7 || next_position.y > 7 + } + + fn is_right_piece_move(self: @Piece, next_position: Vec2) -> bool { + let n_x = next_position.x; + let n_y = next_position.y; + assert(!(n_x == *self.position.x && n_y == *self.position.y), 'Cannot move same position'); + match self.piece_type { + PieceType::Pawn => { + match self.color { + Color::White => { + (n_x == *self.position.x && n_y == *self.position.y + 1) + || (n_x == *self.position.x && n_y == *self.position.y + 2) + || (n_x == *self.position.x + 1 && n_y == *self.position.y + 1) + || (n_x == *self.position.x - 1 && n_y == *self.position.y + 1) + }, + Color::Black => { + (n_x == *self.position.x && n_y == *self.position.y - 1) + || (n_x == *self.position.x && n_y == *self.position.y - 2) + || (n_x == *self.position.x + 1 && n_y == *self.position.y - 1) + || (n_x == *self.position.x - 1 && n_y == *self.position.y - 1) + }, + Color::None => panic(array!['Should not move empty piece']), + } + }, + PieceType::Knight => { n_x == *self.position.x + 2 && n_y == *self.position.y + 1 }, + PieceType::Bishop => { + (n_x <= *self.position.x && n_y <= *self.position.y && *self.position.y + - n_y == *self.position.x + - n_x) + || (n_x <= *self.position.x && n_y >= *self.position.y && *self.position.y + - n_y == *self.position.x + - n_x) + || (n_x >= *self.position.x && n_y <= *self.position.y && *self.position.y + - n_y == *self.position.x + - n_x) + || (n_x >= *self.position.x && n_y >= *self.position.y && *self.position.y + - n_y == *self.position.x + - n_x) + }, + PieceType::Rook => { + (n_x == *self.position.x || n_y != *self.position.y) + || (n_x != *self.position.x || n_y == *self.position.y) + }, + PieceType::Queen => { + (n_x == *self.position.x || n_y != *self.position.y) + || (n_x != *self.position.x || n_y == *self.position.y) + || (n_x != *self.position.x || n_y != *self.position.y) + }, + PieceType::King => { + (n_x <= *self.position.x + 1 && n_y <= *self.position.y + 1) + || (n_x <= *self.position.x + 1 && n_y <= *self.position.y - 1) + || (n_x <= *self.position.x - 1 && n_y <= *self.position.y + 1) + || (n_x <= *self.position.x - 1 && n_y <= *self.position.y - 1) + }, + PieceType::None => panic(array!['Should not move empty piece']), + } + } } ```
-This tutorial is extracted from [here](https://github.com/dojoengine/origami/tree/main/dojo-chess) +This tutorial is extracted from [here](https://github.com/dojoengine/origami/tree/main/examples/chess) Congratulations! You've completed the basic setup for building an on-chain chess game 🎉 diff --git a/src/tutorial/onchain-chess/1-action.md b/src/tutorial/onchain-chess/1-action.md index 9ee96eb3..2b78107e 100644 --- a/src/tutorial/onchain-chess/1-action.md +++ b/src/tutorial/onchain-chess/1-action.md @@ -4,7 +4,7 @@ This chapter will address implementing `actions.cairo`, which spawns the game & ## What is `actions` contract? -To play chess, you need, to start game, spawn the pieces, and move around the board. the `actions` contract has two dominant functions `spawn` function which spawns the game entity, places each piece in its proper position on the board and returns the game_id, and the `move` funtion which allows pieces to be moved around the board. +To play chess, you need, to start game, spawn the pieces, and move around the board. The `actions` contract has two dominant functions `spawn` function which spawns the game entity, places each piece in its proper position on the board and returns the game_id, and the `move` funtion which allows pieces to be moved around the board.

image @@ -15,13 +15,13 @@ To play chess, you need, to start game, spawn the pieces, and move around the bo ```rust,ignore use starknet::ContractAddress; - + use chess::models::piece::Vec2; #[starknet::interface] trait IActions { fn move( self: @ContractState, - curr_position: (u32, u32), - next_position: (u32, u32), + curr_position: Vec2, + next_position: Vec2, caller: ContractAddress, //player game_id: u32 ); @@ -36,15 +36,16 @@ To play chess, you need, to start game, spawn the pieces, and move around the bo ```rust,ignore #[dojo::contract] mod actions { - use dojo_chess::models::{Color, Square, PieceType, Game, GameTurn}; - use super::{ContractAddress, IActions}; - use dojo_chess::utils::{is_out_of_board, is_right_piece_move, is_piece_is_mine}; + use chess::models::player::{Player, Color, PlayerTrait}; + use chess::models::piece::{Piece, PieceType, PieceTrait}; + use chess::models::game::{Game, GameTurn, GameTurnTrait}; + use super::{ContractAddress, IActions, Vec2}; } ``` Should be noted that `actions` is the contract name. -3. Write a `spawn` function that accepts the `white address`, and `black address` as input and set necessary states using `set!(...)`.Implement the game entity, comprised of the `Game` model and `GameTurn` model we created in the `models.cairo` and Implement the square entities from a1 to h8 containing the correct `PieceType` in the `spawn` fn. +3. Write a `spawn` function that accepts the `white address`, and `black address` as input and set necessary states using `set!(...)`. Implement the `player` entity from player model. Implement the game entity, comprised of the `Game` model and `GameTurn` model we created in the `game.cairo` and implement the piece entities from a1 to h8 containing the correct `PieceType` in the `spawn` fn. ```rust,ignore #[external(v0)] @@ -54,35 +55,105 @@ Should be noted that `actions` is the contract name. ) -> u32 { let world = self.world_dispatcher.read(); let game_id = world.uuid(); + + // set Players + set!( + world, + ( + Player { game_id, address: black_address, color: Color::Black }, + Player { game_id, address: white_address, color: Color::White }, + ) + ); + + // set Game and GameTurn set!( world, ( Game { - game_id, - winner: Color::None(()), - white: white_address, - black: black_address + game_id, winner: Color::None, white: white_address, black: black_address }, - GameTurn { game_id, turn: Color::White(())}, + GameTurn { game_id, player_color: Color::White }, ) ); - set!(world, (Square { game_id, x: 0, y: 0, piece: PieceType::WhiteRook })); - set!(world, (Square { game_id, x: 0, y: 1, piece: PieceType::WhitePawn })); - set!(world, (Square { game_id, x: 1, y: 6, piece: PieceType::BlackPawn })); - set!(world, (Square { game_id, x: 1, y: 0, piece: PieceType::WhiteKnight })); + // set Pieces + set!( + world, + (Piece { + game_id, + color: Color::White, + position: Vec2 { x: 0, y: 0 }, + piece_type: PieceType::Rook + }) + ); + set!( + world, + (Piece { + game_id, + color: Color::White, + position: Vec2 { x: 0, y: 1 }, + piece_type: PieceType::Pawn + }) + ); + set!( + world, + (Piece { + game_id, + color: Color::Black, + position: Vec2 { x: 1, y: 6 }, + piece_type: PieceType::Pawn + }) + ); + set!( + world, + (Piece { + game_id, + color: Color::White, + position: Vec2 { x: 1, y: 0 }, + piece_type: PieceType::Knight + }) + ); + set!( + world, + (Piece { + game_id, + color: Color::None, + position: Vec2 { x: 0, y: 2 }, + piece_type: PieceType::None + }) + ); + + set!( + world, + (Piece { + game_id, + color: Color::None, + position: Vec2 { x: 0, y: 3 }, + piece_type: PieceType::None + }) + ); + set!( + world, + (Piece { + game_id, + color: Color::None, + position: Vec2 { x: 1, y: 4 }, + piece_type: PieceType::None + }) + ); //the rest of the positions on the board goes here.... game_id } - fn move(self: @ContractState, curr_position: (u32, u32), - next_position: (u32, u32), caller: ContractAddress, + fn move( + self: @ContractState, + curr_position: Vec2, + next_position: Vec2, + caller: ContractAddress, //player game_id: u32 - ) { + ) { // Upcoming code } } ``` - -Before we implement the `move()` function, we are going to setup some helper function in our `utils.cairo` file diff --git a/src/tutorial/onchain-chess/2-legal.md b/src/tutorial/onchain-chess/2-legal.md deleted file mode 100644 index b2136ffb..00000000 --- a/src/tutorial/onchain-chess/2-legal.md +++ /dev/null @@ -1,110 +0,0 @@ -# 2. Utils and Legal Moves - -In order to keep our code has dry as possible, we decide to modularize some functions. Those functions are the ones we import from `utils.cairo` into `actions.cairo`. - -```rust,ignore - use dojo_chess::utils::{is_out_of_board, is_right_piece_move, is_piece_is_mine}; -``` - -This functions will check: - -- If the next move goes outside the board. -- If there's a piece that can be captured. -- If the next move is allowed for the type of piece. -- If the user can allow to make a action (based on the piece's color). -- ... You can also add other custom check functions. - -## Make Check Functions - -We need to add some check functions in `actions` contract. These will help make sure the next move is allowed. Fill the following code in your `utils.cairo` file - -1. See if player is moving the right piece - -```rust,ignore - use dojo_chess::models::PieceType; - use starknet::ContractAddress; - fn is_piece_is_mine(maybe_piece: PieceType) -> bool { - false - } -``` - -2. See if the next spot is still on the board. - -```rust,ignore - fn is_out_of_board(next_position: (u32, u32)) -> bool { - let (n_x, n_y) = next_position; - if n_x > 7 || n_x < 0 { - return false; - } - if n_y > 7 || n_y < 0 { - return false; - } - true - } -``` - -3. See if the person trying the move is doing it at the right time and with their piece color. - -```rust,ignore - fn is_correct_turn(maybe_piece: PieceType, caller: ContractAddress, game_id: felt252) -> bool { - true - } -``` - -4. see if it's the right move - -```c - fn is_right_piece_move( - maybe_piece: PieceType, curr_position: (u32, u32), next_position: (u32, u32) - ) -> bool { - let (c_x, c_y) = curr_position; - let (n_x, n_y) = next_position; - match maybe_piece { - PieceType::WhitePawn => { - true - }, - PieceType::WhiteKnight => { - if n_x == c_x + 2 && n_y == c_x + 1 { - return true; - } - - panic(array!['Knight illegal move']) - }, - PieceType::WhiteBishop => { - true - }, - PieceType::WhiteRook => { - true - }, - PieceType::WhiteQueen => { - true - }, - PieceType::WhiteKing => { - true - }, - PieceType::BlackPawn => { - true - }, - PieceType::BlackKnight => { - true - }, - PieceType::BlackBishop => { - true - }, - PieceType::BlackRook => { - true - }, - PieceType::BlackQueen => { - true - }, - PieceType::BlackKing => { - true - }, - PieceType::None(_) => panic(array!['Should not move empty square']), - } - } -``` - -5. You can also add other check functions to be extra sure the move is allowed. - - We will use these check functions to implement the `move()` function in the contract on our next chapter. You can decide how to set them up and which ones to use. diff --git a/src/tutorial/onchain-chess/2-move.md b/src/tutorial/onchain-chess/2-move.md new file mode 100644 index 00000000..37f9716c --- /dev/null +++ b/src/tutorial/onchain-chess/2-move.md @@ -0,0 +1,236 @@ +# 2 Move function + +1. Write a `move` function that accepts the `current position`, `next position`, `caller address`, and `game_id`. The `move` function should look like this: + +```c + #[external(v0)] + impl PlayerActionsImpl of IActions { + fn spawn( + self: @ContractState, white_address: ContractAddress, black_address: ContractAddress + ) -> u32 { + // Rest of code + } + fn move( + self: @ContractState, + curr_position: Vec2, + next_position: Vec2, + caller: ContractAddress, //player + game_id: u32 + ) { + let world = self.world_dispatcher.read(); + let mut current_piece = get!(world, (game_id, curr_position), (Piece)); + // check if next_position is out of board or not + assert(!PieceTrait::is_out_of_board(next_position), 'Should be inside board'); + + // check if this is the right move for this piece type + assert( + current_piece.is_right_piece_move(next_position), 'Illegal move for type of piece' + ); + // Get piece data from to next_position in the board + let mut next_position_piece = get!(world, (game_id, next_position), (Piece)); + + let player = get!(world, (game_id, caller), (Player)); + // check if there is already a piece in next_position + assert( + next_position_piece.piece_type == PieceType::None + || player.is_not_my_piece(next_position_piece.color), + 'Already same color piece exist' + ); + + next_position_piece.piece_type = current_piece.piece_type; + next_position_piece.color = player.color; + // make current_piece piece none + current_piece.piece_type = PieceType::None; + current_piece.color = Color::None; + set!(world, (next_position_piece)); + set!(world, (current_piece)); + + // change turn + let mut game_turn = get!(world, game_id, (GameTurn)); + game_turn.player_color = game_turn.next_turn(); + set!(world, (game_turn)); + } + } +``` + +2. Run `sozo build` to compile the code. + + Great, Now we can start testing our functions + +## Test Flow + +- Spawn the test world (`spawn_test_world`) that imports the models in testing. +- Deploy actions contract +- Interact with `spawn` function in the `actions` contract by providing white and black player's wallet addresses as inputs. +- Retrieve the game entity and piece entity created in `actions` contract. +- Ensure the game has been correctly created. +- Verify that each `Piece` is located in the correct position. + +## Unit Tests + +- Copy the test below and add it to your `tests/units.cairo` file. + +```c +#[cfg(test)] +mod tests { + use starknet::ContractAddress; + use dojo::test_utils::{spawn_test_world, deploy_contract}; + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; + use chess::models::player::{Player, Color, player}; + use chess::models::piece::{Piece, PieceType, Vec2, piece}; + use chess::models::game::{Game, GameTurn, game, game_turn}; + use chess::actions::{actions, IActionsDispatcher, IActionsDispatcherTrait}; + + // helper setup function + fn setup_world() -> (IWorldDispatcher, IActionsDispatcher) { + // models + let mut models = array![ + game::TEST_CLASS_HASH, + player::TEST_CLASS_HASH, + game_turn::TEST_CLASS_HASH, + piece::TEST_CLASS_HASH + ]; + // deploy world with models + let world = spawn_test_world(models); + + // deploy systems contract + let contract_address = world + .deploy_contract('salt', actions::TEST_CLASS_HASH.try_into().unwrap()); + let actions_system = IActionsDispatcher { contract_address }; + + (world, actions_system) + } + + #[test] + #[available_gas(3000000000000000)] + fn test_spawn() { + let white = starknet::contract_address_const::<0x01>(); + let black = starknet::contract_address_const::<0x02>(); + let (world, actions_system) = setup_world(); + + //system calls + let game_id = actions_system.spawn(white, black); + + //get game + let game = get!(world, game_id, (Game)); + let game_turn = get!(world, game_id, (GameTurn)); + assert(game_turn.player_color == Color::White, 'should be white turn'); + assert(game.white == white, 'white address is incorrect'); + assert(game.black == black, 'black address is incorrect'); + + //get a1 piece + let curr_pos = Vec2 { x: 0, y: 0 }; + let a1 = get!(world, (game_id, curr_pos), (Piece)); + assert(a1.piece_type == PieceType::Rook, 'should be Rook'); + assert(a1.color == Color::White, 'should be white color'); + assert(a1.piece_type != PieceType::None, 'should have piece'); + } + + #[test] + #[available_gas(3000000000000000)] + fn test_move() { + let white = starknet::contract_address_const::<0x01>(); + let black = starknet::contract_address_const::<0x02>(); + + let (world, actions_system) = setup_world(); + let game_id = actions_system.spawn(white, black); + let curr_pos = Vec2 { x: 0, y: 1 }; + let a2 = get!(world, (game_id, curr_pos), (Piece)); + assert(a2.piece_type == PieceType::Pawn, 'should be Pawn'); + assert(a2.color == Color::White, 'should be white color piece 1'); + assert(a2.piece_type != PieceType::None, 'should have piece'); + + let next_pos = Vec2 { x: 0, y: 2 }; + let game_turn = get!(world, game_id, (GameTurn)); + assert(game_turn.player_color == Color::White, 'should be white player turn'); + actions_system.move(curr_pos, next_pos, white.into(), game_id); + + let curr_pos = next_pos; + let c3 = get!(world, (game_id, curr_pos), (Piece)); + assert(c3.piece_type == PieceType::Pawn, 'should be Pawn'); + assert(c3.color == Color::White, 'should be white color piece 2'); + assert(c3.piece_type != PieceType::None, 'should have piece'); + + let game_turn = get!(world, game_id, (GameTurn)); + assert(game_turn.player_color == Color::Black, 'should be black player turn'); + } +} +``` + +## Diving into the Code + +### setup_world + +We should list all models with each having CLASS_HASH as elements and then we deploy world to models with `spawn_test_world` + +```rust,ignore + //models + let mut models = array![ + game::TEST_CLASS_HASH, + player::TEST_CLASS_HASH, + game_turn::TEST_CLASS_HASH, + piece::TEST_CLASS_HASH + ]; + // deploy world with models + let world = spawn_test_world(models); +``` + +After that, we deploy our system contracts, then we return our `world` and `actions_systems` dispatchers. + +```rust,ignore + let contract_address = world + .deploy_contract('salt', actions::TEST_CLASS_HASH.try_into().unwrap()); + let actions_system = IActionsDispatcher { contract_address }; + + (world, actions_system) +``` + +### test_spawn + +First, we'll set up the players address and their colors. + +```rust,ignore + let white = starknet::contract_address_const::<0x01>(); + let black = starknet::contract_address_const::<0x02>(); +``` + +We use `spawn` function in `actions.cairo` to put our pieces on the board. Each square position holds a piece. The system's `spawn` function needs some input i.e the addresses of the players. + +```rust,ignore + // spawn + let game_id = actions_system.spawn(white, black); +``` + +Then we check if the players got their setup address. After that we check if a White rook is at (0,0). Remember, to get a piece that exists on the position, you need to use the keys of the `Piece` model, which are `game_id`, and `curr_pos`. + +```c + //get a1 square + let curr_pos = Vec2 { x: 0, y: 0 }; + let a1 = get!(world, (game_id, curr_pos), (Piece)); + assert(a1.piece_type == PieceType::Rook, 'should be Rook'); + assert(a1.color == Color::White, 'should be white color'); + assert(a1.piece_type != PieceType::None, 'should have piece'); +``` + +### test_move + +Here, after setting up the board, we use `move` function in the contract to make moves. Provide the current position, the next position, the player's address, and the game id. + +```rust,ignore + //Move White Pawn to (0,2) + actions_system.move(curr_pos, next_pos, white.into(), game_id); +``` + +Then we check if a White Pawn is at the new position. + +```c + let curr_pos = next_pos; + let c3 = get!(world, (game_id, curr_pos), (Piece)); + assert(c3.piece_type == PieceType::Pawn, 'should be Pawn'); + assert(c3.color == Color::White, 'should be white color piece 2'); + assert(c3.piece_type != PieceType::None, 'should have piece'); +``` + +## Need help? + +If you're stuck, don't hesitate to ask questions at the [Dojo community](https://discord.gg/akd2yfuRS3)! diff --git a/src/tutorial/onchain-chess/3-move.md b/src/tutorial/onchain-chess/3-move.md deleted file mode 100644 index c7d167f4..00000000 --- a/src/tutorial/onchain-chess/3-move.md +++ /dev/null @@ -1,218 +0,0 @@ -# 3 Move function - -1. Write a `move` function that accepts the `current position`, `next position`, `caller address`, and `game id`. The `move` function should look like this: - -```c - #[external(v0)] - impl PlayerActionsImpl of IActions { - fn spawn( - self: @ContractState, white_address: ContractAddress, black_address: ContractAddress - ) { - // Rest of code - } - fn move( - self: @ContractState, - curr_position: (u32, u32), - next_position: (u32, u32), - caller: ContractAddress, //player - game_id: felt252 - ) { - let world = self.world_dispatcher.read(); - - let (current_x, current_y) = curr_position; - let (next_x, next_y) = next_position; - current_x.print(); - current_y.print(); - - next_x.print(); - next_y.print(); - - let mut current_square = get!(world, (game_id, current_x, current_y), (Square)); - - // check if next_position is out of board or not - assert(is_out_of_board(next_position), 'Should be inside board'); - - // check if this is the right piece type move - assert( - is_right_piece_move(current_square.piece, curr_position, next_position), - 'Should be right piece move' - ); - let target_piece = current_square.piece; - // make current_square piece none and move piece to next_square - current_square.piece = PieceType::None(()); - let mut next_square = get!(world, (game_id, next_x, next_y), (Square)); - - // check the piece already in next_suqare - let maybe_next_square_piece = next_square.piece; - - if maybe_next_square_piece == PieceType::None(()) { - next_square.piece = target_piece; - } else { - if is_piece_is_mine(maybe_next_square_piece) { - panic(array!['Already same color piece exist']) - } else { - next_square.piece = target_piece; - } - } - - set!(world, (next_square)); - set!(world, (current_square)); - } - } -``` - -2. Run `sozo build` to compile the code. - - Great, Now we can start testing our functions - -## Test Flow - -- Spawn the test world (`spawn_test_world`) that imports the models in testing. -- deploy actions contract -- interact with `spawn` function in the `actions` contract by providing white and black player's wallet addresses as inputs. -- Retrieve the game entity and piece entity created in `actions` contract. -- Ensure the game has been correctly created. -- Verify that each `Piece` is located in the correct `Square`. - -## Unit Tests - -```c -#[cfg(test)] -mod tests { - use starknet::ContractAddress; - use dojo::test_utils::{spawn_test_world, deploy_contract}; - use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; - use dojo_chess::models::{Game, game, GameTurn, game_turn, Square, square, PieceType}; - use dojo_chess::actions::actions; - use dojo_chess::actions::{IActionsDispatcher, IActionsDispatcherTrait}; - - // helper setup function - fn setup_world() -> (IWorldDispatcher, IActionsDispatcher) { - // models - let mut models = array![ - game::TEST_CLASS_HASH, game_turn::TEST_CLASS_HASH, square::TEST_CLASS_HASH - ]; - // deploy world with models - let world = spawn_test_world(models); - - // deploy systems contract - let contract_address = world - .deploy_contract('salt', actions::TEST_CLASS_HASH.try_into().unwrap()); - let actions_system = IActionsDispatcher { contract_address }; - - (world, actions_system) - } - - #[test] - #[available_gas(3000000000000000)] - fn test_initiate() { - let white = starknet::contract_address_const::<0x01>(); - let black = starknet::contract_address_const::<0x02>(); - - let (world, actions_system) = setup_world(); - - //system calls - let game_id = actions_system.spawn(white, black); - - //get game - let game = get!(world, game_id, (Game)); - assert(game.white == white, 'white address is incorrect'); - assert(game.black == black, 'black address is incorrect'); - - //get a1 square - let a1 = get!(world, (game_id, 0, 0), (Square)); - assert(a1.piece == PieceType::WhiteRook, 'should be White Rook'); - assert(a1.piece != PieceType::None, 'should have piece'); - } - - - #[test] - #[available_gas(3000000000000000)] - fn test_move() { - let white = starknet::contract_address_const::<0x01>(); - let black = starknet::contract_address_const::<0x02>(); - - let (world, actions_system) = setup_world(); - actions_system.spawn(white, black); - - let game_id = world.uuid(); - - let a2 = get!(world, (game_id, 0, 1), (Square)); - assert(a2.piece == PieceType::WhitePawn, 'should be White Pawn'); - assert(a2.piece != PieceType::None, 'should have piece'); - - actions_system.move((0, 1), (0, 2), white.into(), game_id); - - let c3 = get!(world, (game_id, 0, 2), (Square)); - assert(c3.piece == PieceType::WhitePawn, 'should be White Pawn'); - assert(c3.piece != PieceType::None, 'should have piece'); - } -} -``` - -## Diving into the Code - -### setup_world - -We should list both models with each having CLASS_HASH as elements and then we deploy world to models with `spawn_test_world` - -```rust,ignore - //models - let mut models = array![game::TEST_CLASS_HASH, game_turn::TEST_CLASS_HASH, square::TEST_CLASS_HASH]; - let world = spawn_test_world(models); -``` - -We then deploy our system contracts, then we return our `world` and `actions_systems` dispatcher - -```rust,ignore - let contract_address = world - .deploy_contract('salt', actions::TEST_CLASS_HASH.try_into().unwrap()); - let actions_system = IActionsDispatcher { contract_address }; - (world, actions_system) -``` - -### test_initiate - -First, we'll set up the players address and their colors. - -```rust,ignore - let white = starknet::contract_address_const::<0x01>(); - let black = starknet::contract_address_const::<0x02>(); -``` - -We use `spawn` function in `actions.cairo` to put our Square pieces on the board. Each Square holds a piece. The system's `spawn` function needs some input i.e the addresses of the players. - -```rust,ignore - // spawn - let game_id = actions_system.spawn(white, black); -``` - -Then we check if the players got their setup address. After that we check if a White rook is at (0,0). Remember, to get a piece that exists on the square, you need to use the keys of the `Square` model, which are `game_id`, `x`, and `y`. - -```c - //get a1 square - let a1 = get!(world, (game_id, 0, 0), (Square)); - assert(a1.piece == PieceType::WhiteRook, 'should be White Rook'); - assert(a1.piece != PieceType::None, 'should have piece'); -``` - -### test_move - -Here, after setting up the board, we use `move` function in the contract to make moves. Provide the current position, the next position, the player's address, and the game id. - -```rust,ignore - //Move White Pawn to (0,2) - actions_system.move((0, 1), (0, 2), white.into(), game_id); -``` - -Then we check if a White Pawn is at the new position. - -```c - let c3 = get!(world, (game_id, 0, 2), (Square)); - assert(c3.piece == PieceType::WhitePawn, 'should be White Pawn'); - assert(c3.piece != PieceType::None, 'should have piece'); -``` - -## Need help? - -If you're stuck, don't hesitate to ask questions at the [Dojo community](https://discord.gg/akd2yfuRS3)! diff --git a/src/tutorial/onchain-chess/3-test.md b/src/tutorial/onchain-chess/3-test.md new file mode 100644 index 00000000..fe9b79f9 --- /dev/null +++ b/src/tutorial/onchain-chess/3-test.md @@ -0,0 +1,98 @@ +# 3 Test Contract + +In this chapter, we'll use everything we've learned to run a full chess game scenario. + +Here's what we'll do in our test: + +1. Call spawn to setup `white_pawn` to (0,1) and `black_pawn` to (1,6) +2. Move `white_pawn` to (0,3) +3. Move `black_pawn` to (1,4) +4. Move `white_pawn` to (1,4) +5. Capture `black_pawn` + +To place the pieces, use our `spawn` function in our `actions` contract. For moving them, use the `move` contract. Remember to check if a piece can be captured when using `move`. + +Before we get to the code, set up your integration test like this: + +- Copy the test below and add it to your `tests/integration.cairo` file. + +## Full Code + +```c +mod tests { + use chess::models::piece::{Piece, PieceType, Vec2}; + use dojo::world::IWorldDispatcherTrait; + use chess::tests::units::tests::setup_world; + use chess::actions::{IActionsDispatcher, IActionsDispatcherTrait}; + use chess::models::player::{Color}; + + #[test] + #[available_gas(3000000000000000)] + fn integration() { + let white = starknet::contract_address_const::<0x01>(); + let black = starknet::contract_address_const::<0x02>(); + + let (world, actions_system) = setup_world(); + + //system calls + let game_id = actions_system.spawn(white, black); + + //White pawn is setup in (0,1) + let wp_curr_pos = Vec2 { x: 0, y: 1 }; + let a2 = get!(world, (game_id, wp_curr_pos), (Piece)); + assert(a2.piece_type == PieceType::Pawn, 'should be Pawn in (0,1)'); + assert(a2.color == Color::White, 'should be white color'); + assert(a2.piece_type != PieceType::None, 'should have piece in (0,1)'); + + //Black pawn is setup in (1,6) + let bp_curr_pos = Vec2 { x: 1, y: 6 }; + let b7 = get!(world, (game_id, bp_curr_pos), (Piece)); + assert(b7.piece_type == PieceType::Pawn, 'should be Pawn in (1,6)'); + assert(b7.color == Color::Black, 'should be black color'); + assert(b7.piece_type != PieceType::None, 'should have piece in (1,6)'); + + //Move White Pawn to (0,3) + let wp_next_pos = Vec2 { x: 0, y: 3 }; + actions_system.move(wp_curr_pos, wp_next_pos, white.into(), game_id); + + //White pawn is now in (0,3) + let wp_curr_pos = wp_next_pos; + let a4 = get!(world, (game_id, wp_curr_pos), (Piece)); + assert(a4.piece_type == PieceType::Pawn, 'should be Pawn in (0,3)'); + assert(a4.color == Color::White, 'should be white color'); + assert(a4.piece_type != PieceType::None, 'should have piece in (0,3)'); + + //Move black Pawn to (1,4) + let bp_next_pos = Vec2 { x: 1, y: 4 }; + actions_system.move(bp_curr_pos, bp_next_pos, black.into(), game_id); + + //Black pawn is now in (1,4) + let bp_curr_pos = bp_next_pos; + let b5 = get!(world, (game_id, bp_curr_pos), (Piece)); + assert(b5.piece_type == PieceType::Pawn, 'should be Pawn in (1,4)'); + assert(b5.color == Color::Black, 'should be black color'); + assert(b5.piece_type != PieceType::None, 'should have piece in (1,4)'); + + // Move White Pawn to (1,4) and capture black pawn + actions_system.move(wp_curr_pos, bp_curr_pos, white.into(), game_id); + + let wp_curr_pos = bp_curr_pos; + let b5 = get!(world, (game_id, wp_curr_pos), (Piece)); + assert(b5.piece_type == PieceType::Pawn, 'should be Pawn in (1,4)'); + assert(b5.color == Color::White, 'should be white color'); + assert(b5.piece_type != PieceType::None, 'should have piece in (1,4)'); + } +} +``` + +Keep moving pieces and checking if they're in the right places. + +## Congratulations! + +You've made the basic contracts for a chess game using the Dojo engine! This tutorial was just the beginning. There are many ways to make the game better, like optimizing parts, adding checks, or considering special cases. If you want to do more with this chess game, try these challenges: + +- Add a checkmate feature. Our game doesn't end now, so decide when it should! +- Include special moves like castling, En Passant Capture, or Pawn Promotion. +- Make your own chess rules! You could even create your own version of the [immortal game](https://immortal.game/) + +Lastly, share your project with others in the [Dojo community](https://discord.gg/akd2yfuRS3)! diff --git a/src/tutorial/onchain-chess/4-test.md b/src/tutorial/onchain-chess/4-test.md deleted file mode 100644 index 098d50d9..00000000 --- a/src/tutorial/onchain-chess/4-test.md +++ /dev/null @@ -1,87 +0,0 @@ -# 4 Test Contract - -In this chapter, we'll use everything we've learned to run a full chess game scenario. - -Here's what we'll do in our test: - -1. Call spawn to setup `white_pawn` to (0,1) and `black_pawn` to (1,6) -2. Move `white_pawn` to (0,3) -3. Move `black_pawn` to (1,4) -4. Move `white_pawn` to (1,4) -5. Capture `black_pawn` - -To place the pieces, use our `spawn` function in our `actions` contract. For moving them, use the `move` contract. Remember to check if a piece can be captured when using `move`. - -Before we get to the code, set up your integration test like this: - -- Copy the test below and add it to your `src/tests.cairo` file. - -## Full Code - -```c -#[cfg(test)] -mod tests { - use dojo_chess::models::{Square, PieceType}; - use dojo::world::IWorldDispatcherTrait; - use dojo_chess::actions::tests::setup_world; - use dojo_chess::actions::{IActionsDispatcher, IActionsDispatcherTrait}; - - #[test] - #[available_gas(3000000000000000)] - fn integration() { - let white = starknet::contract_address_const::<0x01>(); - let black = starknet::contract_address_const::<0x02>(); - - let (world, actions_system) = setup_world(); - - //system calls - let game_id = actions_system.spawn(white, black); - - //White pawn is now in (0,1) - let a2 = get!(world, (game_id, 0, 1), (Square)); - assert(a2.piece == PieceType::WhitePawn, 'should be White Pawn in (0,1)'); - assert(a2.piece != PieceType::None, 'should have piece in (0,1)'); - - //Black pawn is now in (1,6) - let b7 = get!(world, (game_id, 1, 6), (Square)); - assert(b7.piece == PieceType::BlackPawn, 'should be Black Pawn in (1,6)'); - assert(b7.piece != PieceType::None, 'should have piece in (1,6)'); - - //Move White Pawn to (0,3) - actions_system.move((0, 1), (0, 3), white.into(), game_id); - - //White pawn is now in (0,3) - let a4 = get!(world, (game_id, 0, 3), (Square)); - assert(a4.piece == PieceType::WhitePawn, 'should be White Pawn in (0,3)'); - assert(a4.piece != PieceType::None, 'should have piece in (0,3)'); - - //Move black Pawn to (1,4) - actions_system.move((1, 6), (1, 4), white.into(), game_id); - - //Black pawn is now in (1,4) - let b5 = get!(world, (game_id, 1, 4), (Square)); - assert(b5.piece == PieceType::BlackPawn, 'should be Black Pawn in (1,4)'); - assert(b5.piece != PieceType::None, 'should have piece in (1,4)'); - - // Move White Pawn to (1,4) - // Capture black pawn - actions_system.move((0, 3), (1, 4), white.into(), game_id); - - let b5 = get!(world, (game_id, 1, 4), (Square)); - assert(b5.piece == PieceType::WhitePawn, 'should be White Pawn in (1,4)'); - assert(b5.piece != PieceType::None, 'should have piece in (1,4)'); - } -} -``` - -Keep moving pieces and checking if they're in the right places. - -## Congratulations! - -You've made the basic contracts for a chess game using the Dojo engine! This tutorial was just the beginning. There are many ways to make the game better, like optimizing parts, adding checks, or considering special cases. If you want to do more with this chess game, try these challenges: - -- Add a checkmate feature. Our game doesn't end now, so decide when it should! -- Include special moves like castling, En Passant Capture, or Pawn Promotion. -- Make your own chess rules! You could even create your own version of the [immortal game](https://immortal.game/) - -Lastly, share your project with others in the [Dojo community](https://discord.gg/akd2yfuRS3)!