Creating a Tic Tac Toe game in Pygame is a great way to practice Python programming and understand game development basics.

This guide will walk you through creating a two-player Tic Tac Toe game with Pygame and then modifying it to include an AI opponent.

If you want a pure Python version with the command line, check out the previous article: https://developer-service.blog/building-an-ai-powered-tic-tac-toe-in-python/

You can also follow along with the video version of this post:


What is Pygame?

Pygame is a set of Python modules designed for writing video games.

It provides functionalities and tools to create games and multimedia applications. Pygame is built on top of the Simple DirectMedia Layer (SDL) library, which handles lower-level game-related tasks such as graphics, sound, and input.

Key Features of Pygame

  • Cross-Platform: Pygame runs on nearly every operating system and hardware platform where Python is supported, including Windows, macOS, and Linux.
  • 2D Graphics: Pygame provides easy-to-use methods for drawing shapes, rendering images, and handling animations.
  • Sound and Music: It supports various audio formats and allows for the playback of sound effects and background music.
  • Input Handling: Pygame can handle keyboard, mouse, and joystick input, making it suitable for creating interactive applications and games.
  • Simple API: The API is designed to be easy to learn for beginners, with straightforward functions and a clear structure.
  • Community and Resources: Pygame has a large community and plenty of tutorials, making it easier to find help and learn through examples.

Setting Up Your Environment

Before we begin, ensure you have Pygame installed. You can install it using pip:

pip install pygame

This is the only additional library needed to write our game.


Two-Player Tic Tac Toe

First, let's create a basic Tic Tac Toe game where two human players can play against each other.

We go step by step in building our game.

Step 1: Import Required Modules and Initialize Pygame

Start by importing Pygame and initializing it.

import pygame
import sys

pygame.init()

Step 2: Define Constants and Initialize the Screen

Set up the screen dimensions, colors, and other constants.

# Screen size
WIDTH, HEIGHT = 600, 600
LINE_WIDTH = 15
WIN_LINE_WIDTH = 15

# Color definitions
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)

# Board size
BOARD_ROWS = 3
BOARD_COLS = 3
SQUARE_SIZE = WIDTH // BOARD_COLS
CIRCLE_RADIUS = SQUARE_SIZE // 3
CIRCLE_WIDTH = 15
CROSS_WIDTH = 25
SPACE = SQUARE_SIZE // 4

# Initialize screen
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Tic Tac Toe')
screen.fill(WHITE)

Here's a breakdown of what it does:

  • Screen size: The variables WIDTH and HEIGHT are set to 600, indicating that the game window will be a square of 600x600 pixels. LINE_WIDTH and WIN_LINE_WIDTH are set to 15.
  • Color definitions: The variables WHITE, BLACK, and RED are defined as tuples, each containing three values between 0 and 255. These are RGB values, a common way to define colors in programming.
  • Board size: The variables BOARD_ROWS and BOARD_COLS are set to 3, indicating that the game board will be a 3x3 grid, as is standard for Tic Tac Toe. SQUARE_SIZE is calculated by dividing WIDTH by BOARD_COLS, meaning each square on the board will be 200x200 pixels. CIRCLE_RADIUS is set to one-third of SQUARE_SIZE, and CIRCLE_WIDTH and CROSS_WIDTH are set to 15 and 25 respectively. SPACE is set to one-fourth of SQUARE_SIZE.
  • Initialize screen: The pygame.display.set_mode() function is used to create a game window of the size specified by WIDTH and HEIGHT. The pygame.display.set_caption() function is used to set the title of the game window to 'Tic Tac Toe'. The screen.fill(WHITE) function is used to set the background color of the game window to white.

Step 3: Initialize the Game Board

Create a 2D list to represent the game board.

board = [[0 for _ in range(BOARD_COLS)] for _ in range(BOARD_ROWS)]

The variable board is initialized as a list. The list comprehension [0 for _ in range(BOARD_COLS)] creates a new list of zeros with a length equal to BOARD_COLS, which is 3 in this case. The _ variable is a throwaway variable that is not used in the code.

The outer list comprehension [0 for _ in range(BOARD_ROWS)] repeats the inner list comprehension BOARD_ROWS number of times, which is also 3. The result is a 3x3 list of lists, where each inner list represents a row on the game board, and the zeros in the inner lists represent the individual squares on the board.

Step 4: Draw the Board

Create functions to draw the grid lines and the Xs and Os.

