Building a Tic Tac Toe Game on Aptos with Move Language

Pulkit GovraniPulkit Govrani
7 min read

Tic Tac Toe is a classic game that serves as a great example for demonstrating fundamental programming concepts. In this blog, we will learn how to implement a Tic Tac Toe game using the Move programming language on the Aptos blockchain platform. We will explore the structure, logic, and functionalities of the code, providing a thorough understanding of how to build decentralized applications (dApps) with Move.

Prerequisites for Understanding Tic Tac Toe Game Code in Move Language

Before diving into the code for implementing a Tic Tac Toe game using the Move language on the Aptos blockchain, it is essential to have some foundational knowledge and tools. Here are the prerequisites:

  1. Basic Understanding of Blockchain Technology: Familiarity with how blockchain works, including concepts like distributed ledgers, smart contracts, and decentralized applications (dApps).
  1. Introduction to Move Language: Move is a programming language designed by Facebook's Diem project (formerly Libra) for secure and efficient blockchain applications. Understanding its syntax and basic concepts is crucial. You should know about modules, structs, functions, and the type system in Move.

  2. Familiarity with Aptos Blockchain: Aptos is a scalable and secure blockchain platform that uses the Move language. Knowing its architecture and how to deploy and interact with Move modules on Aptos will be beneficial.

  3. Basic Programming Concepts: You should be comfortable with programming concepts such as variables, data types, functions, control structures, and error handling.

5. Development Environment: Setting up a development environment for Move and Aptos, including installing necessary tools like Move CLI, Aptos CLI, and any required dependencies.

The Tic Tac Toe game implemented here includes the following features:

  • Initializing a new game

  • Making a move

  • Checking for a winner

  • Emitting events for moves, wins, and draws

  • Resetting the game

  • Viewing the current game state

Basic Syntax Explanation of Complete Code:

  1. Module Declaration:

    module tictactoe::game { ... }: This defines a module named game within the tictactoe namespace. A module in Move is similar to a class in other programming languages. It groups related functions and data structures.

  2. Imports:

    use std::error;, use std::signer;, etc.: These lines import various standard libraries and functionalities that our module will use. For example, std::error helps handle errors, and std::vector allows us to use vectors (dynamic arrays).

  3. Constants:

    const EINVALID_MOVE: u64 = 1;, etc.: These lines define constants used in our game. Constants are fixed values that don't change. Here, we're defining error codes and player identifiers. For example, EINVALID_MOVE is an error code for invalid moves, and PLAYER_X represents one player.

  4. Game Struct:

    struct Game has key { ... }: This defines a data structure called Game. A struct in Move is similar to a class or a record in other languages. It groups related data together. The Game struct has three fields: board (a vector of 9 cells representing the Tic Tac Toe board), current_turn (indicating whose turn it is), and winner (indicating the winner).

  5. Event Structs:

MoveEvent has drop, store { ... }: This defines an event called MoveEvent. Events are special structures that get emitted during transactions to record what happened. MoveEvent stores the player who made a move and the position of the move.

Similarly, WinEvent is emitted when a player wins, and DrawEvent is emitted when the game ends in a draw.

6. Function Declaration:

public entry fun init_game(account: &signer) { ... }: This defines a public entry function named init_game. Public entry functions can be called from outside the module. This function initializes a new game.

7. Initializing the Game:

move_to(account, Game { ... });: This line creates a new Game struct and assigns it to the given account. vector::from_elem(EMPTY, 9) creates a vector of 9 empty cells, setting up the initial board. The current turn is set to PLAYER_X, and the winner is set to EMPTY.

Declaring Constants & Structs

module tictactoe::game {

    use std::error;
    use std::signer;
    use std::string::{String, utf8};
    use std::vector;
    use aptos_framework::event;
    use aptos_framework::timestamp;

    const EINVALID_MOVE: u64 = 1;
    const EINVALID_PLAYER: u64 = 2;
    const EINVALID_INDEX: u64 = 3;
    const EGAME_OVER: u64 = 4;

    const PLAYER_X: u8 = 1;
    const PLAYER_O: u8 = 2;
    const EMPTY: u8 = 0;

