Categories
Coding

Graphical Tic Tac Toe

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()


Leave a Reply

Your email address will not be published. Required fields are marked *