How to Create a Tic Tac Toe Python Game With Pygame

Tic Tac Toe is a classic paper-and-pencil game where two players take turns marking spaces in a 3×3 grid, aiming to get three of their marks in a row horizontally, vertically, or diagonally, while preventing their opponent from doing the same.

Today, we embark on a journey to explore the timeless game of Tic Tac Toe, leveraging the power of Python and the renowned game development library Pygame.

Setting Up Your Environment and Installing Dependencies

To kickstart our Tic Tac Toe game development journey with Pygame, let’s establish a virtual environment to manage our project dependencies. Employing a virtual environment guarantees the isolation of our project’s dependencies from the system-wide Python installation, fostering superior dependency management and mitigating conflicts between distinct projects.

Initially, we’ll craft a virtual environment by executing the following command in our terminal or command prompt.

Bash
$ python3 -m venv tic_tac_venv

This command instantiates a virtual environment named tic_tac_venv within the current directory. To activate the virtual environment, platform-specific commands can be utilized.

  • On Windows
Bash
$ tic_tac_venv\Scripts\activate
  • On macOS and Linux
Bash
$ source tic_tac_venv/bin/activate

After activating the virtual environment, we can proceed to install the necessary module, Pygame. Pygame comprises a collection of Python modules tailored for video game development. We’ll employ pip, Python’s package manager, to install it.

Bash
$ pip install pygame

With Pygame successfully installed, we’re ready to delve into crafting the Python script for our Tic Tac Toe game. Let’s advance by creating a Python file, such as tic_tac_game.py, within our project directory.

Initialization and Setup

import pygame
import sys
import random

# Initialize Pygame
pygame.init()

# Constants
WIDTH, HEIGHT = 600, 600
LINE_WIDTH = 15
BOARD_ROWS, BOARD_COLS = 3, 3
SQUARE_SIZE = WIDTH // BOARD_COLS
CIRCLE_RADIUS = SQUARE_SIZE // 3
CIRCLE_WIDTH = 15
CROSS_WIDTH = 25
SPACE = SQUARE_SIZE // 4
# Colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
BG_COLOR = (28, 170, 156)

# Initialize the screen
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tic-Tac-Toe")
screen.fill(BG_COLOR)

# Fonts
pygame.font.init()
font = pygame.font.SysFont("comicsans", 75)

# Board
board = [[" " for _ in range(BOARD_COLS)] for _ in range(BOARD_ROWS)]

This code snippet initializes the Pygame library, which provides functionalities for developing games in Python. It imports the required modules. pygame, sys, and random. Pygame is initialized using the pygame.init() function, setting up the necessary environment for game development.

Several constants are then defined to configure the game’s settings:

  • WIDTH and HEIGHT set the dimensions of the game window to 600×600 pixels.
  • LINE_WIDTH determines the thickness of the lines used to draw the Tic-Tac-Toe grid.
  • BOARD_ROWS and BOARD_COLS specify the number of rows and columns on the game board, both set to 3 for a standard Tic-Tac-Toe grid.
  • SQUARE_SIZE calculates the size of each square on the game board based on the window dimensions and the number of rows and columns.
  • CIRCLE_RADIUS sets the radius of the circle used to draw the O symbol.
  • CIRCLE_WIDTH determines the thickness of the circle’s outline.
  • CROSS_WIDTH specifies the thickness of the lines used to draw the X symbol.
  • SPACE defines the spacing between the symbol and the edge of each square.

Following the constants, a series of color constants are defined using RGB values. These include WHITE, BLACK, RED, GREEN, BLUE, and BG_COLOR, which represents the background color of the game window.

The game screen is then initialized using pygame.display.set_mode() with the specified width and height. The window’s title is set to “Tic-Tac-Toe” using pygame.display.set_caption(), and the screen is filled with the background color defined earlier.

Additionally, the code initializes fonts using pygame.font.init() and selects the “comicsans” font with a size of 75 points for displaying text within the game.

