Telos Network
Search…
Tic-Tac-Toe Game Contract

Goal

The following tutorial will guide you to build a sample Player vs Player game contract. We will apply the knowledge we acquired eariler and use the tic-tac-toe game to demonstrate.

The Rules of the Game

For this game, we are using a standard 3x3 tic-tac-toe board. Players are divided into two roles: host and challenger. The host always makes the first move. Each pair of players can ONLY have up to two games at the same time, one where the first player becomes the host and the other one where the second player becomes the host.

Board

Instead of using o and x as in the traditional tic-tac-toe game, we use 1 to denote movement by host, 2 to denote movement by challenger, and 0 to denote empty cell. Furthermore, we will use one dimensional array to store the board. Hence:
(0,0)
(1,0)
(2,0)
(0,0)
-
o
x
(0,1)
-
x
-
(0,2)
x
o
o
Assuming x is host, the above board is equal to [0, 2, 1, 0, 1, 0, 1, 2, 2]

Actions

A user will have the following actions to interact with this contract:
    create: create a new game
    restart: restart an existing game, host or challenger is allowed to do this
    close: close an existing game, which frees up the storage used to store the game, only host is allowed to do this
    move: make a movement

Contract account

For the following guide, we are going to push the contract to an account called tic.tac.toe.
1
cleos create account eosio tic.tac.toe YOUR_PUBLIC_KEY
Copied!
Ensure that you have your wallet unlocked and the creator's private active key in the wallet imported, otherwise the above command will fail.
[[info | Wallet Unlocking]] | For instructions on wallet unlocking and keys importing, see section Create Development Wallet.
In the above step, if you see YOUR_PUBLIC_KEY instead of the public key value, you can either go back to section 1.4 Create Development Wallet and persist the value or replace YOUR_PUBLIC_KEY with the public key value manually.

Start

We are going to create two files here:
    1.
    tic.tac.toe.hpp => header file where the structure of the contract is defined
    2.
    tic.tac.toe.cpp => main part of the contract where the action handler is defined

Defining Structure

Let's first start with the header file and define the structure of the contract. Open tic.tac.toe.hpp and start with the following boilerplate:
1
// Import necessary library
2
#include <eosio/eosio.hpp>
3
4
// Generic eosio library, i.e. print, type, math, etc
5
using namespace eosio;
6
7
8
class[[eosio::contract("tic.tac.toe")]] tic_tac_toe : public contract
9
{
10
public:
11
using contract::contract;
12
tic_tac_toe(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds) {}
13
};
Copied!

Games Table

For this contract, we will need to have a table that stores a list of games. Let's define it:
1
...
2
class [[eosio::contract("tic.tac.toe")]] tic_tac_toe : public contract {
3
public:
4
...
5
typedef eosio::multi_index<"games"_n, game> games;
6
};
Copied!
    First template parameter defines the name of the table
    Second template parameter defines the structure that it stores (will be defined in the next section)

Game Structure

Let's define the structure for the game. Please ensure that this struct definition appears before the table definition in the code.
1
...
2
class tic_tac_toe : public eosio::contract {
3
public:
4
...
5
static constexpr name none = "none"_n;
6
static constexpr name draw = "draw"_n;
7
8
struct [[eosio::table]] game
9
{
10
11
static constexpr uint16_t board_width = 3;
12
static constexpr uint16_t board_height = board_width;
13
14
game() : board(board_width * board_height, 0){}
15
16
name challenger;
17
name host;
18
name turn; // = account name of host/ challenger
19
name winner = none; // = none/ draw/ name of host/ name of challenger
20
21
std::vector<uint8_t> board;
22
23
// Initialize board with empty cell
24
void initialize_board()
25
{
26
board.assign(board_width * board_height, 0);
27
}
28
29
// Reset game
30
void reset_game()
31
{
32
initialize_board();
33
turn = host;
34
winner = "none"_n;
35
}
36
37
auto primary_key() const { return challenger.value; }
38
EOSLIB_SERIALIZE( game, (challenger)(host)(turn)(winner)(board))
39
};
40
};
Copied!
The primary_key method is required by the above table definition for games. That is how the table knows what field is the lookup key for the table.

Action Handlers Structure

Create

To create the game, we need host account name and challenger's account name.
1
[[eosio::action]]
2
void create(const name &challenger, name &host);
Copied!

Restart

To restart the game, we need host account name and challenger's account name to identify the game. Furthermore, we need to specify who wants to restart the game, so we can verify the correct signature is provided.
1
[[eosio::action]]
2
void restart(const name &challenger, const name &host, const name &by);
Copied!