    struct Game has key {
        board: vector<u8>,
        current_turn: u8,
        winner: u8,
    }

Code Explanation:

Constants: Error codes and player identifiers are defined. EINVALID_MOVE, EINVALID_PLAYER, EINVALID_INDEX, and EGAME_OVER are error codes for different invalid states. PLAYER_X and PLAYER_O represent the two players, and EMPTY represents an empty cell on the board.

Game Struct: The Game struct holds the game's state, including the board (a vector of 9 cells), the current turn, and the winner.

Creating Event Structs

#[event]
struct MoveEvent has drop, store {
    player: u8,
    position: u8,
}
#[event]
struct WinEvent has drop, store {
    winner: u8,
}
#[event]
struct DrawEvent has drop, store {
}

Code Explanation:

MoveEvent: Emitted when a player makes a move, containing the player and the position.

WinEvent: Emitted when a player wins, containing the winner.

DrawEvent: Emitted when the game is a draw.

Initializing a Game

 public entry fun init_game(account: &signer) {
        move_to(account, Game {
            board: vector::from_elem(EMPTY, 9),
            current_turn: PLAYER_X,
            winner: EMPTY,
        });
    }

Making a Move

public entry fun make_move(account: &signer, position: u8) acquires Game {

        let game = borrow_global_mut<Game>(signer::address_of(account));
        assert!(game.winner == EMPTY, EGAME_OVER);
        assert!(position < 9, EINVALID_INDEX);

        if (game.board[position] != EMPTY) {
            abort EINVALID_MOVE;
        }

        game.board[position] = game.current_turn;
        event::emit(MoveEvent { player: game.current_turn, position }); 

        if (check_winner(&game.board, game.current_turn)) {
            game.winner = game.current_turn;
            event::emit(WinEvent { winner: game.current_turn });
        } else if (is_draw(&game.board)) {
            event::emit(DrawEvent {});
        } else {
            game.current_turn = if (game.current_turn == PLAYER_X) { PLAYER_O } else { PLAYER_X };
        }
    }

Code Explanation:

It first checks if the game is over and if the position is valid.

If the cell at the given position is not empty, it aborts with EINVALID_MOVE.

It updates the board with the current player's move and emits a MoveEvent.

It checks if the current player has won using the check_winner function. If so, it sets the winner and emits a WinEvent.

If the board is full and there is no winner, it emits a DrawEvent.

If the game is still ongoing, it switches the turn to the other player.

Resetting the Game

 public entry fun reset_game(account: &signer) acquires Game {
        let game = borrow_global_mut<Game>(signer::address_of(account));
        game.board = vector::from_elem(EMPTY, 9);
        game.current_turn = PLAYER_X;
        game.winner = EMPTY;
 }
#[view]
public fun get_board(): vector<u8> acquires Game {
   let game = borrow_global<Game>(signer::address_of(account));
   game.board
}

#[view]
public fun get_current_turn(): u8 acquires Game {
   let game = borrow_global<Game>(signer::address_of(account));
   game.current_turn
}

#[view]
public fun get_winner(): u8 acquires Game {
   let game = borrow_global<Game>(signer::address_of(account));
   game.winner
}

Code Explanation:

get_board: Returns the current state of the board.

get_current_turn: Returns the current player's turn.

get_winner: Returns the winner of the game.

These view functions are essential for querying the game state without modifying it.

Checking the Winner of the Game

inline fun check_winner(board: &vector<u8>, player: u8): bool {
   let win_positions = vector::vector([
       vector::vector([0, 1, 2]),
       vector::vector([3, 4, 5]),
       vector::vector([6, 7, 8]),
       vector::vector([0, 3, 6]),
       vector::vector([1, 4, 7]),
       vector::vector([2, 5, 8]),
       vector::vector([0, 4, 8]),
       vector::vector([2, 4, 6])
   ]);

vector::any(&win_positions, move |positions| {
    vector::all(positions, move |&pos| board[pos] == player)
 })
}

Code Explanation:

  • win_positions: Defines all possible winning positions on the board.

  • vector::any: Checks if any of the winning positions are fully occupied by the current player.

  • vector::all: Checks if all positions in a winning line are occupied by the current player.

This function returns true if the current player has a winning combination, and false otherwise.

Checking for the Draw

 inline fun is_draw(board: &vector<u8>): bool {
    !vector::any(board, move |&cell| cell == EMPTY)
 }

This function returns true if the game is a draw, and false otherwise.

Conclusion

In this blog, we've explored a comprehensive implementation of a Tic Tac Toe game using the Move language on the Aptos blockchain platform. We covered the entire code, explaining each part in detail, from initializing the game, making moves, checking for winners, emitting events, resetting the game, and viewing the game state.

Building decentralized applications with Move and Aptos offers a secure and efficient way to develop robust blockchain-based games and other applications. By understanding the code structure and logic presented here, you can extend and customize this Tic Tac Toe game or create your own dApps on the Aptos platform.

0
Subscribe to my newsletter

Read articles from Pulkit Govrani directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pulkit Govrani
Pulkit Govrani