Let’s Build a Simple Tic-Tac-Toe Game with Pygame!

Hey everyone! Today, we’re going to dive into the exciting world of game development using Python and a super fun library called Pygame. If you’ve ever wanted to create your own games but felt intimidated, Tic-Tac-Toe is the perfect starting point. It’s simple enough to understand but teaches you many core game development concepts.

We’ll be building a classic Tic-Tac-Toe game where two players can take turns marking ‘X’s and ‘O’s on a 3×3 grid, right on your computer screen! You’ll learn how to draw graphics, handle mouse clicks, and figure out when someone wins.

What is Pygame?

Before we jump into coding, let’s briefly talk about Pygame.

  • Pygame is a set of Python modules (think of them as toolkits) designed specifically for writing video games. It gives you easy ways to draw shapes and images, play sounds, and react to user inputs like keyboard presses or mouse clicks. It’s a fantastic library for beginners because it simplifies many complex parts of game creation.

Getting Started: Setting Up Your Environment

First things first, you need Python installed on your computer. If you don’t have it, head over to the official Python website and download the latest version.

Once Python is ready, open your command prompt or terminal and install Pygame. This is usually a one-line command:

pip install pygame
  • pip: This is Python’s package installer, a tool that helps you install and manage software packages (like Pygame) written in Python.

If everything goes well, you’re all set to start coding!

Our Game Plan: How We’ll Build Tic-Tac-Toe

Building a game, even a simple one, involves several steps. Here’s our roadmap:

  1. Initialize Pygame and Set Up the Window: We’ll get Pygame ready and create the window where our game will appear.
  2. Draw the Game Board: We need a visual 3×3 grid for players to mark their moves.
  3. Manage Game State: Keep track of whose turn it is, what’s on the board, and if the game is over.
  4. Handle Player Clicks: Detect where a player clicks and update the board with ‘X’ or ‘O’.
  5. Draw ‘X’s and ‘O’s: Visually represent the player’s moves on the board.
  6. Check for a Winner or Draw: Determine if a player has won or if the game is a draw.
  7. Display Messages: Show who won or if it’s a draw, and offer a way to restart.
  8. The Main Game Loop: This is the heart of any game, constantly updating and drawing everything.

Let’s start coding!

Step-by-Step Implementation

We’ll build our game piece by piece. You can create a new Python file (e.g., tic_tac_toe.py) and follow along.

1. Basic Setup and Window Creation

First, we import Pygame, initialize it, and set up our game window.

import pygame
import sys

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

WIDTH, HEIGHT = 600, 600
LINE_WIDTH = 10
BOARD_ROWS, BOARD_COLS = 3, 3
SQUARE_SIZE = WIDTH // BOARD_COLS # Each square will be 200x200 pixels
CIRCLE_RADIUS = SQUARE_SIZE // 3
CIRCLE_WIDTH = 15
CROSS_WIDTH = 25
SPACE = SQUARE_SIZE // 4 # Space for X and O not to touch edges

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tic-Tac-Toe!")
screen.fill(WHITE)

board = [[0, 0, 0],
         [0, 0, 0],
         [0, 0, 0]]
player = 1 # Player 1 is 'X', Player 2 is 'O'
game_over = False
winner = None
  • pygame.init(): This function gets all the Pygame modules ready to be used. You should always call it at the beginning of your Pygame programs.
  • pygame.display.set_mode((width, height)): This creates a display Surface (the window where all our graphics will appear) with the specified width and height.
  • pygame.display.set_caption(): Sets the title that appears at the top of your game window.
  • screen.fill(color): Fills the entire screen Surface with a solid color.

2. Drawing the Game Board

Next, let’s draw the lines that form our 3×3 Tic-Tac-Toe grid.

def draw_board():
    # 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)
  • pygame.draw.line(surface, color, start_pos, end_pos, width): This function draws a straight line on a given surface (our screen) with a specific color, from a start_pos coordinate to an end_pos coordinate, and with a certain width.

3. Drawing ‘X’s and ‘O’s

Now we need functions to draw the ‘X’ and ‘O’ marks when players make their moves.

