How to make a TicTacToe App in ReactPy

reactpy tictactoe
Updated: September 3, 2023

A. Introduction

TicTacToe is a two-player game where players take turns to place a piece type or a character such as 'X' / 'O' or 'W' / 'B' on a 3x3 board. The goal is to align three of their pieces horizontally, vertically, or diagonally to win the game. If no one is able to achieve that goal the result is a tie or a draw. I will be using ReactPy [1] to build this application.

The squares on the board are represented by nine buttons. The layout uses a bootstrap flex arranging a single column with three rows where each row has 3 buttons. Each button has a value and function assigned to keep track of the board status that helps in determining whether the game ends.

The final app will be something like this.

Before the game starts.
The game ends.

B. Code Snippets

1. Docstring, imports and constants

"""A Tic-Tac-Toe Web Application

Uses bootstrap 5 to style the elements.

requirements:
    reactpy
"""


from reactpy import component, html, hooks
from reactpy.backend.fastapi import configure, Options
from fastapi import FastAPI


BOOTSTRAP_CSS = html.link(
    {
        'rel': 'stylesheet',
        'href': 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/'
                'dist/css/bootstrap.min.css',
        'integrity': 'sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Y'
                     'z1ztcQTwFspd3yD65VohhpuuCOmLASjC',
        'crossorigin': 'anonymous',
    }
)

PAGE_TITLE = 'ReactPy-TicTactoe'
SQ_SIZE = '60px'
PC_CHAR = {'p1': 'W', 'p2': 'B'}

WIN_RESULT_PATTERN = [
    [0, 1, 2],  # hor
    [3, 4, 5],  # hor
    [6, 7, 8],  # hor
    [0, 4, 8],  # diag
    [2, 4, 6],  # anti-diag
    [0, 3, 6],  # ver
    [1, 4, 7],  # ver
    [2, 5, 8],  # ver
]
  • Use the fastapi backend.
  • Bootstrap css is used for styling.
  • Square size is 60px.
  • The piece for player 1 is 'W' and 'B' for player 2.
  • WIN_RESULT_PATTERN is used to determine the winner of the game.

2. Game States

@component
def Tictactoe():
    stm, set_stm = hooks.use_state(True)  # side to move

    board_status = [None] * 9  # a list from 9 squares
    set_board_status = [None] * 9  # an array of 9 functions
    for i in range(len(board_status)):
        board_status[i], set_board_status[i] = hooks.use_state('')
  • We have stm or side to move, the state to track which player will move next. This is useful to know what piece will be drawn on the board square after every move until the game ends.
  • The board_status is a list of pieces that is moved to a given square like [None, None, ...] up to nine elements. This is just an initialization for 9 elements.
  • The set_board_status is an array of functions that corresponds to the board_status. Basically we have nine functions because of nine squares on the board. Every button press is tracked to update what piece is dropped on the board square.

3. MakeMove

def make_move(event):
    btn_value = event['currentTarget']['value']
    set_board_status[int(btn_value)](
        PC_CHAR['p1'] if stm else PC_CHAR['p2']
    )
    set_stm(not stm)
  • To make a move on the board, each player presses a button on its turn. If player 1 moves, the button text value changes from empty to "W". It will be "B" for player 2.
  • The set_board_status updates the value of the board_status array. If stm is true, "W" will be marked on the button based on the PC_CHAR dictionary. Likewise it will be "B" of stm is false. Then stm is updated for the next player's turn.

4. Utility function

def disable_buttons():
    """Disables button if game is over.
    
    We call this function once we know that a game is over.
    Board square button is disabled if its value is not an empty char.
    The initial value of a square is '' and we just replace it with ' '
    to disable the button.
    """
    for i, v in enumerate(board_status):
        if v == '':
            set_board_status[i](' ')

5. Building a square

