Pelajaran 3

Developing a TicTacToe Game on Tezos

The world of blockchain gaming is ripe with opportunities for developers. It provides a unique and innovative way to integrate decentralized and transparent mechanisms into games. By developing games on a blockchain, we can incorporate features such as secure and transparent transactions, ownership of in-game assets, and more. In this lesson, we're going to step into blockchain gaming by developing a classic game of TicTacToe on the Tezos blockchain. Our aim with this lesson is to understand the dynamics of game logic and state management in a blockchain-based game.

Let’s begin by exploring our contract for the TicTacToe game:

Contract Structure

Python
# TicTacToe - Example for illustrative purposes only.

import smartpy as sp


@sp.module
def main():
    class TicTacToe(sp.Contract):
        def __init__(self):
            self.data.nbMoves = 0
            self.data.winner = 0
            self.data.draw = False
            self.data.deck = {
                0: {0: 0, 1: 0, 2: 0},
                1: {0: 0, 1: 0, 2: 0},
                2: {0: 0, 1: 0, 2: 0},
            }
            self.data.nextPlayer = 1

        @sp.entrypoint
        def play(self, params):
            assert self.data.winner == 0 and not self.data.draw
            assert params.i >= 0 and params.i < 3
            assert params.j >= 0 and params.j < 3
            assert params.move == self.data.nextPlayer
            assert self.data.deck[params.i][params.j] == 0
            self.data.deck[params.i][params.j] = params.move
            self.data.nbMoves += 1
            self.data.nextPlayer = 3 - self.data.nextPlayer
            self.data.winner = self.checkLine(
                sp.record(winner=self.data.winner, line=self.data.deck[params.i])
            )
            self.data.winner = self.checkLine(
                sp.record(
                    winner=self.data.winner,
                    line={
                        0: self.data.deck[0][params.j],
                        1: self.data.deck[1][params.j],
                        2: self.data.deck[2][params.j],
                    },
                )
            )
            self.data.winner = self.checkLine(
                sp.record(
                    winner=self.data.winner,
                    line={
                        0: self.data.deck[0][0],
                        1: self.data.deck[1][1],
                        2: self.data.deck[2][2],
                    },
                )
            )
            self.data.winner = self.checkLine(
                sp.record(
                    winner=self.data.winner,
                    line={
                        0: self.data.deck[0][2],
                        1: self.data.deck[1][1],
                        2: self.data.deck[2][0],
                    },
                )
            )
            if self.data.nbMoves == 9 and self.data.winner == 0:
                self.data.draw = True

        @sp.private()
        def checkLine(self, winner, line):
            winner_ = winner
            if line[0] != 0 and line[0] == line[1] and line[0] == line[2]:
                winner_ = line[0]
            return winner_

        # Add a game reset function
        @sp.entrypoint
        def confirm_and_reset(self):
            assert self.data.winner != 0 or self.data.draw
            self.__init__()

# Tests
if "templates" not in __name__:

    @sp.add_test(name="TicTacToe")
    def test():
        scenario = sp.test_scenario(main)
        scenario.h1("Tic-Tac-Toe")
        # define a contract
        c1 = main.TicTacToe()

        # show its representation
        scenario.h2("A sequence of interactions with a winner")
        scenario += c1
        scenario.h2("Message execution")
        scenario.h3("A first move in the center")
        c1.play(i=1, j=1, move=1)
        scenario.h3("A forbidden move")
        c1.play(i=1, j=1, move=2).run(valid=False)
        scenario.h3("A second move")
        c1.play(i=1, j=2, move=2)
        scenario.h3("Other moves")
        c1.play(i=2, j=1, move=1)
        c1.play(i=2, j=2, move=2)
        scenario.verify(c1.data.winner == 0)
        c1.play(i=0, j=1, move=1)
        scenario.verify(c1.data.winner == 1)
        scenario.p("Player1 has won")
        c1.play(i=0, j=0, move=2).run(valid=False)

        c2 = main.TicTacToe()
        scenario.h2("A sequence of interactions with a draw")
        scenario += c2
        scenario.h2("Message execution")
        scenario.h3("A first move in the center")
        c2.play(i=1, j=1, move=1)
        scenario.h3("A forbidden move")
        c2.play(i=1, j=1, move=2).run(valid=False)
        scenario.h3("A second move")
        c2.play(i=1, j=2, move=2)
        scenario.h3("Other moves")
        c2.play(i=2, j=1, move=1)
        c2.play(i=2, j=2, move=2)
        c2.play(i=0, j=0, move=1)
        c2.play(i=0, j=1, move=2)
        c2.play(i=0, j=2, move=1)
        c2.play(i=2, j=0, move=2)
        c2.play(i=1, j=0, move=1)

        # Add tests for game reset
        scenario.h2("Testing game reset")
        scenario.p("Winner or draw confirmed, now resetting the game")
        c1.confirm_and_reset()
        scenario.verify(c1.data.nbMoves == 0)
        scenario.verify(c1.data.winner == 0)
        scenario.verify(not c1.data.draw)

        c2.confirm_and_reset()
        scenario.verify(c2.data.nbMoves == 0)
        scenario.verify(c2.data.winner == 0)
        scenario.verify(not c2.data.draw)