def draw_lines():
    # Horizontal lines
    pygame.draw.line(screen, BLACK, (0, SQUARE_SIZE), (WIDTH, SQUARE_SIZE), LINE_WIDTH)
    pygame.draw.line(screen, BLACK, (0, 2 * SQUARE_SIZE), (WIDTH, 2 * SQUARE_SIZE), LINE_WIDTH)
    # Vertical lines
    pygame.draw.line(screen, BLACK, (SQUARE_SIZE, 0), (SQUARE_SIZE, HEIGHT), LINE_WIDTH)
    pygame.draw.line(screen, BLACK, (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] == 1:
                pygame.draw.circle(screen, BLACK, (
                    int(col * SQUARE_SIZE + SQUARE_SIZE // 2), int(row * SQUARE_SIZE + SQUARE_SIZE // 2)),
                                   CIRCLE_RADIUS,
                                   CIRCLE_WIDTH)
            elif board[row][col] == 2:
                pygame.draw.line(screen, BLACK, (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, BLACK, (col * SQUARE_SIZE + SPACE, row * SQUARE_SIZE + SPACE),
                                 (col * SQUARE_SIZE + SQUARE_SIZE - SPACE, row * SQUARE_SIZE + SQUARE_SIZE - SPACE),
                                 CROSS_WIDTH)

This code defines two functions, draw_lines() and draw_figures(), that are used to draw the game board and the X's and O's on the board.

  • draw_lines() function:

This function is responsible for drawing the horizontal and vertical lines that make up the 3x3 grid of the Tic Tac Toe game board.

The pygame.draw.line() function is used to draw each line. It takes five arguments: the first is the surface to draw on (in this case, the screen), the second is the color of the line (BLACK), the third and fourth are the starting and ending coordinates of the line, and the fifth is the width of the line (LINE_WIDTH).

The function draws two horizontal lines and two vertical lines, with the coordinates calculated using the SQUARE_SIZE variable, which is the size of each square on the board.

  • draw_figures() function:

This function is responsible for drawing the X's and O's on the game board, based on the current state of the game.

The function uses a nested for loop to iterate over each square on the board, represented by the board list. The board list is a 3x3 list of lists, where each inner list represents a row on the board, and the zeros in the inner lists represent the individual squares on the board.

If the value of a square is 1, the function uses the pygame.draw.circle() function to draw an O on the square. The function takes four arguments: the first is the surface to draw on (in this case, the screen), the second is the color of the circle (BLACK), the third is the center coordinates of the circle, and the fourth is the radius of the circle (CIRCLE_RADIUS).

If the value of a square is 2, the function uses the pygame.draw.line() function to draw an X on the square. The function draws two lines that intersect at the center of the square to form the X. The coordinates of the lines are calculated using the SQUARE_SIZE and SPACE variables.

Step 5: Handle Player Moves

Create functions to mark the squares and check if a player has won.

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


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


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


def check_win(_player):
    # Vertical win
    for col in range(BOARD_COLS):
        if board[0][col] == _player and board[1][col] == _player and board[2][col] == _player:
            print(f"Vertical win on column {col} by player {_player}")
            draw_vertical_winning_line(col)
            return True

    # Horizontal win
    for row in range(BOARD_ROWS):
        if board[row][0] == _player and board[row][1] == _player and board[row][2] == _player:
            print(f"Horizontal win on row {row} by player {_player}")
            draw_horizontal_winning_line(row)
            return True

    # Ascending diagonal win
    if board[2][0] == _player and board[1][1] == _player and board[0][2] == _player:
        print(f"Ascending diagonal win by player {_player}")
        draw_asc_diagonal()
        return True

    # Descending diagonal win
    if board[0][0] == _player and board[1][1] == _player and board[2][2] == _player:
        print(f"Descending diagonal win by player {_player}")
        draw_desc_diagonal()
        return True

    return False

This code defines four functions, mark_square(), available_square(), is_board_full(), and check_win(), that are used to manage the state of the game board and to determine the winner of the game.

  • mark_square(row, col, _player) function:

This function is responsible for marking a square on the game board with the current player's marker (X or O). It takes three arguments: row and col are the coordinates of the square to be marked, and _player is the current player (1 for X and 2 for O).

The function assigns the value of _player to the square at the specified coordinates in the board list.

  • available_square(row, col) function:

This function is responsible for determining whether a square on the game board is available to be marked or not. It takes two arguments: row and col are the coordinates of the square to be checked.

The function checks the value of the square at the specified coordinates in the board list. If the value is 0, the square is available and the function returns True. If the value is 1 or 2, the square is already marked and the function returns False.

  • is_board_full() function:

This function is responsible for determining whether the game board is full or not.

The function uses a nested for loop to iterate over each square on the board, represented by the board list. If the value of a square is 0, the function returns False immediately, indicating that the board is not full. If the loop completes without finding any squares with a value of 0, the function returns True, indicating that the board is full.

  • check_win(_player) function:

This function is responsible for determining whether the current player has won the game or not. It takes one argument: _player is the current player (1 for X and 2 for O).

The function checks for four possible winning conditions:

  • Vertical win: The function checks each column of the board to see if the current player has marked all three squares in the column. If a winning condition is found, the function prints a message to the console, draws a vertical line on the screen to indicate the winning column, and returns True.
  • Horizontal win: The function checks each row of the board to see if the current player has marked all three squares in the row. If a winning condition is found, the function prints a message to the console, draws a horizontal line on the screen to indicate the winning row, and returns True.
  • Ascending diagonal win: The function checks the ascending diagonal of the board (from the top-left to the bottom-right) to see if the current player has marked all three squares in the diagonal. If a winning condition is found, the function prints a message to the console, draws an ascending diagonal line on the screen to indicate the winning diagonal, and returns True.
  • Descending diagonal win: The function checks the descending diagonal of the board (from the top-right to the bottom-left) to see if the current player has marked all three squares in the diagonal. If a winning condition is found, the function prints a message to the console, draws a descending diagonal line on the screen to indicate the winning diagonal, and returns True.

If no winning conditions are found, the function returns False.

Step 6: Draw Winning Lines

These functions are called from the check_win() function when a winning condition is detected.

def draw_vertical_winning_line(col):
    pos_x = col * SQUARE_SIZE + SQUARE_SIZE // 2
    color = BLACK
    pygame.draw.line(screen, color, (pos_x, 15), (pos_x, HEIGHT - 15), WIN_LINE_WIDTH)


def draw_horizontal_winning_line(row):
    pos_y = row * SQUARE_SIZE + SQUARE_SIZE // 2
    color = BLACK
    pygame.draw.line(screen, color, (15, pos_y), (WIDTH - 15, pos_y), WIN_LINE_WIDTH)


def draw_asc_diagonal():
    color = BLACK
    pygame.draw.line(screen, color, (15, HEIGHT - 15), (WIDTH - 15, 15), WIN_LINE_WIDTH)


def draw_desc_diagonal():
    color = BLACK
    pygame.draw.line(screen, color, (15, 15), (WIDTH - 15, HEIGHT - 15), WIN_LINE_WIDTH)

This code defines four functions, draw_vertical_winning_line(), draw_horizontal_winning_line(), draw_asc_diagonal(), and draw_desc_diagonal(), that are used to draw lines on the screen to indicate the winning squares.

  • draw_vertical_winning_line(col) function:

This function is responsible for drawing a vertical line on the screen to indicate the winning squares in a vertical winning condition. It takes one argument: col is the index of the column that contains the winning squares.

The function calculates the x-coordinate of the center of the column using the SQUARE_SIZE variable. It then uses the pygame.draw.line() function to draw a line from the top of the screen to the bottom of the screen, passing through the center of the column. The line is drawn in the BLACK color and has a width of WIN_LINE_WIDTH pixels.

  • draw_horizontal_winning_line(row) function:

This function is responsible for drawing a horizontal line on the screen to indicate the winning squares in a horizontal winning condition. It takes one argument: row is the index of the row that contains the winning squares.

The function calculates the y-coordinate of the center of the row using the SQUARE_SIZE variable. It then uses the pygame.draw.line() function to draw a line from the left of the screen to the right of the screen, passing through the center of the row. The line is drawn in the BLACK color and has a width of WIN_LINE_WIDTH pixels.

  • draw_asc_diagonal() function:

This function is responsible for drawing an ascending diagonal line on the screen to indicate the winning squares in an ascending diagonal winning condition. It takes no arguments.

The function uses the pygame.draw.line() function to draw a line from the top-left corner of the screen to the bottom-right corner of the screen. The line is drawn in the BLACK color and has a width of WIN_LINE_WIDTH pixels.

  • draw_desc_diagonal() function:

This function is responsible for drawing a descending diagonal line on the screen to indicate the winning squares in a descending diagonal winning condition. It takes no arguments.

The function uses the pygame.draw.line() function to draw a line from the top-right corner of the screen to the bottom-left corner of the screen. The line is drawn in the BLACK color and has a width of WIN_LINE_WIDTH pixels.

Step 7: Main Game Loop

Set up the main game loop to handle events and update the screen.

Tagged in: