0. Setup
Before starting recommend following the hello-dojo chapter to gain a basic understanding of the Dojo game.
Initializing the Project
Create a new Dojo project folder. You can name your project what you want.
mkdir chessOpen the project folder.
cd chessAnd initialize the project using sozo init.
sozo initCleaning Up the Boilerplate
The project comes with a lot of boilerplate codes. Clear it all. Make sure your directory looks like this
├── README.md
├── Scarb.toml
└── src
├── actions.cairo
├── lib.cairo
├── models
│ ├── game.cairo
│ ├── piece.cairo
│ └── player.cairo
├── models.cairo
├── tests
│ ├── integration.cairo
│ └── units.cairo
└── tests.cairoRemodel your lib.cairo, to look like this :
mod actions;
mod models;
mod tests;Remodel your models.cairo, to look like this :
mod game;
mod piece;
mod player;Remodel your tests.cairo, to look like this :
mod integration;
mod units;Make sure your Scarb.toml looks like this:
[package]
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.4.2" }
[[target.dojo]]
[tool.dojo]
initializer_class_hash = "0xbeef"
[tool.dojo.env]
rpc_url = "http://localhost:5050/"
# Default account for katana with seed = 0
account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"
private_key = "0x1800000000300000180000000000030000000000003006001800006600"
Compile your project with:
sozo buildBasic Models
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 position, that position will hold that piece.
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.
use starknet::ContractAddress;
#[derive(Model, Drop, Serde)]
struct Player {
#[key]
game_id: u32,
#[key]
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.
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.
use chess::models::player::Color;
use starknet::ContractAddress;
#[derive(Model, Drop, Serde)]
struct Piece {
#[key]
game_id: u32,
#[key]
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 {
Pawn,
Knight,
Bishop,
Rook,
Queen,
King,
None,
}Basic systems
Starting from the next chapter, you will implement the actions.cairo file. This is where our game logic/contract will reside.
For now, actions.cairo should look like this:
#[dojo::contract]
mod actions {
}It should be noted that Systems function are contract methods, by implication, rather than implementing the game logic in systems, we are implementing it in a contract.
Compile your project
Now try sozo build to build.
Complied? Great! then let's move on. If not fix the issues, so that you can run the sozo build command successfully.
Implement Traits for models
Before you move on, implement traits for models so we can use them in the next chapter when creating the action contract.
Requirements
Firt we have to define the following traits for Game, Player, Piece models respectively.
trait GameTurnTrait {
fn next_turn(self: @GameTurn) -> Color;
}
trait PlayerTrait {
fn is_not_my_piece(self: @Player, piece_color: Color) -> bool;
}
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
// code for player.cairo file
trait PlayerTrait {
fn is_not_my_piece(self: @Player, piece_color: Color) -> bool;
}
impl PalyerImpl of PlayerTrait {
fn is_not_my_piece(self: @Player, piece_color: Color) -> bool {
*self.color != piece_color
}
}
// 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'])
}
}
}
// 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;
}
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
Congratulations! You've completed the basic setup for building an on-chain chess game 🎉