Close

To close the game, we need host account name and challenger's account name to identify the game.
1
[[eosio::action]]
2
void close(const name &challenger, const name &host);
Copied!

Move

To make a move, we need host account name and challenger's account name to identify the game. Furthermore, we need to specify who makes this move and the movement he is making.
1
[[eosio::action]]
2
void move(const name &challenger, const name &host, const name &by, const uint16_t &row, const uint16_t &column);
Copied!

Action Handlers Recap

To recap, we should have declared the following action handlers which will be defined in tic.tac.toe.cpp later.
1
// Import necessary library
2
#include <eosio/eosio.hpp>
3
4
// Generic eosio library, i.e. print, type, math, etc
5
using namespace eosio;
6
7
class[[eosio::contract("tic.tac.toe")]] tic_tac_toe : public contract
8
{
9
public:
10
using contract::contract;
11
tic_tac_toe(name receiver, name code, datastream<const char *> ds) : contract(receiver, code, ds) {}
12
13
static constexpr name none = "none"_n;
14
static constexpr name draw = "draw"_n;
15
16
struct [[eosio::table]] game
17
{
18
19
static constexpr uint16_t board_width = 3;
20
static constexpr uint16_t board_height = board_width;
21
22
game() : board(board_width * board_height, 0){}
23
24
name challenger;
25
name host;
26
name turn; // = account name of host/ challenger
27
name winner = none; // = none/ draw/ name of host/ name of challenger
28
29
std::vector<uint8_t> board;
30
31
// Initialize board with empty cell
32
void initialize_board()
33
{
34
board.assign(board_width * board_height, 0);
35
}
36
37
// Reset game
38
void reset_game()
39
{
40
initialize_board();
41
turn = host;
42
winner = "none"_n;
43
}
44
45
auto primary_key() const { return challenger.value; }
46
EOSLIB_SERIALIZE( game, (challenger)(host)(turn)(winner)(board))
47
};
48
49
typedef eosio::multi_index<"games"_n, game> games;
50
51
[[eosio::action]]
52
void create(const name &challenger, name &host);
53
54
[[eosio::action]]
55
void restart(const name &challenger, const name &host, const name &by);
56
57
[[eosio::action]]
58
void close(const name &challenger, const name &host);
59
60
[[eosio::action]]
61
void move(const name &challenger, const name &host, const name &by, const uint16_t &row, const uint16_t &column);
62
};
Copied!

Main Game Logic

Let's open tic.tac.toe.cpp and set up the boilerplate:
1
#include "tic.tac.toe.hpp"
Copied!

Action Handlers

We want tic tac toe contract to only react to actions sent to the tic.tac.toe account and react differently according to the type of the action. The actions that we declared previously are create, move, restart, and close. Let's define the individual action handlers in the next section.

Action Handler - create

For the create action handler, we want to:
    1.
    Ensure that the action has the signature from the host
    2.
    Ensure that the challenger and host are not the same player
    3.
    Ensure that there is no existing game
    4.
    Store the newly created game to the db
1
void tic_tac_toe::create(const name &challenger, name &host) {
2
require_auth(host);
3
check(challenger != host, "challenger shouldn't be the same as host");
4
5
// Check if game already exists
6
games existing_host_games(get_self(), host.value);
7
auto itr = existing_host_games.find(challenger.value);
8
check(itr == existing_host_games.end(), "game already exists");
9
10
existing_host_games.emplace(host, [&](auto &g) {
11
g.challenger = challenger;
12
g.host = host;
13
g.turn = host;
14
});
15
}
Copied!

Action Handler - move

For the move action handler, we want to:
    1.
    Ensure that the action has the signature from the host/ challenger
    2.
    Ensure that the game exists
    3.
    Ensure that the game is not finished yet
    4.
    Ensure that the move action is done by host or challenger
    5.
    Ensure that this is the right user's turn
    6.
    Verify movement is valid
    7.
    Update board with the new move
    8.
    Change the move_turn to the other player
    9.
    Determine if there is a winner
    10.
    Store the updated game to the db
