
On the bus ride back home from the latest GPN, my dad and I brainstormed ideas to make a graphical Tic Tac Toe game using python.
The inspiration came from the GPN session I attended, where we learnt how to make a text based multiplayer version of the classic childhood favorite game, Tic Tac Toe.
This time, the coding was a bit quicker, as we had already tackled the basic game logic from the text based version. We were also equipped with Pygame knowledge we gained from coding our earlier project, Caver Drone.
# Created by Ahla, Anas 16/07/2025
import sys # Provides access to system-specific functions
import os # Used for handling file paths
import pygame # For handling graphics, sounds, and game logic
import random # Allows generation of random numbers
pygame.init() # Initialization of graphics UI
pygame.mixer.init() # Required for enabling sound
# Get absolute path to resource (for dev and PyInstaller)
def resource_path(relative_path):
try:
base_path = sys._MEIPASS # For PyInstaller
except Exception:
base_path = os.path.abspath(".") # For dev/testing
return os.path.join(base_path, relative_path)
# Define a font for X and O
symbolFont = pygame.font.SysFont("Courier New", 144) # (font name, font size)
statusFont = pygame.font.SysFont("Courier New", 18, True) # (font name, font size, font weight)
colorBlack = (0, 0, 0) # 0x000000
colorRed = (255, 0, 0) #0xFF0000
colorGreen = (0, 255, 0) #0x00FF00
colorWhite = (255, 255, 255) #0xFFFFFF
clockFrame = pygame.time.Clock() # Creates a Clock object to help control the frame rate
# Sounds
winSFX = pygame.mixer.Sound(resource_path("assets/yay.mp3")) # Sound effect for winning
errorSFX = pygame.mixer.Sound(resource_path("assets/error.mp3")) # Sound effect for wrong click
tieSFX = pygame.mixer.Sound(resource_path("assets/oh-no.mp3")) # Sound effect for tie
# Sound settings
winSFX.set_volume(0.5) # 50% volume
errorSFX.set_volume(0.5) # 50% volume
tieSFX.set_volume(0.5) # 50% volume
# Graphics
screenWidthX = 800
screenHeightY = 800
playLayerX = 712
playLayerY = 712
cellSizeXY = playLayerX // 3
offsetX = (screenWidthX-playLayerX)//2
offsetY = (screenHeightY-playLayerY)//2
numberFPS = 10
screen = pygame.display.set_mode((screenWidthX, screenHeightY)) # Graphics window dimensions
pygame.display.set_caption("Tic Tac Toe by Ahla") # Title for the Graphics window
screen.fill(colorBlack) # Dark background for Graphics window
# Prepare a layer for board background
boardLayer = pygame.image.load(resource_path("assets/board_background.png")).convert_alpha() # Board background
# Declare an empty array of 9 elements
board = [" "," "," "," "," "," "," "," "," "]
# Define winning logic
def check_winner(board):
global playLayer
# First row is filled with the same symbol (O or X), so game over!
if board[0] == board[1] == board[2] != " ":
playLayer = pygame.image.load(resource_path("assets/row0.png")).convert_alpha() # Board background
return True
# Second row is filled with the same symbol (O or X), so game over!
elif board[3] == board[4] == board[5] != " ":
playLayer = pygame.image.load(resource_path("assets/row1.png")).convert_alpha() # Board background
return True
# Third row is filled with the same symbol (O or X), so game over!
elif board[6] == board[7] == board[8] != " ":
playLayer = pygame.image.load(resource_path("assets/row2.png")).convert_alpha() # Board background
return True
# First column is filled with the same symbol (O or X), so game over!
if board[0] == board[3] == board[6] != " ":
playLayer = pygame.image.load(resource_path("assets/col0.png")).convert_alpha() # Board background
return True
# Second column is filled with the same symbol (O or X), so game over!
elif board[1] == board[4] == board[7] != " ":
playLayer = pygame.image.load(resource_path("assets/col1.png")).convert_alpha() # Board background
return True
# Third column is filled with the same symbol (O or X), so game over!
elif board[2] == board[5] == board[8] != " ":
playLayer = pygame.image.load(resource_path("assets/col2.png")).convert_alpha() # Board background
return True
# Diagonal (left to right) is filled with the same symbol (O or X), so game over!
if board[0] == board[4] == board[8] != " ":
playLayer = pygame.image.load(resource_path("assets/dia-l-r.png")).convert_alpha() # Board background
return True
# Diagonal (right to left) is filled with the same symbol (O or X), so game over!
elif board[2] == board[4] == board[6] != " ":
playLayer = pygame.image.load(resource_path("assets/dia-r-l.png")).convert_alpha() # Board background
return True
# No one has won so far, continue the game
else:
return False
# Computer AI game logic
def get_computer_move(board, computer_symbol):
opponent_symbol = "O" if computer_symbol == "X" else "X"
# 1. Check for winning move
for i in range(9):
if board[i] == " ":
board[i] = computer_symbol
if check_winner(board):
board[i] = " "
return i
board[i] = " "
# 2. Check for blocking move
for i in range(9):
if board[i] == " ":
board[i] = opponent_symbol
if check_winner(board):
board[i] = " "
return i
board[i] = " "
# 3. Choose randomly if no win/block
# Create an empty array
empty_positions = []
# First, get the empty positions on the board
for i in range(len(board)):
if board[i] == " ":
empty_positions.append(i)
# Now generate a random number for the Computer ONLY from empty_positions
return random.choice(empty_positions)
# Function to draw the symbols on board
def print_board(board):
# Reset board image
screen.blit(boardLayer, (0, 0))
# Output the text to display
symbolTextRow0 = symbolFont.render(f"{board[0]} {board[1]} {board[2]}", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at position
screen.blit(symbolTextRow0, (90, 90))
# Output the text to display
symbolTextRow1 = symbolFont.render(f"{board[3]} {board[4]} {board[5]}", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at position
screen.blit(symbolTextRow1, (90, 327))
# Output the text to display
symbolTextRow2 = symbolFont.render(f"{board[6]} {board[7]} {board[8]}", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at position
screen.blit(symbolTextRow2, (90, 564))
pygame.display.flip() # Display the graphics.
# Mouse Click detection function
def get_MouseXY():
mouse_x, mouse_y = pygame.mouse.get_pos() # Get screen coordinates
# Adjust for playLayer offset
mousePlayX = mouse_x - offsetX
mousePlayY = mouse_y - offsetY
# Check if click is inside the playLayer (712x712)
if 0 <= mousePlayX < 712 and 0 <= mousePlayY < 712:
return (mousePlayX, mousePlayY) # Coordinates relative to playLayer
else:
return None # Clicked outside the play area
# Define Game loop function
def game_loop():
# Define variables
game_over = False
symbol = "O"
current_player = player_O
# Declare an empty array of 9 elements
board = [" "," "," "," "," "," "," "," "," "]
# Reset board image
screen.blit(boardLayer, (0, 0))
pygame.display.flip() # Display the graphics.
while game_over == False:
for event in pygame.event.get(): # Waiting for someone to click close (X) on Graphics window
if event.type == pygame.QUIT:
# Boolean for infinite loop is made false to exit
pygame.quit() # Terminating graphics UI
sys.exit() # Exits the script completely
# Take input from the human player with validation
if current_player == player_O:
# Capture mouse click positions
if event.type == pygame.MOUSEBUTTONDOWN:
playLayer_click = get_MouseXY()
if playLayer_click: # Only proceed if click is inside playLayer
x, y = playLayer_click
# Convert to grid cell (0-8) if needed
col = x // cellSizeXY # 0, 1, or 2
row = y // cellSizeXY # 0, 1, or 2
square_index = row * 3 + col
# Check if square is already occupied
if board[square_index] != " ":
# Play error sound
errorSFX.play()
# Output the text to display
resultText = statusFont.render("That square is already occupied. Please choose another.", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at position (32, 6)
screen.blit(resultText, (32, 6))
pygame.display.flip() # Display the graphics.
else:
# Assign the player's symbol to the board array
board[square_index] = symbol
# Draw the symbol on board
print_board(board)
# Check if the game is over
game_over = check_winner(board)
# Change the current player and symbol to the next
if symbol == "O":
current_player = player_X
symbol = "X"
else:
current_player = player_O
symbol = "O"
# Current player is Computer
else:
# AI computer move
square_index = get_computer_move(board, symbol)
# Assign the player's symbol to the board array
board[square_index] = symbol
# Draw the symbol on board
print_board(board)
# Check if the game is over
game_over = check_winner(board)
# Change the current player and symbol to the next
if symbol == "O":
current_player = player_X
symbol = "X"
else:
current_player = player_O
symbol = "O"
if game_over == True:
# Change the current player to the previous (winner)
if symbol == "O":
current_player = player_X
symbol = "X"
else:
current_player = player_O
symbol = "O"
# Play winning sound
winSFX.play()
# Output the text to display
resultText = statusFont.render(f"{current_player} [{symbol}] won! Congratulations!", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at position (32, 6)
screen.blit(resultText, (32, 6))
# Blit the stroke
screen.blit(playLayer, (0, 0))
# Replay?
statusText = statusFont.render("Play again? Yes [Y] or No [N]", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at bottom position
screen.blit(statusText, (32, 774))
pygame.display.flip() # Display the graphics.
break # Exit the game loop if there's a winner
# Check for a tie (board is full)
# No space on the board
if " " not in board:
# Play the "Oh, no!" sound
tieSFX.play()
# Output the text to display
resultText = statusFont.render("It's a TIE! Better luck next time.", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at position (32, 6)
screen.blit(resultText, (32, 6))
# Replay?
statusText = statusFont.render("Play again? Yes [Y] or No [N]", True, colorWhite) # White color text, anti-aliasing ON
# Blit the status text at bottom position
screen.blit(statusText, (32, 774))
pygame.display.flip() # Display the graphics.
# Let's exit the game
game_over = True
break
clockFrame.tick(numberFPS) # Ensure 10 frames per second
# Gameplay begins
# Welcome message and get player names
player_O = "Player A"
player_X = "Computer"
# Game loop
game_loop()
# Play again logic
while True:
for event in pygame.event.get(): # Waiting for someone to click close (X) on Graphics window or ESC key
if event.type == pygame.QUIT or (event.type == pygame.KEYDOWN and (event.key == pygame.K_ESCAPE or event.key == pygame.K_n)):
pygame.quit() # Terminating graphics UI
sys.exit() # Exits the script completely
break
# Check for replay 'Y' or Enter key
if event.type == pygame.KEYDOWN and (event.key == pygame.K_RETURN or event.key == pygame.K_KP_ENTER or event.key == pygame.K_y):
game_loop()