The contract for our TicTacToe game on Tezos is written in the SmartPy language. It consists of two main parts: the state of the contract, and the logic of the game.

State of the Contract

The state of the contract is initialized in the **init function. It includes:

  • nbMoves: This is a counter for the number of moves made in the game. It starts at zero.
  • winner: This keeps track of the winner of the game. Initially, it’s zero, indicating no winner.
  • draw: This is a flag indicating whether the game has ended in a draw. Initially, it’s False.
  • deck: This is a 3x3 grid representing the TicTacToe board. All spots on the board are initially empty, represented by zeroes.
  • nextPlayer: This indicates whose turn it is to play. The game starts with player 1, so it’s initially set to 1.

Game Logic

The game logic is encapsulated in the play function. It performs several checks to ensure a valid move:

  • It checks that no player has won yet and that the game isn’t a draw.
  • It verifies that the indices for the grid spot chosen by the player are within the grid’s boundaries.
  • It makes sure that the player making the move matches the nextPlayer.
  • It ensures that the chosen spot on the grid is empty.
    Once a move is made, the game logic increments the nbMoves, switches the nextPlayer, and checks if the move has resulted in a win or a draw.

The win condition is checked across the row and column of the latest move, as well as the two diagonals.

If all spots on the board are filled and no player has won (i.e., nbMoves equals 9 and winner is still 0), the game is declared a draw.

Checking for a Win

The checkLine function is used to check if any player has won. It checks if all spots in a line (which can be a row, a column, or a diagonal) are filled by the same player. If so, that player is declared the winner.

Interacting with the Contract

Interactions with the contract are represented as transactions. When a player makes a move by calling the play function, it generates a transaction. This transaction gets logged and can be seen in the right panel of the SmartPy IDE:

An unsuccessful or invalid move would also generate a transaction but with an error indication:

The Second Move and Beyond

The first move in our TicTacToe game is relatively straightforward since the game board is empty. However, things get more interesting with the second move and subsequent moves. These moves not only add pieces to the game board but also invoke the game’s logic to check for potential winners.

After the first move, the nextPlayer value switches to player 2. Now, the play function validates player 2’s move. Similar checks are performed to ensure the move is valid, i.e., the selected grid spot is within the boundaries and is empty.

As each player makes a move, the game’s state evolves. The nbMoves increases, the nextPlayer toggles, and the deck is updated. Also, after each move, the contract checks if there’s a winner or if it’s a draw.

For example, after the first player makes a move in the center of the board at i=1, j=1, the second player can play at a different spot, say i=1, j=2. Both of these moves would be validated and executed successfully, with the corresponding transactions being generated.

The Other Moves and Game Progression

Subsequent moves continue in a similar manner. Each player takes turns to play, choosing an empty spot on the board. After each move, the contract checks for any winning condition. If a player fills an entire row, column, or diagonal with their symbol, the game ends and that player is declared the winner. The winner variable in the contract’s state would be updated accordingly.

It’s important to note that once a player has won, no further moves are valid. Any attempt to make a move after the game has ended would be considered invalid and the corresponding transaction would fail.

The Draw Scenario

In some games, it’s possible that no player achieves a winning condition even after the entire game board is filled. This results in a draw. The contract has been designed to handle this situation as well.

If all spots on the board are filled (nbMoves equals 9) and no player has won (winner remains 0), the game is declared a draw. The draw flag in the contract’s state is set to True, indicating the game ended in a draw. Once again, no further moves are valid after this point. Any attempts to make a move after a draw would also fail.

The second part of the TicTacToe contract’s test scenario demonstrates this drawing scenario. It simulates a series of moves that result in a draw and verifies that the contract handles it correctly.

Pernyataan Formal
* Investasi Kripto melibatkan risiko besar. Lanjutkan dengan hati-hati. Kursus ini tidak dimaksudkan sebagai nasihat investasi.
* Kursus ini dibuat oleh penulis yang telah bergabung dengan Gate Learn. Setiap opini yang dibagikan oleh penulis tidak mewakili Gate Learn.
Katalog
Pelajaran 3