1
void tic_tac_toe::move(const name &challenger, const name &host, const name &by, const uint16_t &row, const uint16_t &column)
2
{
3
check(has_auth(by), "the next move should be made by " + by.to_string());
4
5
// Check if game exists
6
games existing_host_games(get_self(), host.value);
7
auto itr = existing_host_games.find(challenger.value);
8
check(itr != existing_host_games.end(), "game doesn't exists");
9
10
// Check if this game hasn't ended yet
11
check(itr->winner == tic_tac_toe::none, "the game has ended!");
12
13
// Check if this game belongs to the action sender
14
check(by == itr->host || by == itr->challenger, "this is not your game!");
15
// Check if this is the action sender's turn
16
check(by == itr->turn, "it's not your turn yet!");
17
18
// Check if user makes a valid movement
19
check(is_valid_movement(row, column, itr->board), "not a valid movement!");
20
21
// Fill the cell, 1 for host, 2 for challenger
22
//TODO could use constant for 1 and 2 as well
23
const uint8_t cell_value = itr->turn == itr->host ? 1 : 2;
24
const auto turn = itr->turn == itr->host ? itr->challenger : itr->host;
25
existing_host_games.modify(itr, itr->host, [&](auto &g) {
26
g.board[row * tic_tac_toe::game::board_width + column] = cell_value;
27
g.turn = turn;
28
g.winner = get_winner(g);
29
});
30
}
Copied!

Movement Validation

Valid movement is defined as movement done inside the board on an empty cell:
1
bool is_empty_cell(const uint8_t &cell)
2
{
3
return cell == 0;
4
}
5
6
bool is_valid_movement(const uint16_t &row, const uint16_t &column, const std::vector<uint8_t> &board)
7
{
8
uint32_t movement_location = row * tic_tac_toe::game::board_width + column;
9
bool is_valid = movement_location < board.size() && is_empty_cell(board[movement_location]);
10
return is_valid;
11
}
Copied!

Get Winner

Winner is defined as the first player who succeeds in placing three of their marks in a horizontal, vertical, or diagonal row.
1
...
2
name get_winner(const tic_tac_toe::game ¤t_game)
3
{
4
auto &board = current_game.board;
5
6
bool is_board_full = true;
7
8
// Use bitwise AND operator to determine the consecutive values of each column, row and diagonal
9
// Since 3 == 0b11, 2 == 0b10, 1 = 0b01, 0 = 0b00
10
std::vector<uint32_t> consecutive_column(tic_tac_toe::game::board_width, 3);
11
std::vector<uint32_t> consecutive_row(tic_tac_toe::game::board_height, 3);
12
uint32_t consecutive_diagonal_backslash = 3;
13
uint32_t consecutive_diagonal_slash = 3;
14
15
for (uint32_t i = 0; i < board.size(); i++)
16
{
17
is_board_full &= is_empty_cell(board[i]);
18
uint16_t row = uint16_t(i / tic_tac_toe::game::board_width);
19
uint16_t column = uint16_t(i % tic_tac_toe::game::board_width);
20
21
// Calculate consecutive row and column value
22
consecutive_row[column] = consecutive_row[column] & board[i];
23
consecutive_column[row] = consecutive_column[row] & board[i];
24
// Calculate consecutive diagonal \ value
25
if (row == column)
26
{
27
consecutive_diagonal_backslash = consecutive_diagonal_backslash & board[i];
28
}
29
// Calculate consecutive diagonal / value
30
if (row + column == tic_tac_toe::game::board_width - 1)
31
{
32
consecutive_diagonal_slash = consecutive_diagonal_slash & board[i];
33
}
34
}
35
36
// Inspect the value of all consecutive row, column, and diagonal and determine winner
37
std::vector<uint32_t> aggregate = {consecutive_diagonal_backslash, consecutive_diagonal_slash};
38
aggregate.insert(aggregate.end(), consecutive_column.begin(), consecutive_column.end());
39
aggregate.insert(aggregate.end(), consecutive_row.begin(), consecutive_row.end());
40
41
for (auto value : aggregate)
42
{
43
if (value == 1)
44
{
45
return current_game.host;
46
}
47
else if (value == 2)
48
{
49
return current_game.challenger;
50
}
51
}
52
// Draw if the board is full, otherwise the winner is not determined yet
53
return is_board_full ? tic_tac_toe::draw : tic_tac_toe::none;
54
}
Copied!

Action Handler - restart

For the restart action handler, we want to:
    1.
    Ensure that the action has the signature from the host/challenger
    2.
    Ensure that the game exists
    3.
    Ensure that the restart action is done by host/challenger
    4.
    Reset the game
    5.
    Store the updated game to the db
1
void tic_tac_toe::restart(const name &challenger, const name &host, const name &by)
2
{
3
check(has_auth(by), "only " + by.to_string() + "can restart the game");
4
5
// Check if game exists
6
games existing_host_games(get_self(), host.value);
7
auto itr = existing_host_games.find(challenger.value);
8
check(itr != existing_host_games.end(), "game doesn't exists");
9
10
// Check if this game belongs to the action sender
11
check(by == itr->host || by == itr->challenger, "this is not your game!");
12
13
// Reset game
14
existing_host_games.modify(itr, itr->host, [](auto &g) {
15
g.reset_game();
16
});
17
}
Copied!

Action Handler - close

For the close action handler, we want to:
    1.
    Ensure that the action has the signature from the host
    2.
    Ensure that the game exists
    3.
    Remove the game from the db
1
void tic_tac_toe::close(const name &challenger, const name &host)
2
{
3
check(has_auth(host), "only the host can close the game");
4
5
require_auth(host);
6
7
// Check if game exists
8
games existing_host_games(get_self(), host.value);
9
auto itr = existing_host_games.find(challenger.value);
10
check(itr != existing_host_games.end(), "game doesn't exists");
11
12
// Remove game
13
existing_host_games.erase(itr);
14
}
Copied!
You can see the final tic.tac.toe.cpp in the next section.

Final Contract Code

The final state of the tic.tac.toe.cpp file:
1
// Import necessary library
2
#include "tic.tac.toe.hpp"
3
4
// Generic eosio library, i.e. print, type, math, etc
5
using namespace eosio;
6
7
bool is_empty_cell(const uint8_t &cell)
8
{
9
return cell == 0;
10
}
11
12
bool is_valid_movement(const uint16_t &row, const uint16_t &column, const std::vector<uint8_t> &board)
13
{
14
uint32_t movement_location = row * tic_tac_toe::game::board_width + column;
15
bool is_valid = movement_location < board.size() && is_empty_cell(board[movement_location]);
16
return is_valid;
17
}
18
19
void tic_tac_toe::create(const name &challenger, name &host) {
20
require_auth(host);
21
check(challenger != host, "challenger shouldn't be the same as host");
22
23
// Check if game already exists
24
games existing_host_games(get_self(), host.value);
25
auto itr = existing_host_games.find(challenger.value);
26
check(itr == existing_host_games.end(), "game already exists");
27
28
existing_host_games.emplace(host, [&](auto &g) {
29
g.challenger = challenger;
30
g.host = host;
31
g.turn = host;
32
});
33
}
34
35
void tic_tac_toe::restart(const name &challenger, const name &host, const name &by)
36
{
37
check(has_auth(by), "only " + by.to_string() + "can restart the game");
38
39
// Check if game exists
40
games existing_host_games(get_self(), host.value);
41
auto itr = existing_host_games.find(challenger.value);
42
check(itr != existing_host_games.end(), "game doesn't exists");
43
44
// Check if this game belongs to the action sender
45
check(by == itr->host || by == itr->challenger, "this is not your game!");
46
47
// Reset game
48
existing_host_games.modify(itr, itr->host, [](auto &g) {
49
g.reset_game();
50
});
51
}
52
53
void tic_tac_toe::close(const name &challenger, const name &host)
54
{
55
check(has_auth(host), "only the host can close the game");
56
57
require_auth(host);
58
59
// Check if game exists
60
games existing_host_games(get_self(), host.value);
61
auto itr = existing_host_games.find(challenger.value);
62
check(itr != existing_host_games.end(), "game doesn't exists");
63
64
// Remove game
65
existing_host_games.erase(itr);
66
}
67
68
void tic_tac_toe::move(const name &challenger, const name &host, const name &by, const uint16_t &row, const uint16_t &column)
69
{
70
check(has_auth(by), "the next move should be made by " + by.to_string());
71
72
// Check if game exists
73
games existing_host_games(get_self(), host.value);
74
auto itr = existing_host_games.find(challenger.value);
75
check(itr != existing_host_games.end(), "game doesn't exists");
76
77
// Check if this game hasn't ended yet
78
check(itr->winner == tic_tac_toe::none, "the game has ended!");
79
80
// Check if this game belongs to the action sender
81
check(by == itr->host || by == itr->challenger, "this is not your game!");
82
// Check if this is the action sender's turn
83
check(by == itr->turn, "it's not your turn yet!");
84
85
// Check if user makes a valid movement
86
check(is_valid_movement(row, column, itr->board), "not a valid movement!");
87
88
// Fill the cell, 1 for host, 2 for challenger
89
//TODO could use constant for 1 and 2 as well
90
const uint8_t cell_value = itr->turn == itr->host ? 1 : 2;
91
const auto turn = itr->turn == itr->host ? itr->challenger : itr->host;
92
existing_host_games.modify(itr, itr->host, [&](auto &g) {
93
g.board[row * tic_tac_toe::game::board_width + column] = cell_value;
94
g.turn = turn;
95
g.winner = get_winner(g);
96
});
97
}
98
99
name get_winner(const tic_tac_toe::game ¤t_game)
100
{
101
auto &board = current_game.board;
102
103
bool is_board_full = true;
104
105
// Use bitwise AND operator to determine the consecutive values of each column, row and diagonal
106
// Since 3 == 0b11, 2 == 0b10, 1 = 0b01, 0 = 0b00
107
std::vector<uint32_t> consecutive_column(tic_tac_toe::game::board_width, 3);
108
std::vector<uint32_t> consecutive_row(tic_tac_toe::game::board_height, 3);
109
uint32_t consecutive_diagonal_backslash = 3;
110
uint32_t consecutive_diagonal_slash = 3;
111
112
for (uint32_t i = 0; i < board.size(); i++)
113
{
114
is_board_full &= is_empty_cell(board[i]);
115
uint16_t row = uint16_t(i / tic_tac_toe::game::board_width);
116
uint16_t column = uint16_t(i % tic_tac_toe::game::board_width);
117
118
// Calculate consecutive row and column value
119
consecutive_row[column] = consecutive_row[column] & board[i];
120
consecutive_column[row] = consecutive_column[row] & board[i];
121
// Calculate consecutive diagonal \ value
122
if (row == column)
123
{
124
consecutive_diagonal_backslash = consecutive_diagonal_backslash & board[i];
125
}
126
// Calculate consecutive diagonal / value
127
if (row + column == tic_tac_toe::game::board_width - 1)
128
{
129
consecutive_diagonal_slash = consecutive_diagonal_slash & board[i];
130
}
131
}
132
133
// Inspect the value of all consecutive row, column, and diagonal and determine winner
134
std::vector<uint32_t> aggregate = {consecutive_diagonal_backslash, consecutive_diagonal_slash};
135
aggregate.insert(aggregate.end(), consecutive_column.begin(), consecutive_column.end());
136
aggregate.insert(aggregate.end(), consecutive_row.begin(), consecutive_row.end());
137
138
for (auto value : aggregate)
139
{
140
if (value == 1)
141
{
142
return current_game.host;
143
}
144
else if (value == 2)
145
{
146
return current_game.challenger;
147
}
148
}
149
// Draw if the board is full, otherwise the winner is not determined yet
150
return is_board_full ? tic_tac_toe::draw : tic_tac_toe::none;
151
}
Copied!

