Perform basic operations
In this section, we'll dive into the essential operations needed to manage a Tic Tac Toe game on the Chromia blockchain. We'll focus on creating game, starting new games, and making moves on the board. By the end of this tutorial, you will know how to handle these fundamental operations within your decentralized game.
Add data through transactions
In Chromia, data is added to the blockchain through transactions, which consist of one or more database operations. Let's define the key operations needed to create games, start games, and make moves.
Create a game
operation create_game() {
val account = ft4_auth.authenticate();
functions.create_game(account);
}
The create_game
operation initializes a new instance of the pre-defined game
entity. It sets the initial values like created_by
, created_at
, and empty board slots (slot_1
through slot_9
as slot_states.free
) for this particular game instance.
Join a game
operation join_game(entities.game) {
val account = ft4_auth.authenticate();
functions.join_game(game, account);
}
The join_game
operation allows a second player to join an existing game by authenticating the player’s account and updating the player2_account_rowid
field in the specific game
instance. Only authorized players are allowed to join.
Make a move
operation make_move(entities.game, slot: integer) {
val account = ft4_auth.authenticate();
functions.make_move(game, slot, account);
}
The make_move
operation allows a player to mark a board position by updating the appropriate slot in the game
instance. Before recording the move, it validates that:
- The slot is within the valid range (1–9).
- The player is authorized and it’s their turn.
- The game status is active.
- The selected slot is unoccupied.
After validation, make_move
updates the game state with the player’s move and checks if the game has been won, ended in a draw, or should continue to the next player’s turn.
Test the operations
Writing and running unit tests are essential to ensure your game logic is correct. Testing is crucial to verify that your operations function as expected.
Set up the test module
-
In your project's directory, navigate to the test folder. If the test folder doesn't exist, create one.
-
Create a new
module.rell
file inside the test folder. This file will contain your test cases. -
Add the following header to
module.rell
:
@test module;
import tic_tac_toe.{ game, game_status, slot_states };
import tic_tac_toe.external.{ create_game, get_games_by_status, join_game, make_move, get_game_by_id };
import lib.ft4.accounts.{ ft4_account: account, auth_descriptor };
import lib.ft4.core.accounts.strategies.open.{ ras_open };
import lib.ft4.test.utils. { create_auth_descriptor, ft_auth_operation_for };
import lib.ft4.external.accounts.strategies.{ register_account };
- The
@test module;
declaration signals that this module is specifically for test cases. - The
import
statements bring in the necessary components from thetic_tac_toe
andlib.ft4
libraries:- Game management (
create_game
,get_games_by_status
,join_game
,make_move
,get_game_by_id
) functions from thetic_tac_toe.external
module. - Account and authentication utilities (
ft4_account
,auth_descriptor
,register_account
) to create and manage player accounts for the tests. - Test utilities (
create_auth_descriptor
,ft_auth_operation_for
) for handling test-specific setup and authentication.
- Game management (
With these imports, you have access to the main game functions and player management operations, ready for test case development in module.rell
.
Write test cases
Let's write test cases to ensure the basic game operations work correctly.
Create account test
This test validates the creation of user accounts using the create_test_user_data
function. It ensures that the accounts are successfully added to the system by checking the total account count.
function test_create_accounts() {
create_test_user_data(rell.test.keypairs.bob);
create_test_user_data(rell.test.keypairs.eve);
assert_equals(get_all_account().size(), 2);
}
In this test case:
Test: test_create_accounts
- Initialize accounts:
- Use
create_test_user_data
to initialize accounts for two users,bob_kp
(Bob) andeve_kp
(Eve), by passing their respective keypairs fromrell.test.keypairs
.
- Use
- Verify account creation:
- Use
get_all_account().size()
to retrieve the total number of accounts in the system. - Confirm that exactly two accounts exist using
assert_equals(get_all_account().size(), 2)
.
- Use
Create game test
This test case ensures that a player can create a new game and verifies game creation constraints.
function test_create_game() {
val (bob_kp, bob_account) = create_test_user_data(rell.test.keypairs.bob);
val last_block_time = rell.test.last_block_time;
create_test_game(bob_kp);
val all_created_games = get_games_by_status(game_status.created);
val all_created_games_expected = [
(
id = 6,
created_by = bob_account.id,
created_at = last_block_time,
player1_account_id = bob_account.id,
player2_account_id = null,
last_step_account_id = bob_account.id,
last_step_at = last_block_time,
winner = null,
slot_1 = slot_states.free,
slot_2 = slot_states.free,
slot_3 = slot_states.free,
slot_4 = slot_states.free,
slot_5 = slot_states.free,
slot_6 = slot_states.free,
slot_7 = slot_states.free,
slot_8 = slot_states.free,
slot_9 = slot_states.free,
status = game_status.created
)
];
assert_equals(all_created_games.to_gtv_pretty(), all_created_games_expected.to_gtv_pretty());
}
function test_create_game_if_already_in_one_must_fail() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
create_test_game(bob_kp);
rell.test.tx()
.op(ft_auth_operation_for(bob_kp.pub))
.op(create_game())
.sign(bob_kp)
.run_must_fail(
"Player %s are already participating in an ongoing game and cannot join another.".format(bob_kp.pub.hash())
);
}
In this test case:
Test: test_create_game
- Initialize test user data:
- Set up a keypair and account data for Bob using
create_test_user_data
.
- Set up a keypair and account data for Bob using
- Set up and record initial game state:
- Record the current blockchain timestamp as
last_block_time
. - Call
create_test_game(bob_kp)
to initialize a game created by Bob.
- Record the current blockchain timestamp as
- Verify created games:
- Retrieve games with status
created
usingget_games_by_status
. - Define the expected game state in
all_created_games_expected
, matching Bob’s ID ascreated_by
andplayer1_account_id
fields, and confirming that all slots arefree
. - Use
assert_equals
to compare the actual and expected game states, ensuring the new game instance was set up correctly.
- Retrieve games with status
Test: test_create_game_if_already_in_one_must_fail
- Set up keypair and create initial game:
- Set up Bob’s keypair.
- Call
create_test_game(bob_kp)
to create an initial game.
- Attempt to create a second game and expect failure:
- Attempt to create another game for Bob using
create_game
while he’s already in an ongoing game. - Use
run_must_fail
to ensure the transaction fails, with a specific failure message indicating that a player cannot join another game if they’re already participating in one.
- Attempt to create another game for Bob using
Join game test
This test verifies that users can join a game and that only one additional player can join an active game. It involves creating a game, joining it with a second player, and verifying the game's updated state. It also checks that any attempt to join an already active game will fail.
function test_join_game() {
val (bob_kp, bob_account) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, eve_account) = create_test_user_data(rell.test.keypairs.eve);
val game = create_test_game(bob_kp);
val all_created_games = get_games_by_status(game_status.created);
join_test_game(game, eve_kp);
val game_data = get_game_by_id(game.rowid.to_integer());
val game_data_expected = (
id = game.rowid,
created_by = bob_account.id,
created_at = all_created_games[0].created_at,
player1_account_id = bob_account.id,
player2_account_id = eve_account.id,
last_step_account_id = bob_account.id,
last_step_at = all_created_games[0].last_step_at,
winner = null,
slot_1 = slot_states.free,
slot_2 = slot_states.free,
slot_3 = slot_states.free,
slot_4 = slot_states.free,
slot_5 = slot_states.free,
slot_6 = slot_states.free,
slot_7 = slot_states.free,
slot_8 = slot_states.free,
slot_9 = slot_states.free,
status = game_status.active
);
assert_equals(game_data.to_gtv_pretty(), game_data_expected.to_gtv_pretty());
}
function test_join_active_game_must_fail() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, _) = create_test_user_data(rell.test.keypairs.eve);
val (alice_kp, _) = create_test_user_data(rell.test.keypairs.alice);
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
join_test_game_must_fail(game, alice_kp, "The game has already started.");
}
In this test case:
Test: test_join_game
- Initialize accounts:
- Use
create_test_user_data
to initialize accounts for two users,bob_kp
(Bob) andeve_kp
(Eve), by passing their keypairs fromrell.test.keypairs
.
- Use
- Create and retrieve game:
- Create a game by calling
create_test_game(bob_kp)
, where Bob is the creator. - Retrieve the game by status with
get_games_by_status(game_status.created)
, ensuring the game is in a "created" state.
- Create a game by calling
- Join game:
- Use
join_test_game(game, eve_kp)
to let Eve join the game.
- Use
- Verify game state:
- Retrieve game data with
get_game_by_id(game.rowid.to_integer())
. - Create an expected game state
game_data_expected
, verifying:- Bob as the creator (
created_by = bob_account.id
) - Both players (Bob and Eve) are assigned to the game
- Game status has changed to
game_status.active
- Bob as the creator (
- Use
assert_equals(game_data.to_gtv_pretty(), game_data_expected.to_gtv_pretty())
to confirm the game state matches expectations.
- Retrieve game data with
Test: test_join_active_game_must_fail
- Initialize accounts:
- Initialize accounts for three users:
bob_kp
(Bob),eve_kp
(Eve), andalice_kp
(Alice), each created withcreate_test_user_data
.
- Initialize accounts for three users:
- Create and join game:
- Bob creates a game with
create_test_game(bob_kp)
. - Eve successfully joins the game using
join_test_game(game, eve_kp)
.
- Bob creates a game with
- Attempt to join active game:
- Use
join_test_game_must_fail(game, alice_kp, "The game has already started.")
to check that Alice’s attempt to join an already active game fails, with a message asserting that only two players are allowed per game.
- Use
Make move test
This set of test cases ensures that players can make moves in a Tic Tac Toe game while validating the rules of the game.
function test_make_move() {
val (bob_kp, bob_account) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, eve_account) = create_test_user_data(rell.test.keypairs.eve);
val game_created_time = rell.test.last_block_time;
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
val last_move_time = rell.test.last_block_time;
make_test_move(game, 1, eve_kp);
val game_data = get_game_by_id(game.rowid.to_integer());
val game_data_expected = (
id = game.rowid,
created_by = bob_account.id,
created_at = game_created_time,
player1_account_id = bob_account.id,
player2_account_id = eve_account.id,
last_step_account_id = eve_account.id,
last_step_at = last_move_time,
winner = null,
slot_1 = slot_states.one,
slot_2 = slot_states.free,
slot_3 = slot_states.free,
slot_4 = slot_states.free,
slot_5 = slot_states.free,
slot_6 = slot_states.free,
slot_7 = slot_states.free,
slot_8 = slot_states.free,
slot_9 = slot_states.free,
status = game_status.active
);
assert_equals(game_data.to_gtv_pretty(), game_data_expected.to_gtv_pretty());
}
function test_move_on_wrong_turn_must_fail() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, _) = create_test_user_data(rell.test.keypairs.eve);
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move_must_fail(game, 1, bob_kp, "Action denied: incorrect player attempting to move.");
}
function test_move_with_wrong_slot_must_fail() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, _) = create_test_user_data(rell.test.keypairs.eve);
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move_must_fail(game, 10, eve_kp, "Wrong slot.");
}
function test_move_by_non_participant_slot_must_fail() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, _) = create_test_user_data(rell.test.keypairs.eve);
val (alice_kp, _) = create_test_user_data(rell.test.keypairs.alice);
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move_must_fail(game, 1, alice_kp, "Player is not a participant in this game.");
}
function test_move_on_taken_slot_must_fail() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, _) = create_test_user_data(rell.test.keypairs.eve);
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move(game, 1, eve_kp);
make_test_move_must_fail(game, 1, bob_kp, "Slot already taken.");
}
function test_move_in_non_active_game_must_fail() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
val game = create_test_game(bob_kp);
make_test_move_must_fail(game, 1, bob_kp, "Game is not active.");
}
In this test case:
Test: test_make_move
- Initialize test user data:
- Set up keypairs and accounts for Bob and Eve using
create_test_user_data
.
- Set up keypairs and accounts for Bob and Eve using
- Create and join game:
- Create a new game for Bob using
create_test_game(bob_kp)
. - Eve joins the game with
join_test_game(game, eve_kp)
.
- Create a new game for Bob using
- Make a move:
- Eve makes a move in slot 1 using
make_test_move(game, 1, eve_kp)
.
- Eve makes a move in slot 1 using
- Verify game state after move:
- Retrieve the updated game state with
get_game_by_id
. - Define the expected game data, including updated player details and slot states.
- Use
assert_equals
to compare the actual game data with the expected values, ensuring the move was recorded correctly.
- Retrieve the updated game state with
Test: test_move_on_wrong_turn_must_fail
- Set up keypairs:
- Initialize keypairs for Bob and Eve.
- Create game and join:
- Create a game for Bob and have Eve join it.
- Attempt move on wrong turn:
- Bob tries to make a move when it’s Eve's turn, using
make_test_move_must_fail
. This should fail with an appropriate error message.
- Bob tries to make a move when it’s Eve's turn, using
Test: test_move_with_wrong_slot_must_fail
- Set up keypairs:
- Initialize keypairs for Bob and Eve.
- Create game and join:
- Create a game for Bob and have Eve join it.
- Attempt move with invalid slot:
- Eve tries to make a move in slot 10, which is invalid. This should fail, returning the "Wrong slot." message.
Test: test_move_by_non_participant_slot_must_fail
- Set up keypairs:
- Initialize keypairs for Bob, Eve, and Alice.
- Create game and join:
- Create a game for Bob and have Eve join it.
- Attempt move by a non-participant:
- Alice tries to make a move, which should fail with the message "Player is not a participant in this game."
Test: test_move_on_taken_slot_must_fail
- Set up keypairs:
- Initialize keypairs for Bob and Eve.
- Create game and join:
- Create a game for Bob and have Eve join it.
- Make first move:
- Eve makes a move in slot 1.
- Attempt move in taken slot:
- Bob tries to make a move in the same slot, which should fail with the message "Slot already taken."
Test: test_move_in_non_active_game_must_fail
- Set up keypairs:
- Initialize a keypair for Bob.
- Create game:
- Create a game for Bob.
- Attempt to move in a non-active game:
- Bob tries to make a move, which should fail with the message "Game is not active."
Create play full game test
function test_play_full_game_first_player_win() {
val (bob_kp, bob_account) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, eve_account) = create_test_user_data(rell.test.keypairs.eve);
val game_created_time = rell.test.last_block_time;
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move(game, 1, eve_kp);
make_test_move(game, 4, bob_kp);
make_test_move(game, 3, eve_kp);
make_test_move(game, 5, bob_kp);
make_test_move(game, 9, eve_kp);
val last_move_time = rell.test.last_block_time;
make_test_move(game, 6, bob_kp);
val game_data = get_game_by_id(game.rowid.to_integer());
val game_data_expected = (
id = game.rowid,
created_by = bob_account.id,
created_at = game_created_time,
player1_account_id = bob_account.id,
player2_account_id = eve_account.id,
last_step_account_id = bob_account.id,
last_step_at = last_move_time,
winner = bob_account.id,
slot_1 = slot_states.one,
slot_2 = slot_states.free,
slot_3 = slot_states.one,
slot_4 = slot_states.zero,
slot_5 = slot_states.zero,
slot_6 = slot_states.zero,
slot_7 = slot_states.free,
slot_8 = slot_states.free,
slot_9 = slot_states.one,
status = game_status.winner
);
assert_equals(game_data.to_gtv_pretty(), game_data_expected.to_gtv_pretty());
}
function test_play_full_game_second_player_win() {
val (bob_kp, bob_account) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, eve_account) = create_test_user_data(rell.test.keypairs.eve);
val game_created_time = rell.test.last_block_time;
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move(game, 1, eve_kp);
make_test_move(game, 2, bob_kp);
make_test_move(game, 4, eve_kp);
make_test_move(game, 3, bob_kp);
val last_move_time = rell.test.last_block_time;
make_test_move(game, 7, eve_kp);
val game_data = get_game_by_id(game.rowid.to_integer());
val game_data_expected = (
id = game.rowid,
created_by = bob_account.id,
created_at = game_created_time,
player1_account_id = bob_account.id,
player2_account_id = eve_account.id,
last_step_account_id = eve_account.id,
last_step_at = last_move_time,
winner = eve_account.id,
slot_1 = slot_states.one,
slot_2 = slot_states.zero,
slot_3 = slot_states.zero,
slot_4 = slot_states.one,
slot_5 = slot_states.free,
slot_6 = slot_states.free,
slot_7 = slot_states.one,
slot_8 = slot_states.free,
slot_9 = slot_states.free,
status = game_status.winner
);
assert_equals(game_data.to_gtv_pretty(), game_data_expected.to_gtv_pretty());
}
function test_play_full_game_draw() {
val (bob_kp, bob_account) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, eve_account) = create_test_user_data(rell.test.keypairs.eve);
val game_created_time = rell.test.last_block_time;
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move(game, 2, eve_kp);
make_test_move(game, 1, bob_kp);
make_test_move(game, 4, eve_kp);
make_test_move(game, 3, bob_kp);
make_test_move(game, 5, eve_kp);
make_test_move(game, 6, bob_kp);
make_test_move(game, 7, eve_kp);
make_test_move(game, 8, bob_kp);
val last_move_time = rell.test.last_block_time;
make_test_move(game, 9, eve_kp);
val game_data = get_game_by_id(game.rowid.to_integer());
val game_data_expected = (
id = game.rowid,
created_by = bob_account.id,
created_at = game_created_time,
player1_account_id = bob_account.id,
player2_account_id = eve_account.id,
last_step_account_id = eve_account.id,
last_step_at = last_move_time,
winner = null,
slot_1 = slot_states.zero,
slot_2 = slot_states.one,
slot_3 = slot_states.zero,
slot_4 = slot_states.one,
slot_5 = slot_states.one,
slot_6 = slot_states.zero,
slot_7 = slot_states.one,
slot_8 = slot_states.zero,
slot_9 = slot_states.one,
status = game_status.draw
);
assert_equals(game_data.to_gtv_pretty(), game_data_expected.to_gtv_pretty());
}
function test_create_new_game_after_completion() {
val (bob_kp, _) = create_test_user_data(rell.test.keypairs.bob);
val (eve_kp, _) = create_test_user_data(rell.test.keypairs.eve);
val game = create_test_game(bob_kp);
join_test_game(game, eve_kp);
make_test_move(game, 1, eve_kp);
make_test_move(game, 4, bob_kp);
make_test_move(game, 3, eve_kp);
make_test_move(game, 5, bob_kp);
make_test_move(game, 9, eve_kp);
make_test_move(game, 6, bob_kp);
create_test_game(bob_kp);
val all_created_games = get_games_by_status(game_status.created);
assert_equals(all_created_games.size(), 1);
}
In this test case:
Test: test_play_full_game_first_player_win
- Initialize keypairs:
- Create keypairs for
bob_kp
andeve_kp
(representing Bob and Eve) for game participation.
- Create keypairs for
- Start a game:
- Use
create_test_game(bob_kp)
to initiate a game where Bob is the creator.
- Use
- Join and play:
- Eve joins using
join_test_game(game, eve_kp)
. - Sequential moves simulate gameplay, resulting in a winning scenario for Bob on move 6.
- Eve joins using
- Verify outcome:
- Retrieve game data and verify that:
- The winner is set to
bob_account.id
. - The game board reflects the expected win pattern.
- The game status is
game_status.winner
.
- The winner is set to
- Retrieve game data and verify that:
Test: test_play_full_game_second_player_win
- Initialize keypairs and start a game:
- Similar to the first test, set up keypairs and start a game with Bob as the creator.
- Simulate gameplay:
- Sequential moves result in a win for Eve on move 7.
- Verify outcome:
- Retrieve game data to confirm:
- The winner is set to
eve_account.id
. - The game board reflects the expected win pattern.
- The game status is
game_status.winner
.
- The winner is set to
- Retrieve game data to confirm:
Test: test_play_full_game_draw
- Initialize and start a game:
- Similar setup as above.
- Simulate gameplay:
- Sequential moves result in a draw, with no winning slots filled.
- Verify outcome:
- Check that:
- The winner is
null
(indicating a draw). - All board slots are occupied with no win pattern.
- The game status is
game_status.draw
.
- The winner is
- Check that:
Test: test_create_new_game_after_completion
- Initialize keypairs and start a game:
- Create keypairs and initiate a game with Bob as the game creator.
- Simulate gameplay and restart:
- Sequential moves complete a winning scenario.
- Create a new game for Bob post-completion and verify:
- Verify outcome:
- Ensure only one game is available with
game_status.created
.
- Ensure only one game is available with
Run the tests
Now that you have written your test, you can run it to verify the functionality of your dapp.
-
Open a terminal and navigate to your project's root directory.
-
Run the following command to execute the tests:
chr test
This command will run the tests located in the test: modules
in the chromia.yml
. If all tests pass, you’ll see confirmation that your game’s functionality is working as expected.
Congratulations! You’ve successfully implemented and tested the core operations for your decentralized Tic Tac Toe game on the Chromia blockchain.
In the next part of this tutorial, we’ll explore advanced features like game state persistence and user input validation to enhance your game further.