def draw_figures():
    for row in range(BOARD_ROWS):
        for col in range(BOARD_COLS):
            if board[row][col] == 1: # Player 1 (X)
                # Draw an 'X'
                pygame.draw.line(screen, BLUE, (col * SQUARE_SIZE + SPACE, row * SQUARE_SIZE + SPACE),
                                (col * SQUARE_SIZE + SQUARE_SIZE - SPACE, row * SQUARE_SIZE + SQUARE_SIZE - SPACE), CROSS_WIDTH)
                pygame.draw.line(screen, BLUE, (col * SQUARE_SIZE + SQUARE_SIZE - SPACE, row * SQUARE_SIZE + SPACE),
                                (col * SQUARE_SIZE + SPACE, row * SQUARE_SIZE + SQUARE_SIZE - SPACE), CROSS_WIDTH)
            elif board[row][col] == 2: # Player 2 (O)
                # Draw an 'O'
                pygame.draw.circle(screen, RED, (int(col * SQUARE_SIZE + SQUARE_SIZE // 2),
                                                int(row * SQUARE_SIZE + SQUARE_SIZE // 2)), CIRCLE_RADIUS, CIRCLE_WIDTH)
  • pygame.draw.circle(surface, color, center_pos, radius, width): Draws a circle. center_pos is the (x, y) coordinate of the circle’s center, radius is its size, and width is the thickness of the line used to draw it (0 for filled).

4. Checking for a Winner or Draw

This is where the game logic comes in. We need to check all possible winning combinations (rows, columns, and diagonals).

def check_win(player_val):
    global game_over, winner

    # Check horizontal win
    for row in range(BOARD_ROWS):
        if board[row][0] == player_val and board[row][1] == player_val and board[row][2] == player_val:
            game_over = True
            winner = player_val
            pygame.draw.line(screen, GREEN, (0, row * SQUARE_SIZE + SQUARE_SIZE // 2),
                            (WIDTH, row * SQUARE_SIZE + SQUARE_SIZE // 2), LINE_WIDTH)
            return True

    # Check vertical win
    for col in range(BOARD_COLS):
        if board[0][col] == player_val and board[1][col] == player_val and board[2][col] == player_val:
            game_over = True
            winner = player_val
            pygame.draw.line(screen, GREEN, (col * SQUARE_SIZE + SQUARE_SIZE // 2, 0),
                            (col * SQUARE_SIZE + SQUARE_SIZE // 2, HEIGHT), LINE_WIDTH)
            return True

    # Check ascending diagonal win
    if board[2][0] == player_val and board[1][1] == player_val and board[0][2] == player_val:
        game_over = True
        winner = player_val
        pygame.draw.line(screen, GREEN, (SPACE, HEIGHT - SPACE), (WIDTH - SPACE, SPACE), LINE_WIDTH)
        return True

    # Check descending diagonal win
    if board[0][0] == player_val and board[1][1] == player_val and board[2][2] == player_val:
        game_over = True
        winner = player_val
        pygame.draw.line(screen, GREEN, (SPACE, SPACE), (WIDTH - SPACE, HEIGHT - SPACE), LINE_WIDTH)
        return True

    return False

def check_draw():
    for row in range(BOARD_ROWS):
        for col in range(BOARD_COLS):
            if board[row][col] == 0: # If any square is empty, it's not a draw yet
                return False
    return True # If no square is empty and no winner, it's a draw

5. Displaying Game Messages

We need to show messages like “Player X Wins!” or “It’s a Draw!”.

def display_message(message):
    font = pygame.font.Font(None, 80) # None for default font, 80 for font size
    text = font.render(message, True, BLACK) # Render the text: (text, antialias, color)
    text_rect = text.get_rect(center=(WIDTH // 2, HEIGHT // 2)) # Get rectangle for centering
    screen.blit(text, text_rect) # Draw the text onto the screen

    # Add a smaller message for restarting
    small_font = pygame.font.Font(None, 40)
    restart_text = small_font.render("Press 'R' to Restart", True, GRAY)
    restart_text_rect = restart_text.get_rect(center=(WIDTH // 2, HEIGHT // 2 + 50))
    screen.blit(restart_text, restart_text_rect)
  • pygame.font.Font(None, size): Creates a font object. None uses Pygame’s default font.
  • font.render(text, antialias, color): Renders text into a new Surface. antialias smooths out the edges of the text.
  • screen.blit(source_surface, dest_position): Draws one image (source_surface) onto another (screen) at a specific dest_position.

6. Resetting the Game

When the game ends, players might want to play again.

def restart_game():
    global board, player, game_over, winner
    board = [[0, 0, 0],
             [0, 0, 0],
             [0, 0, 0]]
    player = 1
    game_over = False
    winner = None
    screen.fill(WHITE) # Clear the screen
    draw_board() # Redraw the empty board

7. The Main Game Loop

This is the continuous loop that keeps our game running, handling events, updating the screen, and drawing everything.

running = True
draw_board()

while running:
    for event in pygame.event.get(): # Check for all events (user actions)
        if event.type == pygame.QUIT: # If the user clicks the 'X' to close the window
            running = False
            sys.exit() # Exit the program

        if event.type == pygame.MOUSEBUTTONDOWN and not game_over:
            mouseX = event.pos[0] # x-coordinate of mouse click
            mouseY = event.pos[1] # y-coordinate of mouse click

            # Determine which square was clicked
            clicked_col = mouseX // SQUARE_SIZE
            clicked_row = mouseY // SQUARE_SIZE

            # Make sure click is within board bounds and the cell is empty
            if 0 <= clicked_row < BOARD_ROWS and 0 <= clicked_col < BOARD_COLS and board[clicked_row][clicked_col] == 0:
                board[clicked_row][clicked_col] = player # Place current player's mark

                if check_win(player):
                    message = f"Player {winner} Wins!"
                elif check_draw():
                    game_over = True
                    message = "It's a Draw!"
                else:
                    # Switch player for the next turn
                    player = 1 if player == 2 else 2 # If player was 2, switch to 1; otherwise, switch to 2

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_r: # Check if 'R' key is pressed
                restart_game()

    # Always redraw everything in the loop
    screen.fill(WHITE) # Clear the screen each frame
    draw_board() # Draw the grid lines
    draw_figures() # Draw X's and O's

    if game_over:
        if winner:
            display_message(f"Player {winner} Wins!")
        else:
            display_message("It's a Draw!")

    pygame.display.update() # Update the full display Surface to the screen
  • while running:: This loop continues as long as running is True. Most of your game logic and drawing will happen inside this loop.
  • pygame.event.get(): This function fetches all the user events (like mouse clicks, keyboard presses, window closing) that have happened since the last call.
  • event.type == pygame.QUIT: Checks if the user clicked the close button of the window.
  • pygame.MOUSEBUTTONDOWN: This event occurs when a mouse button is pressed down.
  • pygame.display.update(): This is crucial! It takes everything you’ve drawn on the screen Surface and actually displays it on your computer monitor. Without this, you wouldn’t see any changes.

Putting It All Together (Full Code)

Here’s the complete code for our simple Tic-Tac-Toe game. You can copy and paste this into a tic_tac_toe.py file and run it!

import pygame
import sys

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (200, 200, 200)
BLUE = (0, 0, 255)
RED = (255, 0, 0)
GREEN = (0, 255, 0)

WIDTH, HEIGHT = 600, 600
LINE_WIDTH = 10
BOARD_ROWS, BOARD_COLS = 3, 3
SQUARE_SIZE = WIDTH // BOARD_COLS # Each square will be 200x200 pixels
CIRCLE_RADIUS = SQUARE_SIZE // 3
CIRCLE_WIDTH = 15
CROSS_WIDTH = 25
SPACE = SQUARE_SIZE // 4 # Space for X and O not to touch edges

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Tic-Tac-Toe!")
screen.fill(WHITE)

board = [[0, 0, 0],
         [0, 0, 0],
         [0, 0, 0]]
player = 1 # Player 1 is 'X', Player 2 is 'O'
game_over = False
winner = None

def draw_board():
    # 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: # Player 1 (X)
                # Draw an 'X'
                pygame.draw.line(screen, BLUE, (col * SQUARE_SIZE + SPACE, row * SQUARE_SIZE + SPACE),
                                (col * SQUARE_SIZE + SQUARE_SIZE - SPACE, row * SQUARE_SIZE + SQUARE_SIZE - SPACE), CROSS_WIDTH)
                pygame.draw.line(screen, BLUE, (col * SQUARE_SIZE + SQUARE_SIZE - SPACE, row * SQUARE_SIZE + SPACE),
                                (col * SQUARE_SIZE + SPACE, row * SQUARE_SIZE + SQUARE_SIZE - SPACE), CROSS_WIDTH)
            elif board[row][col] == 2: # Player 2 (O)
                # Draw an 'O'
                pygame.draw.circle(screen, RED, (int(col * SQUARE_SIZE + SQUARE_SIZE // 2),
                                                int(row * SQUARE_SIZE + SQUARE_SIZE // 2)), CIRCLE_RADIUS, CIRCLE_WIDTH)

def check_win(player_val):
    global game_over, winner

    # Check horizontal win
    for row in range(BOARD_ROWS):
        if board[row][0] == player_val and board[row][1] == player_val and board[row][2] == player_val:
            game_over = True
            winner = player_val
            pygame.draw.line(screen, GREEN, (0, row * SQUARE_SIZE + SQUARE_SIZE // 2),
                            (WIDTH, row * SQUARE_SIZE + SQUARE_SIZE // 2), LINE_WIDTH)
            return True

    # Check vertical win
    for col in range(BOARD_COLS):
        if board[0][col] == player_val and board[1][col] == player_val and board[2][col] == player_val:
            game_over = True
            winner = player_val
            pygame.draw.line(screen, GREEN, (col * SQUARE_SIZE + SQUARE_SIZE // 2, 0),
                            (col * SQUARE_SIZE + SQUARE_SIZE // 2, HEIGHT), LINE_WIDTH)
            return True

    # Check ascending diagonal win (bottom-left to top-right)
    if board[2][0] == player_val and board[1][1] == player_val and board[0][2] == player_val:
        game_over = True
        winner = player_val
        pygame.draw.line(screen, GREEN, (SPACE, HEIGHT - SPACE), (WIDTH - SPACE, SPACE), LINE_WIDTH)
        return True

    # Check descending diagonal win (top-left to bottom-right)
    if board[0][0] == player_val and board[1][1] == player_val and board[2][2] == player_val:
        game_over = True
        winner = player_val
        pygame.draw.line(screen, GREEN, (SPACE, SPACE), (WIDTH - SPACE, HEIGHT - SPACE), LINE_WIDTH)
        return True

    return False

def check_draw():
    for row in range(BOARD_ROWS):
        for col in range(BOARD_COLS):
            if board[row][col] == 0: # If any square is empty, it's not a draw yet
                return False
    # If no winner and no empty squares, it's a draw
    return True

def display_message(message):
    font = pygame.font.Font(None, 80) # None for default font, 80 for font size
    text = font.render(message, True, BLACK) # Render the text: (text, antialias, color)
    text_rect = text.get_rect(center=(WIDTH // 2, HEIGHT // 2)) # Get rectangle for centering
    screen.blit(text, text_rect) # Draw the text onto the screen

    # Add a smaller message for restarting
    small_font = pygame.font.Font(None, 40)
    restart_text = small_font.render("Press 'R' to Restart", True, GRAY)
    restart_text_rect = restart_text.get_rect(center=(WIDTH // 2, HEIGHT // 2 + 50))
    screen.blit(restart_text, restart_text_rect)

def restart_game():
    global board, player, game_over, winner
    board = [[0, 0, 0],
             [0, 0, 0],
             [0, 0, 0]]
    player = 1
    game_over = False
    winner = None
    screen.fill(WHITE) # Clear the screen
    draw_board() # Redraw the empty board

running = True
draw_board()

while running:
    for event in pygame.event.get(): # Check for all events (user actions)
        if event.type == pygame.QUIT: # If the user clicks the 'X' to close the window
            running = False
            sys.exit() # Exit the program gracefully

        if event.type == pygame.MOUSEBUTTONDOWN and not game_over:
            mouseX = event.pos[0] # x-coordinate of mouse click
            mouseY = event.pos[1] # y-coordinate of mouse click

            # Determine which square was clicked
            clicked_col = mouseX // SQUARE_SIZE
            clicked_row = mouseY // SQUARE_SIZE

            # Make sure click is within board bounds and the cell is empty
            if 0 <= clicked_row < BOARD_ROWS and 0 <= clicked_col < BOARD_COLS and board[clicked_row][clicked_col] == 0:
                board[clicked_row][clicked_col] = player # Place current player's mark

                if check_win(player):
                    # Winner determined, message set inside check_win
                    pass
                elif check_draw():
                    game_over = True
                    winner = None # No specific winner in a draw
                else:
                    # Switch player for the next turn
                    player = 1 if player == 2 else 2 # If player was 2, switch to 1; otherwise, switch to 2

        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_r: # Check if 'R' key is pressed
                restart_game()

    # Always redraw everything in the loop
    screen.fill(WHITE) # Clear the screen each frame before drawing
    draw_board() # Draw the grid lines
    draw_figures() # Draw X's and O's

    if game_over:
        if winner:
            display_message(f"Player {winner} Wins!")
        else:
            display_message("It's a Draw!")

    pygame.display.update() # Update the full display Surface to the screen

Conclusion

Congratulations! You’ve just created your very own interactive Tic-Tac-Toe game using Pygame. You’ve learned how to:

  • Set up a Pygame window.
  • Draw shapes and lines to create your game board and player marks.
  • Handle mouse clicks and keyboard presses.
  • Implement game logic for turns, wins, and draws.
  • Display text messages to the player.

This is a fantastic foundation for further game development. Don’t stop here! Try experimenting with:

  • Adding sound effects for moves and wins.
  • Creating a simple AI opponent.
  • Making the game visually more appealing with different colors or images.
  • Adding a scoreboard.

The possibilities are endless. Keep coding, keep experimenting, and most importantly, keep having fun!

Comments

Leave a Reply