Compile

Let's compile our contract, using eosio-cpp:
1
eosio-cpp -I tic_tac_toe.hpp tic_tac_toe.cpp
Copied!

Deploy

Now the wasm file and the abi file are ready. Time to deploy! Create a directory (let's call it tic_tac_toe) and copy your generated tic.tac.toe.wasm and tic_tac_toe.abi files.
1
cleos set contract tic.tac.toe tic_tac_toe -p [email protected]
Copied!
Ensure that your wallet is unlocked and you have tic.tac.toe key imported.

Play the Game

After the deployment and the transaction is confirmed, the contract is already available in the blockchain. You can play with it now!
[[info | Test Account]] | If you have not created these accounts already, refer to this article for creating test accounts Create Test Accounts

Create a Game

We are going to use bob and alice accounts to play this game:
1
cleos push action tic.tac.toe create '{"challenger":"bob", "host":"alice"}' --permission [email protected]
Copied!

Make Moves

1
cleos push action tic.tac.toe move '{"challenger":"bob", "host":"alice", "by":"alice", "row":0, "column":0}' --permission [email protected]
2
3
cleos push action tic.tac.toe move '{"challenger":"bob", "host":"alice", "by":"bob", "row":1, "column":1}' --permission [email protected]
Copied!

See the Game Status

1
$ cleos get table tic.tac.toe alice games
2
{
3
"rows": [{
4
"challenger": "bob",
5
"host": "alice",
6
"turn": "bob",
7
"winner": "none",
8
"board": [
9
1,
10
0,
11
0,
12
0,
13
2,
14
0,
15
0,
16
0,
17
0
18
]
19
}
20
],
21
"more": false
22
}
Copied!

Restart the Game

1
cleos push action tic.tac.toe restart '{"challenger":"bob", "host":"alice", "by":"alice"}' --permission [email protected]
Copied!

Close the Game

1
cleos push action tic.tac.toe close '{"challenger":"bob", "host":"alice"}' --permission [email protected]
Copied!
Last modified 1yr ago