@component
def CreateSquare(index):
    """Creates a button to represent a square."""
    return html.button(
        {
            'style': {'width': SQ_SIZE, 'height': SQ_SIZE},
            'class': 'btn shadow-none rounded-1 btn-warning \
                      border-secondary m-0 fw-bold fs-3 \
                      align-items-center justify-content-center',
            'value': index,
            'on_click': make_move,
            'disabled': board_status[index],
        },
        board_status[index]
    )

A very important component for a board game design. It is responsible for creating a single square given an index from [0 to 8]. Note we use bootstrap 5 to style the components.

  • html.button - creates a button element. It has an attribute enclosed in curly braces {} formatted as a Python dictionary and a content board_status[index].
  • style
    • The width and height values are the size of the button. Since they are the same, the button figure is a square. The value SQ_SIZE is 60px.
  • The class
    • btn - the button.
    • shadow-none - removes the button shadow.
    • rounded-1 - creates a round corner of the square button.
    • btn-warning - sets the button color to gold/yellow.
    • border-secondary - the color of the button border.
    • m-0 - a margin with value 0, meaning there is no space around its surrounding elements.
    • fw-bold - bolds the text on the button.
    • fs-3 - the font-size.
    • align-items-center - centers an item vertically.
    • justify-content-center - centers content horizontally.
  • value - the value returned when this button is clicked. It is used to track which button is pressed to update board status.
  • on_click - defines a function to execute if button is clicked. The value make_move function will be executed. This is the point where users interact with the application.
  • disabled - disables a button or clicking a button does nothing, no function will be run. When the board_status value at a given index is '' or empty char, the text on the button will not be shown.
  • board_status[index] - this is the content of the button. This is the text displayed on top of the button. If the value is an empty char or '' or a white space ' ' nothing will be shown on the button.

6. Generate Squares

@component
def MakeBoardSquares(values: list):
    """Creates a row of board squares as buttons."""
    return html.div(
        {'class': 'd-flex flex-row'},
        [CreateSquare(i) for i in values]
    )
  • This is our board square builder. It creates the nine squares for the whole TicTacToe board.
  • The values parameter is a list like [1, 2, 3]. It will create the 3 squares along the row with flex-row class. It is called 3 times to build three rows of three squares to complete the 3x3 grid.

7. Who is the winner

@component
def determine_winner():
    result = None
    for res in WIN_RESULT_PATTERN:
        if all([board_status[v] == PC_CHAR['p1'] for v in res]):
            result = PC_CHAR['p1']
            break
        if all([board_status[v] == PC_CHAR['p2'] for v in res]):
            result = PC_CHAR['p2']
            break
    if result is not None:
        disable_buttons()
        return html.h5({'class': 'text-success'}, f'The winner is {result}.')
    done = all([bs == PC_CHAR['p2'] or bs == PC_CHAR['p1']
                for bs in board_status])
    if done:
        disable_buttons()
        return html.h5({'class': 'text-secondary'}, 'The result is a draw.')
    return html.h5({'class': 'text-muted'}, 'No winner yet.')

The most important part of this component is the for loop.

for res in WIN_RESULT_PATTERN:
    if all([board_status[v] == PC_CHAR['p1'] for v in res]):
        result = PC_CHAR['p1']
        break
    if all([board_status[v] == PC_CHAR['p2'] for v in res]):
        result = PC_CHAR['p2']
        break

The constant list of lists WIN_RESULT_PATTERN yields a res. So the res can be [0, 1, 2], [3, 4, 5] and others, see its values in the Docstring, imports and constants section.

Each value in the res is further parsed in the list comprehension.

[board_status[v] == PC_CHAR['p1'] for v in res]

The board_status is a list with an initial values of ''

board_status = ['', '', '', '', '', '', '', '', '']

That list has an index.

[0, 1, 2, 3, 4, 5, 6, 7, 8]

Can also be written as:

[0, 1, 2,
 3, 4, 5,
 6, 7, 8]

If the first player drops a piece at board index 0, or the top left corner, the board_status would look like this.