Finally, a 2D list named board is created to represent the Tic-Tac-Toe grid. Each element in the list is initialized to an empty space " " using list comprehension, resulting in a 3×3 grid of empty spaces, ready to store player moves during gameplay.

Drawing Functions

def draw_lines():
    # Horizontal lines
    pygame.draw.line(screen, WHITE, (0, SQUARE_SIZE), (WIDTH, SQUARE_SIZE), LINE_WIDTH)
    pygame.draw.line(screen, WHITE, (0, 2 * SQUARE_SIZE), (WIDTH, 2 * SQUARE_SIZE), LINE_WIDTH)
    # Vertical lines
    pygame.draw.line(screen, WHITE, (SQUARE_SIZE, 0), (SQUARE_SIZE, HEIGHT), LINE_WIDTH)
    pygame.draw.line(screen, WHITE, (2 * SQUARE_SIZE, 0), (2 * SQUARE_SIZE, HEIGHT), LINE_WIDTH)


def draw_figures():
    for row in range(BOARD_ROWS):
        for col in range(BOARD_COLS):
            if board[row][col] == "X":
                pygame.draw.line(screen, RED, (col * SQUARE_SIZE + SPACE, row * SQUARE_SIZE + SQUARE_SIZE - SPACE),
                                 (col * SQUARE_SIZE + SQUARE_SIZE - SPACE, row * SQUARE_SIZE + SPACE), CROSS_WIDTH)
                pygame.draw.line(screen, RED, (col * SQUARE_SIZE + SPACE, row * SQUARE_SIZE + SPACE),
                                 (col * SQUARE_SIZE + SQUARE_SIZE - SPACE, row * SQUARE_SIZE + SQUARE_SIZE - SPACE),
                                 CROSS_WIDTH)
            elif board[row][col] == "O":
                pygame.draw.circle(screen, BLUE, (int(col * SQUARE_SIZE + SQUARE_SIZE / 2),
                                                  int(row * SQUARE_SIZE + SQUARE_SIZE / 2)), CIRCLE_RADIUS,
                                   CIRCLE_WIDTH)

The provided code snippet contains two functions: draw_lines() and draw_figures(), which are responsible for rendering the Tic-Tac-Toe grid lines and player symbols (X’s and O’s) onto the game screen, respectively.

The draw_lines() function draws the horizontal and vertical lines that form the grid of the Tic-Tac-Toe board. It utilizes the pygame.draw.line() function to draw lines on the screen surface (screen). Four lines are drawn to create the Tic-Tac-Toe grid: two horizontal lines and two vertical lines. The horizontal lines are drawn at positions (0, SQUARE_SIZE) and (0, 2 * SQUARE_SIZE) with lengths spanning the width of the screen (WIDTH) and a specified line width (LINE_WIDTH). Similarly, the vertical lines are drawn at positions (SQUARE_SIZE, 0) and (2 * SQUARE_SIZE, 0) with lengths spanning the height of the screen (HEIGHT) and the same line width. These lines are drawn using the WHITE color constant defined earlier.

The draw_figures() function is responsible for rendering the player symbols (X’s and O’s) onto the game screen based on the current state of the game board (board). It iterates over each cell in the board using nested loops. If the current cell contains an “X” or “O”, the function uses either lines or circles to draw the respective symbol. For an “X”, two diagonal lines are drawn to create the X shape, while for an “O”, a circle is drawn at the center of the corresponding square on the grid. The color of the symbols is determined by the RED and BLUE color constants for “X” and “O” respectively, and the line thickness for “X” symbols is set by the CROSS_WIDTH constant, while the circle’s outline width for “O” symbols is set by CIRCLE_WIDTH.

Game Logic

def mark_square(row, col, player):
    board[row][col] = player

The mark_square() function is responsible for updating the game board with the symbol (X or O) placed by a player at a specific position identified by its row and column coordinates.

This function takes three parameters: row, col, and player. row and col represent the coordinates of the square on the game board where the player wants to place their symbol, and player indicates whether it’s “X” or “O“.