Developing a TicTacToe Game on Tezos

The world of blockchain gaming is ripe with opportunities for developers. It provides a unique and innovative way to integrate decentralized and transparent mechanisms into games. By developing games on a blockchain, we can incorporate features such as secure and transparent transactions, ownership of in-game assets, and more. In this lesson, we're going to step into blockchain gaming by developing a classic game of TicTacToe on the Tezos blockchain. Our aim with this lesson is to understand the dynamics of game logic and state management in a blockchain-based game.

Let’s begin by exploring our contract for the TicTacToe game:

Contract Structure

Python
# TicTacToe - Example for illustrative purposes only.

import smartpy as sp


@sp.module
def main():
    class TicTacToe(sp.Contract):
        def __init__(self):
            self.data.nbMoves = 0
            self.data.winner = 0
            self.data.draw = False
            self.data.deck = {
                0: {0: 0, 1: 0, 2: 0},
                1: {0: 0, 1: 0, 2: 0},
                2: {0: 0, 1: 0, 2: 0},
            }
            self.data.nextPlayer = 1

        @sp.entrypoint
        def play(self, params):
            assert self.data.winner == 0 and not self.data.draw
            assert params.i >= 0 and params.i < 3
            assert params.j >= 0 and params.j < 3
            assert params.move == self.data.nextPlayer
            assert self.data.deck[params.i][params.j] == 0
            self.data.deck[params.i][params.j] = params.move
            self.data.nbMoves += 1
            self.data.nextPlayer = 3 - self.data.nextPlayer
            self.data.winner = self.checkLine(
                sp.record(winner=self.data.winner, line=self.data.deck[params.i])
            )
            self.data.winner = self.checkLine(
                sp.record(
                    winner=self.data.winner,
                    line={
                        0: self.data.deck[0][params.j],
                        1: self.data.deck[1][params.j],
                        2: self.data.deck[2][params.j],
                    },
                )
            )
            self.data.winner = self.checkLine(
                sp.record(
                    winner=self.data.winner,
                    line={
                        0: self.data.deck[0][0],
                        1: self.data.deck[1][1],
                        2: self.data.deck[2][2],
                    },
                )
            )
            self.data.winner = self.checkLine(
                sp.record(
                    winner=self.data.winner,
                    line={
                        0: self.data.deck[0][2],
                        1: self.data.deck[1][1],
                        2: self.data.deck[2][0],
                    },
                )
            )
            if self.data.nbMoves == 9 and self.data.winner == 0:
                self.data.draw = True

        @sp.private()
        def checkLine(self, winner, line):
            winner_ = winner
            if line[0] != 0 and line[0] == line[1] and line[0] == line[2]:
                winner_ = line[0]
            return winner_

        # Add a game reset function
        @sp.entrypoint
        def confirm_and_reset(self):
            assert self.data.winner != 0 or self.data.draw
            self.__init__()

# Tests
if "templates" not in __name__:

    @sp.add_test(name="TicTacToe")
    def test():
        scenario = sp.test_scenario(main)
        scenario.h1("Tic-Tac-Toe")
        # define a contract
        c1 = main.TicTacToe()

        # show its representation
        scenario.h2("A sequence of interactions with a winner")
        scenario += c1
        scenario.h2("Message execution")
        scenario.h3("A first move in the center")
        c1.play(i=1, j=1, move=1)
        scenario.h3("A forbidden move")
        c1.play(i=1, j=1, move=2).run(valid=False)
        scenario.h3("A second move")
        c1.play(i=1, j=2, move=2)
        scenario.h3("Other moves")
        c1.play(i=2, j=1, move=1)
        c1.play(i=2, j=2, move=2)
        scenario.verify(c1.data.winner == 0)
        c1.play(i=0, j=1, move=1)
        scenario.verify(c1.data.winner == 1)
        scenario.p("Player1 has won")
        c1.play(i=0, j=0, move=2).run(valid=False)

        c2 = main.TicTacToe()
        scenario.h2("A sequence of interactions with a draw")
        scenario += c2
        scenario.h2("Message execution")
        scenario.h3("A first move in the center")
        c2.play(i=1, j=1, move=1)
        scenario.h3("A forbidden move")
        c2.play(i=1, j=1, move=2).run(valid=False)
        scenario.h3("A second move")
        c2.play(i=1, j=2, move=2)
        scenario.h3("Other moves")
        c2.play(i=2, j=1, move=1)
        c2.play(i=2, j=2, move=2)
        c2.play(i=0, j=0, move=1)
        c2.play(i=0, j=1, move=2)
        c2.play(i=0, j=2, move=1)
        c2.play(i=2, j=0, move=2)
        c2.play(i=1, j=0, move=1)

        # Add tests for game reset
        scenario.h2("Testing game reset")
        scenario.p("Winner or draw confirmed, now resetting the game")
        c1.confirm_and_reset()
        scenario.verify(c1.data.nbMoves == 0)
        scenario.verify(c1.data.winner == 0)
        scenario.verify(not c1.data.draw)

        c2.confirm_and_reset()
        scenario.verify(c2.data.nbMoves == 0)
        scenario.verify(c2.data.winner == 0)
        scenario.verify(not c2.data.draw)

