Skip to main content

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

src/main/operations.rell
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

src/main/operations.rell
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

src/main/operations.rell
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

  1. In your project's directory, navigate to the test folder. If the test folder doesn't exist, create one.

  2. Create a new module.rell file inside the test folder. This file will contain your test cases.

  3. Add the following header to module.rell:

src/test/tic_tac_toe/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 the tic_tac_toe and lib.ft4 libraries:
    • Game management (create_game, get_games_by_status, join_game, make_move, get_game_by_id) functions from the tic_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.

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.

src/test/tic_tac_toe/module.rell
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) and eve_kp (Eve), by passing their respective keypairs from rell.test.keypairs.
  • 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).

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 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.
  • Verify created games:
    • Retrieve games with status created using get_games_by_status.
    • Define the expected game state in all_created_games_expected, matching Bob’s ID as created_by and player1_account_id fields, and confirming that all slots are free.
    • Use assert_equals to compare the actual and expected game states, ensuring the new game instance was set up correctly.
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.

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.

src/test/tic_tac_toe/module.rell
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) and eve_kp (Eve), by passing their keypairs from rell.test.keypairs.
  • 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.
  • Join game:
    • Use join_test_game(game, eve_kp) to let Eve join the game.
  • 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
    • Use assert_equals(game_data.to_gtv_pretty(), game_data_expected.to_gtv_pretty()) to confirm the game state matches expectations.
Test: test_join_active_game_must_fail
  • Initialize accounts:
    • Initialize accounts for three users: bob_kp (Bob), eve_kp (Eve), and alice_kp (Alice), each created with create_test_user_data.
  • 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).
  • 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.

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.
  • 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).
  • Make a move:
    • Eve makes a move in slot 1 using make_test_move(game, 1, eve_kp).
  • 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.
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.
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 and eve_kp (representing Bob and Eve) for game participation.
  • Start a game:
    • Use create_test_game(bob_kp) to initiate a game where Bob is the creator.
  • 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.
  • 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.
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.
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.
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.

Run the tests

Now that you have written your test, you can run it to verify the functionality of your dapp.

  1. Open a terminal and navigate to your project's root directory.

  2. 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.