[ad_1]
It’s a new day and a great time to learn something new; I’m here to help you achieve that :). This article is meant to guide you through creating a tic-tac-toe P2P game (with a gambling feature) using Rust (with !ink).
After going through this long read (also practically), you should be able to interact with PSP22 standard tokens on Substrate chains like Aleph Zero (well this is our major focus anyways).
I’ll be demonstrating the psp22::balanceOf(), psp22::allowance(), psp22::transfer() and psp22::transferFrom() PSP22 methods all along in this article.
Fun fact: I transpile Solidity to Rust !ink and vice versa, JSYK 😎
Apparently, two out of five people know about the “tic-tac-toe” game, though it may be addressed with a different name depending on people’s discretion, orientation, locale or choice.
Note that the source code presented in this article isn’t audited in any way and is subject to vulnerabilities; it is solely for educational purposes and nothing else.
In case you’ve never heard about the tic-tac-toe game, it is a board game with a square-pattern grid of 3 by 3 cells (might have a larger amount based on players’ preferences, for instance, 6 by 6, 10 by 10 e.t.c) that is played using two characters for each player, typically “X” and “O”.
The players make moves by inserting (drawing) their associated character into an empty cell with the aim of creating a horizontal, diagonal or vertical pattern while preventing their opponent from doing the same, as well as outsmarting them in order to achieve their complete line without hindrance. For every round, if there is no winner (i.e. all slots are filled without any player making a valid pattern), the game is marked as a draw and a new round is commenced.
From the image below, it can be seen that the player assigned the “X” symbol is the winner in all three rounds (ignore the fact that “O” hasn’t been played all the rounds; player two is a dweeb).
In this example, both players would stake an amount to play the game. The winner takes all the tokens and the game is over, but if it is a draw, the tokens are refunded to both parties and the game is over. The board gets reset after each round of play also.
Now that you know how the game is played, let’s dive into the actual implementation with Rust 😎. Cool huh: )
Kindly follow this link to get started with installing “cargo-contract” for smart contracts development with Rust.
Before we start, there has to be a defined structure for the project folder, and we’ll be creating two files that will be used to build the contract.
The “cargo.toml” is more like the well-known “package.json” from NPM (Node.js) and helps us to define the properties, dependencies and information associated with the project, while we’ll be using “lib.rs” as our entry file (that’s where all the weightlifting will be done).
Those are the two files that will be used in this project.
Use the contents of this file to compose your “cargo.toml” file.
You’ll basically need to change the name and email in the “authors” key in the file at line 4 to match yours.
The file would look thus:
[package]
name = "tic_tac_toe"
version = "0.8.0"
authors = ["YOUR_NAME <[email protected]>"]
edition = "2021"
overflow-checks = false[dependencies]
# Import of all ink! crates
ink_primitives = { version = "~3.3.0", default-features = false }
ink_metadata = { version = "~3.3.0", default-features = false, features = ["derive"], optional = true }
ink_env = { version = "~3.3.0", default-features = false }
ink_storage = { version = "~3.3.0", default-features = false }
ink_lang = { version = "~3.3.0", default-features = false }
ink_prelude = { version = "~3.3.0", default-features = false }
ink_engine = { version = "~3.3.0", default-features = false, optional = true }
scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] }
scale-info = { version = "2", default-features = false, features = ["derive"], optional = true }
openbrush = { version = "~2.2.0", default-features = false, features = ["psp22"] }
[lib]
overflow-checks = false
name = "tic_tac_toe"
path = "lib.rs"
crate-type = [
# Used for normal contract Wasm blobs.
"cdylib"
]
[profile.release]
overflow-checks = false
[features]
default = ["std"]
std = [
"ink_primitives/std",
"ink_metadata",
"ink_metadata/std",
"ink_env/std",
"ink_storage/std",
"ink_lang/std",
"scale/std",
"scale-info",
"scale-info/std",
# Brush dependency
"openbrush/std",
]
First of all, we need to create the module and import all necessary dependencies that would gear the functioning of the smart contract, as well as the constructor initialization code and struct for the storage keys that will be used throughout the source code.
#![cfg_attr(not(feature = "std"), no_std)]use ink_lang as ink;
#[cfg(not(feature = "ink-as-dependency"))]
#[ink::contract]
pub mod tic_tac_toe {
use ink_prelude::vec;
use ink_prelude::vec::Vec;
use ink_storage::traits::SpreadAllocate;
use openbrush::contracts::traits::psp22::PSP22Ref;
use ink_env::{CallFlags};
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct TicTacToe {
board: Vec<u64>, //0 to 8 cells
turn: AccountId,
symbols: ink_storage::Mapping<AccountId, u64>,
player_one: AccountId,
player_two: AccountId,
staking_token: AccountId,
stake_amount: Balance,
stakes: ink_storage::Mapping<AccountId, Balance>,
last_winner: AccountId,
}
}
The keys defined in the storage struct “TicTacToe” will be discussed as we proceed in this article; don’t panic :).
The next thing we want to do is to define the constructor in an implementation of the “tic_tac_toe” module right after the “TicTacToe” struct that would be called upon deployment of our smart contracts so as to initialize some of the values we want to register into it at default.
We want to define the players’ addresses, the PSP22 token to be used in the game, and their symbols as well as the stake amount for the given game.
These can be supplied as arguments into the constructor thus:
impl TicTacToe {
/// Creates a new instance of this contract.
#[ink(constructor)]
pub fn new(player_one:AccountId, player_two:AccountId, player_one_symbol:u64, player_two_symbol:u64, staking_token: AccountId, stake_amount: Balance) -> Self {
//Do something here...
}
}
Within the “new()” method, we have to store the values as well as make some routine checks so that there is no anomaly in the contract’s behaviour in the gameplay.
let me = ink_lang::utils::initialize_contract(|contract: &mut Self| {let board = vec![0; 9]; //empty array
contract.board = board; //set board to empty state
contract.staking_token = staking_token; //set staking token
contract.stake_amount = stake_amount; //set stake amount
assert!(player_one != player_two); //addresses must not be the same
assert!(player_one_symbol != player_two_symbol); //symbols must be distinct
assert!((player_one_symbol == 1 || player_one_symbol == 2) && (player_two_symbol == 1 || player_two_symbol == 2)); //symbols must be either 1 or 2
contract.player_one = player_one; //set player one address
contract.player_two = player_two; //set player two address
contract.symbols.insert(player_one, &player_one_symbol); //set player one symbol
contract.symbols.insert(player_two, &player_two_symbol); //set player two symbol
contract.turn = player_one; //initialize turn to player one
});
me
We’re taking 0 as an empty cell, 1 as X and 2 as O in this example. We’ll also be using 0 to 8 to represent the cells of the board, starting from top-left to bottom-right in order, which means the top-left cell is 0, the bottom-right cell is 8 and the cell in the centre is 5.
Analysing the code above from top-to-bottom, we see that an empty array is created and we loop through it to insert 0 to each index from 0 to 8, meaning an empty board for a start.
We also set the staking token from its argument as well as the stake amount.
We want to ensure that player one is not using the same address as player two, and that they aren’t both assigned the same symbol (limited to 1 and 2). We set both addresses of player one and player two using supplied arguments.
We then also insert the symbols chosen by both players into a mapping, with their addresses as the keys and the chosen symbols as the values in u64 type.
Then we finally set player one as the first to take a turn.
Hurray! We’re done with our constructor!
As much as we want to make an intriguing game, we would want to know what is happening within the smart contract pertaining to storage and dynamics, hence we will create a couple of methods to provide storage values to us at will.
#[ink(message)]
pub fn get_stake_amount(&self) -> Balance {
self.stake_amount //amount to be staked in game
}#[ink(message)]
pub fn get_last_winner(&self) -> AccountId {
self.last_winner //address of most recent winner
}
#[ink(message)]
pub fn get_current_turn(&self) -> AccountId {
self.turn //who is meant to play?
}
#[ink(message)]
pub fn get_staking_token(&self) -> AccountId {
self.staking_token //get address of staking token smart contract
}
#[ink(message)]
pub fn get_player_two_stake(&self) -> Balance {
self.stakes.get(self.player_two).unwrap_or(0) //get total amount of tokens staked by player two
}
#[ink(message)]
pub fn get_player_one_stake(&self) -> Balance {
self.stakes.get(self.player_one).unwrap_or(0) //get total amount of tokens staked by player one
}
#[ink(message)]
pub fn get_player_two_symbol(&self) -> u64 {
self.symbols.get(self.player_two).unwrap_or(0) //get player two symbol
}
#[ink(message)]
pub fn get_player_one(&self) -> AccountId {
self.player_one //get player one address
}
#[ink(message)]
pub fn get_player_two(&self) -> AccountId {
self.player_two //get player two address
}
#[ink(message)]
pub fn get_player_one_symbol(&self) -> u64 {
self.symbols.get(self.player_one).unwrap_or(0) //get player one symbol
}
#[ink(message)]
pub fn get_board(&self) -> Vec<u64> {
//read and return board as array
let board = &self.board;
board.to_vec()
}
Most of the methods defined above are self-explanatory, but I will give a brief about each of them to give a clear understanding with regard to what values they are intended to output.
- get_stake_amount()
This is used to get the amount to be staked into the game by the players in order for them to play. - get_last_winner()
This is used to get the address of the player who most recently won the game. - get_current_turn()
This is used to get the player whose turn it is. - get_staking_token()
This is used to get the address of the staking token contract that the players will be using as a tender to stake. - get_player_one_stake()
This is used to get the total amount of tokens staked by player one. - get_player_two_stake()
This is used to get the total amount of tokens staked by player two. - get_player_one()
This is used to get player one’s wallet address. - get_player_two()
This is used to get player two’s wallet address. - get_player_one_symbol()
This is used to get player one’s symbol. - get_player_two_symbol()
This is used to get player two’s symbol. - get_board()
This returns an array representing the current gameplay state of the cells of the board from 0 to 8.
There are methods that we will be using internally to assist in determining values as well as performing in-bound actions within the smart contract.
They are as thus:
#[inline]
pub fn _has_won(&self, symbol: u64) -> bool {
let vertical = [[0,3,6], [1,4,7], [2,5,8]];
let horizontal = [[0,1,2], [3,4,5], [6,7,8]];
let diagonal = [[0,4,8], [2,4,6]];//check vertical
let mut v_win = false;
for i in 0..=2 {
let mut count = 0;
for j in 0..=2 {
if self.board[vertical[i][j]] == symbol {
count += 1;
}
}
if count == 3 {
v_win = true;
break;
}
}
//check horizontal
let mut h_win = false;
for i in 0..=2 {
let mut count = 0;
for j in 0..=2 {
if self.board[horizontal[i][j]] == symbol {
count += 1;
}
}
if count == 3 {
h_win = true;
break;
}
}
//check diagonal
let mut d_win = false;
for i in 0..=1 {
let mut count = 0;
for j in 0..=2 {
if self.board[diagonal[i][j]] == symbol {
count += 1;
}
}
if count == 3 {
d_win = true;
break;
}
}
if v_win == true || h_win == true || d_win == true {
true
}
else {
false
}
}
#[inline]
pub fn _clear_board(&mut self) {
let board = vec![0; 9];
self.board = board;
}
#[inline]
pub fn _is_cell_empty(&self, cell: u64) -> bool {
if self.board[usize::try_from(cell).unwrap()] == 0 {
true
} else {
false
}
}
#[inline]
pub fn _is_board_filled(&self) -> bool {
let mut filled_cells = 0;
let board = &self.board;
for cell in 0..=8 {
if board[usize::try_from(cell).unwrap()] != 0 {
filled_cells += 1;
}
}
if filled_cells == 9 {
true
} else {
false
}
}
#[inline]
pub fn _reward_winner(&mut self, account: AccountId) {
let total_stakes = PSP22Ref::balance_of(&self.staking_token, Self::env().account_id()); //get total stakes
PSP22Ref::transfer(
&self.staking_token,
account,
total_stakes,
ink_prelude::vec![],
); //transfer everything to the winner
self.stakes.insert(self.player_one, &0);
self.stakes.insert(self.player_two, &0);
}
#[inline]
pub fn _refund_tokens(&mut self) {
let total_stakes = PSP22Ref::balance_of(&self.staking_token, Self::env().account_id()); //get total stakes
let per_player = total_stakes / 2;
PSP22Ref::transfer(
&self.staking_token,
self.player_one,
per_player,
ink_prelude::vec![],
); //transfer half to player one
PSP22Ref::transfer(
&self.staking_token,
self.player_two,
per_player,
ink_prelude::vec![],
); //transfer half to player two
self.stakes.insert(self.player_one, &0);
self.stakes.insert(self.player_two, &0);
}
Briefly, I will explain what the inline helper methods above do, in case you do not have a clear grasp of them.
- _has_won(symbol: u64)
This method is used to determine if a given symbol is the winning symbol. Returns a boolean (true or false). Used to check the winner for every turn made. - _clear_board()
This method is used to clear the board (set all cells to 0 or empty as the case may be). - _is_cell_empty()
This is used to determine if a given cell is empty (is with value 0). - _is_board_filled()
This is used to determine if the board has been filled (applied for the check if there is no winner in the active round). - _reward_winner(address: AccountId)
This method is used to transfer all available tokens to the provided address as an argument. - _refund_tokens()
This method is used to refund all tokens (applies to a scenario where there is a tie in the active round).
This is the most crucial aspect of this smart contract, as it could lead to severe consequences if not written with scrutiny.
There are two methods that govern a successful gameplay:
- stake_tokens()
- play(cell: u64)
I’ll start with the first (as I should though :)).
stake_tokens()
#[ink(message)]
pub fn stake_tokens(&mut self) {
let player = self.env().caller(); //get caller address
let stakes = self.stakes.get(player).unwrap_or(0); //get stake if existentassert!(player == self.player_one || player == self.player_two); //Caller must be player one or two
if stakes > 0 {
panic!("Already staked for this round")
} //Make sure player hasn't already staked
let balance = PSP22Ref::balance_of(&self.staking_token, player); //get user balance of token
let allowance =
PSP22Ref::allowance(&self.staking_token, player, Self::env().account_id()); //get spending allowance contract has to player
assert!(balance > self.stake_amount); //balance must be greater than stake amount
assert!(allowance > self.stake_amount); //allowance must be greater than stake amount
//Transfer stake amount from caller (player) to contract
PSP22Ref::transfer_from_builder(
&self.staking_token,
self.env().caller(),
Self::env().account_id(),
self.stake_amount,
ink_prelude::vec![],
)
.call_flags(CallFlags::default().set_allow_reentry(true))
.fire()
.expect("Transfer failed")
.expect("Transfer failed");
self.stakes.insert(player, &self.stake_amount); //Add stake amount to user stake
}
From the code block above, you can deduce that this method is used to deposit the staking tokens (place a bet) for the next round.
Firstly, it checks if the caller is either of player one or player two, then checks if a stake has already been made.
We also check their balance in the staking tokens, ensuring that it is up to the stake amount, and that the spending allowance is sufficient enough for the contract to charge the stake amount from the player’s wallet.
We finally transfer the staking tokens from their wallet and add the stake to the player’s stake mapping.
play(cell: u64)
This method takes an argument with u64 type specifying the cell to play on.
#[ink(message)]
pub fn play(&mut self, cell: u64) {assert!(cell <= 8);
let player = self.env().caller(); //get caller address
assert!(player == self.player_one || player == self.player_two); //caller must be player one or two
assert!(self.get_player_one_stake() > 0 && self.get_player_two_stake() > 0); //both players must have staked
let is_empty = self._is_cell_empty(cell); //check if cell is empty
assert!(is_empty == true); //cell must be empty
assert!(self.turn == player); //must be player's turn
let mut board = self.get_board();
let player_one_symbol = self.get_player_one_symbol();
let player_two_symbol = self.get_player_two_symbol();
let cell_index = usize::try_from(cell).unwrap(); //convert index to usize
board[cell_index] = self.symbols.get(player).unwrap_or(0);
self.board = board;
let player_one_won = self._has_won(player_one_symbol);
let player_two_won = self._has_won(player_two_symbol);
let mut game_over = false;
if player_one_won == true {
//player one won
self.turn = self.player_one; //set player to start next round
self._reward_winner(self.player_one);
self._clear_board(); //clear game board
self.last_winner = self.player_one; //set to last winner
game_over = true; //game is over
} else if player_two_won == true {
//player two won
self.turn = self.player_two; //set player to start next round
self._reward_winner(self.player_two);
self._clear_board(); //clear game board
self.last_winner = self.player_one; //set to last winner
game_over = true;
} else {
if self._is_board_filled() == true {
//It's a draw
self.turn = self.player_one;
self._refund_tokens(); //refund tokens because no one won
self._clear_board();
game_over = true;
}
}
if game_over == false {
if self.turn == self.player_one {
self.turn = self.player_two;
} else {
self.turn = self.player_one;
}
}
}
We do a good number of checks in this method before allowing the player to make a move.
First of all, we want to ensure that the caller is either of the two players associated with the game, also ensuring that both players have staked their tokens for the round.
We then confirm that it is the given player’s turn, after which we would write their symbol into the appropriate key in the board storage array.
We check for win and draw conditions (the check is done for every move made).
If a player wins, they are sent all the money, the board is reset and they are made to be the next players (play the next turn).
If the game ends as a tie, the board is cleared likewise and the tokens are refunded to both parties and a new round starts.
We’re done! 🔥
With all this being said, I deserve a chilled glass of wine. I hope you enjoyed reading and/or practising this guide 💚.
Here is the complete source code (I formatted it though:)):
#![cfg_attr(not(feature = "std"), no_std)]use ink_lang as ink;
#[cfg(not(feature = "ink-as-dependency"))]
#[ink::contract]
pub mod tic_tac_toe {
use ink_prelude::vec;
use ink_prelude::vec::Vec;
use ink_storage::traits::SpreadAllocate;
use openbrush::contracts::traits::psp22::PSP22Ref;
use ink_env::CallFlags;
#[ink(storage)]
#[derive(SpreadAllocate)]
pub struct TicTacToe {
board: Vec<u64>, //0 to 8 cells
turn: AccountId,
symbols: ink_storage::Mapping<AccountId, u64>,
player_one: AccountId,
player_two: AccountId,
staking_token: AccountId,
stake_amount: Balance,
stakes: ink_storage::Mapping<AccountId, Balance>,
last_winner: AccountId,
}
impl TicTacToe {
/// Creates a new instance of this contract.
#[ink(constructor)]
pub fn new(
player_one: AccountId,
player_two: AccountId,
player_one_symbol: u64,
player_two_symbol: u64,
staking_token: AccountId,
stake_amount: Balance,
) -> Self {
let me = ink_lang::utils::initialize_contract(|contract: &mut Self| {
let board = vec![0; 9]; //empty array
contract.board = board; //set board to empty state
contract.staking_token = staking_token; //set staking token
contract.stake_amount = stake_amount; //set stake amount
assert!(player_one != player_two); //addresses must not be the same
assert!(player_one_symbol != player_two_symbol); //symbols must be distinct
assert!(
(player_one_symbol == 1 || player_one_symbol == 2)
&& (player_two_symbol == 1 || player_two_symbol == 2)
); //symbols must be either 1 or 2
contract.player_one = player_one; //set player one address
contract.player_two = player_two; //set player two address
contract.symbols.insert(player_one, &player_one_symbol); //set player one symbol
contract.symbols.insert(player_two, &player_two_symbol); //set player two symbol
contract.turn = player_one; //initialize turn to player one
});
me
}
#[ink(message)]
pub fn get_stake_amount(&self) -> Balance {
self.stake_amount //amount to be staked in game
}
#[ink(message)]
pub fn get_last_winner(&self) -> AccountId {
self.last_winner //address of most recent winner
}
#[ink(message)]
pub fn get_current_turn(&self) -> AccountId {
self.turn //who is meant to play?
}
#[ink(message)]
pub fn get_staking_token(&self) -> AccountId {
self.staking_token //get address of staking token smart contract
}
#[ink(message)]
pub fn get_player_two_stake(&self) -> Balance {
self.stakes.get(self.player_two).unwrap_or(0) //get total amount of tokens staked by player two
}
#[ink(message)]
pub fn get_player_one_stake(&self) -> Balance {
self.stakes.get(self.player_one).unwrap_or(0) //get total amount of tokens staked by player one
}
#[ink(message)]
pub fn get_player_one(&self) -> AccountId {
self.player_one //get player one address
}
#[ink(message)]
pub fn get_player_two(&self) -> AccountId {
self.player_two //get player two address
}
#[ink(message)]
pub fn get_player_two_symbol(&self) -> u64 {
self.symbols.get(self.player_two).unwrap_or(0) //get player two symbol
}
#[ink(message)]
pub fn get_player_one_symbol(&self) -> u64 {
self.symbols.get(self.player_one).unwrap_or(0) //get player one symbol
}
#[ink(message)]
pub fn get_board(&self) -> Vec<u64> {
//read and return board as array
let board = &self.board;
board.to_vec()
}
#[ink(message)]
pub fn stake_tokens(&mut self) {
let player = self.env().caller(); //get caller address
let stakes = self.stakes.get(player).unwrap_or(0); //get stake if existent
assert!(player == self.player_one || player == self.player_two); //Caller must be player one or two
if stakes > 0 {
panic!("Already staked for this round")
} //Make sure player hasn't already staked
let balance = PSP22Ref::balance_of(&self.staking_token, player); //get user balance of token
let allowance =
PSP22Ref::allowance(&self.staking_token, player, Self::env().account_id()); //get spending allowance contract has to player
assert!(balance > self.stake_amount); //balance must be greater than stake amount
assert!(allowance > self.stake_amount); //allowance must be greater than stake amount
//Transfer stake amount from caller (player) to contract
PSP22Ref::transfer_from_builder(
&self.staking_token,
self.env().caller(),
Self::env().account_id(),
self.stake_amount,
ink_prelude::vec![],
)
.call_flags(CallFlags::default().set_allow_reentry(true))
.fire()
.expect("Transfer failed")
.expect("Transfer failed");
self.stakes.insert(player, &self.stake_amount); //Add stake amount to user stake
}
#[inline]
pub fn _has_won(&self, symbol: u64) -> bool {
let vertical = [[0, 3, 6], [1, 4, 7], [2, 5, 8]];
let horizontal = [[0, 1, 2], [3, 4, 5], [6, 7, 8]];
let diagonal = [[0, 4, 8], [2, 4, 6]];
//check vertical
let mut v_win = false;
for i in 0..=2 {
let mut count = 0;
for j in 0..=2 {
if self.board[vertical[i][j]] == symbol {
count += 1;
}
}
if count == 3 {
v_win = true;
break;
}
}
//check horizontal
let mut h_win = false;
for i in 0..=2 {
let mut count = 0;
for j in 0..=2 {
if self.board[horizontal[i][j]] == symbol {
count += 1;
}
}
if count == 3 {
h_win = true;
break;
}
}
//check diagonal
let mut d_win = false;
for i in 0..=1 {
let mut count = 0;
for j in 0..=2 {
if self.board[diagonal[i][j]] == symbol {
count += 1;
}
}
if count == 3 {
d_win = true;
break;
}
}
if v_win == true || h_win == true || d_win == true {
true
} else {
false
}
}
#[inline]
pub fn _clear_board(&mut self) {
let board = vec![0; 9];
self.board = board;
}
#[inline]
pub fn _is_cell_empty(&self, cell: u64) -> bool {
if self.board[usize::try_from(cell).unwrap()] == 0 {
true
} else {
false
}
}
#[inline]
pub fn _is_board_filled(&self) -> bool {
let mut filled_cells = 0;
let board = &self.board;
for cell in 0..=8 {
if board[usize::try_from(cell).unwrap()] != 0 {
filled_cells += 1;
}
}
if filled_cells == 9 {
true
} else {
false
}
}
#[inline]
pub fn _reward_winner(&mut self, account: AccountId) {
let total_stakes = PSP22Ref::balance_of(&self.staking_token, Self::env().account_id()); //get total stakes
PSP22Ref::transfer(
&self.staking_token,
account,
total_stakes,
ink_prelude::vec![],
); //transfer everything to the winner
self.stakes.insert(self.player_one, &0);
self.stakes.insert(self.player_two, &0);
}
#[inline]
pub fn _refund_tokens(&mut self) {
let total_stakes = PSP22Ref::balance_of(&self.staking_token, Self::env().account_id()); //get total stakes
let per_player = total_stakes / 2;
PSP22Ref::transfer(
&self.staking_token,
self.player_one,
per_player,
ink_prelude::vec![],
); //transfer half to player one
PSP22Ref::transfer(
&self.staking_token,
self.player_two,
per_player,
ink_prelude::vec![],
); //transfer half to player two
self.stakes.insert(self.player_one, &0);
self.stakes.insert(self.player_two, &0);
}
#[ink(message)]
pub fn play(&mut self, cell: u64) {
assert!(cell <= 8);
let player = self.env().caller(); //get caller address
assert!(player == self.player_one || player == self.player_two); //caller must be player one or two
assert!(self.get_player_one_stake() > 0 && self.get_player_two_stake() > 0); //both players must have staked
let is_empty = self._is_cell_empty(cell); //check if cell is empty
assert!(is_empty == true); //cell must be empty
assert!(self.turn == player); //must be player's turn
let mut board = self.get_board();
let player_one_symbol = self.get_player_one_symbol();
let player_two_symbol = self.get_player_two_symbol();
let cell_index = usize::try_from(cell).unwrap(); //convert index to usize
board[cell_index] = self.symbols.get(player).unwrap_or(0);
self.board = board;
let player_one_won = self._has_won(player_one_symbol);
let player_two_won = self._has_won(player_two_symbol);
let mut game_over = false;
if player_one_won == true {
//player one won
self.turn = self.player_one; //set player to start next round
self._reward_winner(self.player_one);
self._clear_board(); //clear game board
self.last_winner = self.player_one; //set to last winner
game_over = true; //game is over
} else if player_two_won == true {
//player two won
self.turn = self.player_two; //set player to start next round
self._reward_winner(self.player_two);
self._clear_board(); //clear game board
self.last_winner = self.player_one; //set to last winner
game_over = true;
} else {
if self._is_board_filled() == true {
//It's a draw
self.turn = self.player_one;
self._refund_tokens(); //refund tokens because no one won
self._clear_board();
game_over = true;
}
}
if game_over == false {
if self.turn == self.player_one {
self.turn = self.player_two;
} else {
self.turn = self.player_one;
}
}
}
}
}
If you enjoyed this article and/or found it helpful, kindly follow me on Twitter as @EOttoho to be more updated with my moves 😎. To get access to the full source code on GitHub, visit this link. Kudos.
Feel free to fork the repository and send me a PR anytime you like.
In order to deploy your smart contract on the Aleph Zero Testnet with an intriguing and fluid UI, you can follow this link.
If you’ve reached this point in this article, you deserve some $AZERO tokens 🙂
I’m glad to have you here all the time.
New to trading? Try crypto trading bots or copy trading
[ad_2]
Source link