The contract for our TicTacToe game on Tezos is written in the SmartPy language. It consists of two main parts: the state of the contract, and the logic of the game.

State of the Contract

The state of the contract is initialized in the **init function. It includes:

  • nbMoves: This is a counter for the number of moves made in the game. It starts at zero.
  • winner: This keeps track of the winner of the game. Initially, it’s zero, indicating no winner.
  • draw: This is a flag indicating whether the game has ended in a draw. Initially, it’s False.
  • deck: This is a 3x3 grid representing the TicTacToe board. All spots on the board are initially empty, represented by zeroes.
  • nextPlayer: This indicates whose turn it is to play. The game starts with player 1, so it’s initially set to 1.

Game Logic

The game logic is encapsulated in the play function. It performs several checks to ensure a valid move:

  • It checks that no player has won yet and that the game isn’t a draw.
  • It verifies that the indices for the grid spot chosen by the player are within the grid’s boundaries.
  • It makes sure that the player making the move matches the nextPlayer.
  • It ensures that the chosen spot on the grid is empty.
    Once a move is made, the game logic increments the nbMoves, switches the nextPlayer, and checks if the move has resulted in a win or a draw.

The win condition is checked across the row and column of the latest move, as well as the two diagonals.

If all spots on the board are filled and no player has won (i.e., nbMoves equals 9 and winner is still 0), the game is declared a draw.

Checking for a Win

The checkLine function is used to check if any player has won. It checks if all spots in a line (which can be a row, a column, or a diagonal) are filled by the same player. If so, that player is declared the winner.

Interacting with the Contract

Interactions with the contract are represented as transactions. When a player makes a move by calling the play function, it generates a transaction. This transaction gets logged and can be seen in the right panel of the SmartPy IDE:

An unsuccessful or invalid move would also generate a transaction but with an error indication:

The Second Move and Beyond

The first move in our TicTacToe game is relatively straightforward since the game board is empty. However, things get more interesting with the second move and subsequent moves. These moves not only add pieces to the game board but also invoke the game’s logic to check for potential winners.

After the first move, the nextPlayer value switches to player 2. Now, the play function validates player 2’s move. Similar checks are performed to ensure the move is valid, i.e., the selected grid spot is within the boundaries and is empty.

As each player makes a move, the game’s state evolves. The nbMoves increases, the nextPlayer toggles, and the deck is updated. Also, after each move, the contract checks if there’s a winner or if it’s a draw.

For example, after the first player makes a move in the center of the board at i=1, j=1, the second player can play at a different spot, say i=1, j=2. Both of these moves would be validated and executed successfully, with the corresponding transactions being generated.

The Other Moves and Game Progression

Subsequent moves continue in a similar manner. Each player takes turns to play, choosing an empty spot on the board. After each move, the contract checks for any winning condition. If a player fills an entire row, column, or diagonal with their symbol, the game ends and that player is declared the winner. The winner variable in the contract’s state would be updated accordingly.

It’s important to note that once a player has won, no further moves are valid. Any attempt to make a move after the game has ended would be considered invalid and the corresponding transaction would fail.

The Draw Scenario

In some games, it’s possible that no player achieves a winning condition even after the entire game board is filled. This results in a draw. The contract has been designed to handle this situation as well.

If all spots on the board are filled (nbMoves equals 9) and no player has won (winner remains 0), the game is declared a draw. The draw flag in the contract’s state is set to True, indicating the game ended in a draw. Once again, no further moves are valid after this point. Any attempts to make a move after a draw would also fail.

The second part of the TicTacToe contract’s test scenario demonstrates this drawing scenario. It simulates a series of moves that result in a draw and verifies that the contract handles it correctly.

Pernyataan Formal
* Investasi Kripto melibatkan risiko besar. Lanjutkan dengan hati-hati. Kursus ini tidak dimaksudkan sebagai nasihat investasi.
* Kursus ini dibuat oleh penulis yang telah bergabung dengan Gate Learn. Setiap opini yang dibagikan oleh penulis tidak mewakili Gate Learn.