board_status = ['W', '', '', '', '', '', '', '', '']

If the second player drops a piece at board index 3, the board_status would look like this.

board_status = ['W', '', '', 'B', '', '', '', '', '']

For demonstration purposes we end up with this position.

board_status = ['W', 'W', 'W', 'B', 'B', '', '', '', '']

Or like this.

board_status = ['W', 'W', 'W',
                'B', 'B', '',
                '',  '',  '']

Here we have a winner, the function determine_winner() can detect this. We have three 'W' at indices 0, 1 and 2. This belongs to a [0, 1, 2] pattern. The condition board_status[v] == PC_CHAR['p1'] from all([board_status[v] == PC_CHAR['p1'] for v in res]) makes the three comparisons to True. Since all three values are True we set this player to win the game.

8. App layout

return html.div(
        BOOTSTRAP_CSS,
        html.div(
            html.div({'class': 'container justify-content-center mt-3 text-center'},
                html.div(
                    html.h1({'class': 'text-dark'}, 'TicTacToe'),
                    html.h6({'class': 'text-primary'}, 'Built by ReactPy'),
                    html.p(),

                    html.div(
                        {'class': 'container d-flex justify-content-center'},
                        html.div(
                            {'class': 'd-flex flex-column justify-content-center'},
                            html.div(MakeBoardSquares([0, 1, 2])),
                            html.div(MakeBoardSquares([3, 4, 5])),
                            html.div(MakeBoardSquares([6, 7, 8])),
                        ),
                    ),
                    html.br(),
                    html.div({'class': 'container d-flex justify-content-center'},
                        determine_winner(),
                    ),
                ),
            ),
        ),
    )
  • This is the location for bootstrap CSS.
  • We call other components together with some syling to build the App.

C. Full code

tictactoe.py
"""A Tic-Tac-Toe Web Application

Uses bootstrap 5 to style the elements.

requirements:
    reactpy
"""


from reactpy import component, html, hooks
from reactpy.backend.fastapi import configure, Options
from fastapi import FastAPI


BOOTSTRAP_CSS = html.link(
    {
        'rel': 'stylesheet',
        'href': 'https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/'
                'dist/css/bootstrap.min.css',
        'integrity': 'sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Y'
                     'z1ztcQTwFspd3yD65VohhpuuCOmLASjC',
        'crossorigin': 'anonymous',
    }
)

PAGE_TITLE = 'ReactPy-TicTactoe'
SQ_SIZE = '60px'
PC_CHAR = {'p1': 'W', 'p2': 'B'}

WIN_RESULT_PATTERN = [
    [0, 1, 2],  # hor
    [3, 4, 5],  # hor
    [6, 7, 8],  # hor
    [0, 4, 8],  # diag
    [2, 4, 6],  # anti-diag
    [0, 3, 6],  # ver
    [1, 4, 7],  # ver
    [2, 5, 8],  # ver
]


@component
def Tictactoe():
    stm, set_stm = hooks.use_state(True)  # side to move

    board_status = [None] * 9  # a list from 9 squares
    set_board_status = [None] * 9  # an array of 9 functions
    for i in range(len(board_status)):
        board_status[i], set_board_status[i] = hooks.use_state('')
...

The full source code can be found in my github ReactPy-TicTacToe [2] repository.

Requirements

reactpy[fastapi]
uvicorn[standard]

Run the app with the following command line.

uvicorn tictactoe:app

D. Summary

The TicTacToe App is built with a simple layout. Single column of Title, board layout and result box. The layout is constructed from three rows of three squares. Each square is a button, styled by bootstrap 5 [3]. We use a list as our board status and a list of functions to handle states utilizing the hook's use_state.

The Github ReactPy-TicTacToe [2] code is now updated to include the new game button.

E. References

[1]. ReactPy for building user interfaces in Python
[2]. Github ReactPy-TicTacToe repository
[3]. Bootstrap 5 - an HTML, CSS, and JavaScript framework for building responsive web sites