def available_square(row, col):
    return board[row][col] == " "

The available_square() function checks if a specific square on the game board is available or empty. It takes two parameters, row and col, representing the row and column indices of the square to be checked.

Inside the function, it accesses the corresponding element in the board 2D list using the provided row and col indices. If the value stored in that element is an empty space (" "), the function returns True, indicating that the square is available for a player to make a move. If the square is already occupied by an “X” or “O”, the function returns False, indicating that the square is not available for further moves.

def is_board_full():
    for row in range(BOARD_ROWS):
        for col in range(BOARD_COLS):
            if board[row][col] == " ":
                return False
    return True

The is_board_full() function checks whether the game board is completely filled with player symbols, indicating that the game has reached a draw or tie state. It iterates over each cell of the game board using nested loops, covering all rows and columns.

Within the loops, it examines each cell on the board. If it finds an empty space (represented by " "), indicating that the board is not yet full, the function immediately returns False, indicating that the board is not full.

If the function completes the iteration over all cells without finding any empty spaces, it returns True, indicating that the board is full and there are no more available moves left. This signals that the game has reached a draw state, as no player has won, and there are no empty squares left for additional moves.

def check_win(player):
    # Check rows
    for row in range(BOARD_ROWS):
        if board[row][0] == board[row][1] == board[row][2] == player:
            draw_win_line((row, 0), (row, 2))
            return True
    # Check columns
    for col in range(BOARD_COLS):
        if board[0][col] == board[1][col] == board[2][col] == player:
            draw_win_line((0, col), (2, col))
            return True
    # Check diagonals
    if board[0][0] == board[1][1] == board[2][2] == player:
        draw_win_line((0, 0), (2, 2))
        return True
    if board[0][2] == board[1][1] == board[2][0] == player:
        draw_win_line((0, 2), (2, 0))
        return True
    return False

check_win(player) function is responsible for determining if a player has won the game by achieving a winning combination of their symbols (X or O) on the game board. It takes a single parameter player, indicating the symbol (X or O) being checked for a win.

The function first checks for winning combinations in the rows, iterating through each row of the game board using a for loop. Within each row, it compares the symbols in consecutive columns (indices 0, 1, and 2) to check if they all match the player symbol. If a winning combination is found in any row, the function calls the draw_win_line() function to visually indicate the winning line on the game board and returns True to indicate that the player has won.

Next, the function checks for winning combinations in the columns by iterating through each column of the game board using another for loop. It compares the symbols in consecutive rows (indices 0, 1, and 2) to check if they all match the player symbol. If a winning combination is found in any column, the function again calls draw_win_line() to visually indicate the winning line and returns True.

After checking the rows and columns, the function proceeds to check for winning combinations along the diagonals. It first checks the main diagonal (from top-left to bottom-right) by comparing the symbols at indices (0, 0), (1, 1), and (2, 2). If these symbols all match the player symbol, the function calls draw_win_line() to indicate the winning line and returns True. Similarly, it checks the other diagonal (from top-right to bottom-left) by comparing the symbols at indices (0, 2), (1, 1), and (2, 0), and returns True if a winning combination is found.

If none of the above winning conditions are met, indicating that no winning combination has been achieved by the player, the function returns False to signify that the game is still ongoing and no winner has been determined yet.

def draw_win_line(start, end):
    start_x = start[1] * SQUARE_SIZE + SQUARE_SIZE // 2
    start_y = start[0] * SQUARE_SIZE + SQUARE_SIZE // 2
    end_x = end[1] * SQUARE_SIZE + SQUARE_SIZE // 2
    end_y = end[0] * SQUARE_SIZE + SQUARE_SIZE // 2
    pygame.draw.line(screen, GREEN, (start_x, start_y), (end_x, end_y), LINE_WIDTH)

The above function is responsible for visually drawing a line on the game screen to represent a winning combination achieved by a player. It takes two parameters, start and end, which specify the coordinates of the starting and ending points of the line to be drawn.

