Building Tic Tac Toe in Python

In this Python tutorial, we will create a simple Tic Tac Toe game that you can play against another person. We'll use basic Python concepts and data structures to implement the game logic.

Game Explained

Tic Tac Toe is a two-player game played on a 3x3 grid. Players take turns marking a square with their symbol, typically 'X' and 'O'. The objective is to get three of your symbols in a row, either horizontally, vertically, or diagonally.

Here's how the game will work:

Game Board: We'll represent the game board as a 3x3 grid, initially filled with empty spaces.

Players: We'll have two players, one playing 'X' and the other playing 'O'.

Turns: Players will take turns placing their symbol on the board.

Winning Condition: We'll check after each turn if any player has won by getting three of their symbols in a row.

Draw Condition: If all squares are filled and no player has won, the game is a draw.

Display: We'll display the game board after each turn, allowing players to see the current state of the game.

Game Logic Explained


We will first need to define the width and height of our Pygame GUI.

We do that by determining how big we want each block in the Tic Tac Toe game to be. Then we multiply that by 3 for height and 3 for width. Additionally, we will render a gap above the board displaying which player's turn it is and who won and render this at the top. Therefore, remember to offset that value for everything below it. We then take the 2d array containing player placements and render an X or O text at the center of the block.

# Variables
block_size = 200
num_blocks_x = 3
num_blocks_y = 3
space_above_grid = 30

# Constants
SCREEN_WIDTH = num_blocks_x * block_size
SCREEN_HEIGHT = num_blocks_y * block_size + space_above_grid
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
def render(screen, letter_grid, player_text, winner):
    font_player = pygame.font.Font(None, 70)
    font = pygame.font.Font(None, 36)
    draw_grid(screen, block_size, num_blocks_x, num_blocks_y)

    # Draw texts in the space above the grid
    player_text_surface = font.render("Player Turn: %s" % player_text, True, WHITE)
    player_text_rect = player_text_surface.get_rect(midleft=(10, space_above_grid // 2))
    screen.blit(player_text_surface, player_text_rect)

    winner_text_surface = font.render("Winner:%s" % str(winner), True, WHITE)
    winner_text_rect = winner_text_surface.get_rect(midright=(SCREEN_WIDTH - 10, space_above_grid // 2))
    screen.blit(winner_text_surface, winner_text_rect)

    # Draw letters on the grid
    for y, row in enumerate(letter_grid):
        for x, letter in enumerate(row):
            letter_surface = font_player.render(letter, True, WHITE)
            letter_rect = letter_surface.get_rect(center=(x * block_size + block_size // 2,
                                                           y * block_size + space_above_grid + block_size // 2))
            screen.blit(letter_surface, letter_rect)


Block Determination

We create a 2D array filled with blank strings to represent the game board.

Using Pygame, we draw an 'X' or 'O' in the correct block on the screen.

To determine the block where the player has clicked, we first obtain the mouse click locations. Once received, we divide the x and y coordinates by the block size to identify the corresponding block on the game board, as shown in the code.

def get_block_from_mouse(mouse_pos):
    block_x = mouse_pos[0] // block_size
    block_y = (mouse_pos[1] - space_above_grid) // block_size
    return block_x, block_y

Winner Detection

There are three conditions we need to check to determine if a player is in a winning position: horizontal, vertical, and diagonals. To detect if a player has won, we sum the values in each possible winning position.

Horizontal Detection

We achieve this by examining each row and replacing any instance of the player's symbol with a 1, otherwise assigning a 0. We then sum the row, and if the sum is 3, we have found the winner and return true; otherwise, we proceed to the next check.

Notice the conversion from the letters to numbers to the sum in green, relative to 'x'.

Vertical Detection

Similar to the horizontal detection, we transpose the grid. Transposing means swapping rows with columns. If the sum of any column is 3, we return true; otherwise, we proceed to the next check.

Notice how the rows become columns. We can now apply the same method we used in horizontal detection as the columns are now horizontal.

Diagonal Detection

Here, we define two grids: our original one and a flipped one over the x-axis. We flip the grid so we can apply the same method used for the original to the flipped one, instead of altering the method to find the other diagonal sum. Then, we sum the values in the ith row and ith column. If the sum is 3, we return true.

The code to this is given below:

def winner_detection(letter_grid, player, transpose=False, diagonal=False):
    if diagonal:
        grids = [
        for grid in grids:
            num_row = []
            for i in range(0, len(grid)):
                elem = grid[i][i]
                if elem == player:
            if sum(num_row) == 3:
                return True
        return False
    for row in letter_grid:
        num_row = []
        for elem in row:
            if elem == player:
        if sum(num_row) == 3:
            return True
    if transpose:
        if not diagonal:
            return winner_detection(np.transpose(letter_grid), player, transpose=False, diagonal=True)
        return False
        return winner_detection(np.transpose(letter_grid), player, transpose=True)

I hope you enjoyed this article. Comment if you have any questions. The code can be found here.