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 chess
Open the project folder.
cd chess
And initialize the project using sozo init.
sozo init
Cleaning 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.cairo
Remodel 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 build
Basic 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 🎉