Inside the function, the x and y coordinates of the starting and ending points of the line are calculated based on the provided start and end coordinates. These coordinates are adjusted to be at the center of the respective squares on the game board where the line will be drawn. The adjustment involves multiplying the row and column indices by SQUARE_SIZE (the size of each square) and adding half the SQUARE_SIZE to ensure the line is drawn at the center of the square.

The calculated coordinates are stored in variables start_x, start_y, end_x, and end_y, representing the x and y coordinates of the starting and ending points of the line, respectively.

Finally, the pygame.draw.line() function is called to draw the line on the game screen. It takes several arguments:

  • screen: The surface on which the line will be drawn.
  • GREEN: The color of the line, specified as an RGB tuple.
  • (start_x, start_y): The starting point of the line.
  • (end_x, end_y): The ending point of the line.
  • LINE_WIDTH: The thickness of the line.

When executed, this function draws a line on the game screen connecting the specified starting and ending points, visually indicating the winning combination achieved by a player. The color of the line is set to green, and its thickness is determined by the LINE_WIDTH constant defined earlier.

def draw_winner_text(player):
    winner_text = font.render(f"{player} wins!", True, WHITE)
    screen.blit(winner_text, (WIDTH // 2 - winner_text.get_width() // 2, HEIGHT // 2 - winner_text.get_height() // 2))
    pygame.display.update()
    pygame.time.delay(2000)

The draw_winner_text() function is responsible for displaying a text message indicating which player has won the game. It takes one parameter, player, which specifies the winning player (either “X” or “O”).

Inside the function, a text surface is created using the font.render() function provided by Pygame. This function renders the text string "{player} wins!" where {player} is substituted with the actual winning player, creating a message like “X wins!” or “O wins!”. The text is rendered using the font previously initialized with the specified font type and size. The True parameter indicates that anti-aliasing should be applied to the text to improve its appearance.

The rendered text surface is stored in the variable winner_text.

The screen.blit() function is then used to draw the winner_text onto the game screen. The position where the text will be drawn is specified as a tuple (x, y). Here, the x-coordinate is calculated as (WIDTH // 2 - winner_text.get_width() // 2) to center the text horizontally on the screen, and the y-coordinate is similarly calculated as (HEIGHT // 2 - winner_text.get_height() // 2) to center the text vertically. This ensures that the text is displayed at the center of the game window.

After drawing the text onto the screen, pygame.display.update() is called to update the display and make the text visible.

Finally, pygame.time.delay(2000) introduces a 2-second delay, pausing the game for a brief moment to allow the players to see the winning message before the game resets or continues. This delay provides a visual cue to the players that the game has ended and a winner has been declared.

def minimax(board, depth, is_maximizing):
    if check_win("X"):
        return -1
    elif check_win("O"):
        return 1
    elif is_board_full():
        return 0

    if is_maximizing:
        best_score = -float("inf")
        for row in range(BOARD_ROWS):
            for col in range(BOARD_COLS):
                if board[row][col] == " ":
                    board[row][col] = "O"
                    score = minimax(board, depth + 1, False)
                    board[row][col] = " "
                    best_score = max(score, best_score)
        return best_score
    else:
        best_score = float("inf")
        for row in range(BOARD_ROWS):
            for col in range(BOARD_COLS):
                if board[row][col] == " ":
                    board[row][col] = "X"
                    score = minimax(board, depth + 1, True)
                    board[row][col] = " "
                    best_score = min(score, best_score)
        return best_score

The minimax() function implements the minimax algorithm to recursively evaluate possible moves in Tic-Tac-Toe. It considers all potential moves, alternating between maximizing and minimizing player scores. It assigns scores based on game outcomes (win, loss, or draw) and selects the move with the highest score for the maximizing player and the lowest score for the minimizing player, ultimately determining the best move for the current player.

def get_best_move():
    best_score = -float("inf")
    best_move = ()
    for row in range(BOARD_ROWS):
        for col in range(BOARD_COLS):
            if board[row][col] == " ":
                board[row][col] = "O"
                score = minimax(board, 0, False)
                board[row][col] = " "
                if score > best_score:
                    best_score = score
                    best_move = (row, col)
    return best_move

The get_best_move() function aims to determine the best move for the AI player (represented by “O”) in Tic-Tac-Toe using the minimax algorithm. It iterates through each empty square on the game board, simulates placing an “O” in that position, and evaluates the resulting game state using the minimax() function.

The function initializes best_score to negative infinity and best_move as an empty tuple. It then iterates through each empty square on the board, simulating the placement of “O” in that position. After evaluating the resulting game state using the minimax() function, it compares the score obtained with the current best_score. If the new score is higher, it updates best_score and best_move accordingly.

After iterating through all possible moves, the function returns best_move, representing the position on the board where the AI player should make its move to maximize its chances of winning or minimizing its chances of losing.

def restart_game():
    screen.fill(BG_COLOR)
    draw_lines()
    for row in range(BOARD_ROWS):
        for col in range(BOARD_COLS):
            board[row][col] = " "

The restart_game() function resets the Tic-Tac-Toe game to its initial state for a new round or game session.

First, it clears the game screen by filling it with the background color specified by BG_COLOR. Then, it redraws the grid lines on the game board using the draw_lines() function, ensuring that the grid is properly displayed.

Next, it iterates through each cell of the game board using nested for loops over the rows and columns. For each cell, it sets its value to an empty space " ", effectively clearing any existing player symbols from the board.

By performing these actions, the function effectively resets the game environment, allowing players to start a new game or round with a clean game board ready for gameplay.

def main():
    player_turn = random.choice(["X", "O"])
    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
                pygame.quit()
                sys.exit()
            if event.type == pygame.MOUSEBUTTONDOWN and player_turn == "X":
                x = event.pos[1] // SQUARE_SIZE
                y = event.pos[0] // SQUARE_SIZE
                if available_square(x, y):
                    mark_square(x, y, "X")
                    if check_win("X"):
                        draw_winner_text("X")
                        restart_game()
                    player_turn = "O"
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    restart_game()

        if player_turn == "O" and not is_board_full():
            row, col = get_best_move()
            mark_square(row, col, "O")
            if check_win("O"):
                draw_winner_text("O")
                restart_game()
            player_turn = "X"

        screen.fill(BG_COLOR)
        draw_lines()
        draw_figures()
        if is_board_full() and not check_win("X") and not check_win("O"):
            draw_winner_text("Tie")
            restart_game()

        pygame.display.update()
        pygame.time.delay(100)

The main() function is the central component of the Tic-Tac-Toe game, responsible for managing gameplay and interactions. It begins by randomly assigning the starting player as “X” or “O”. Within a loop, it continuously listens for player input events such as mouse clicks or key presses.

If the player clicks to close the window, the game gracefully quits. If a mouse click occurs and it’s the turn of “X”, the game checks for available squares, marks one for “X”, and checks for a win condition. Similarly, if the player presses the “r” key, the game restarts.

For the AI player (“O”), it selects the best move, marks the square, and checks for a win. After each move, the game updates the screen, redraws the grid and player symbols, and introduces a slight delay to control pacing. This function orchestrates the gameplay loop, ensuring smooth progression of the game, player input handling, and AI decision-making.

if __name__ == "__main__":
    main()

This conditional statement ensures that the main() function is executed when the script is run as the main program. It checks if the script is being executed directly, as opposed to being imported as a module in another script. If it’s the main program, it calls the main() function, initiating the execution of the Tic-Tac-Toe game.

Conclusion

Let’s assemble all code segments and compile our Tic Tac Toe game script. To begin playing, simply type ‘python3 tic_tac_game.py‘.

Bash
$ python3 tic_tac_game.py

This will launch the game window, allowing you to play the Tic Tac Toe game.

tic_tac_game.py

Explore the complete source code on GitHub and experience the classic Tic Tac Toe game, brought to life with Python and Pygame!


DarkLight
×