Skip to content

Building a Tic Tac Toe Game in Python

Updated: at 04:34 AM

Tic Tac Toe is a classic paper and pencil game that is often used as an introductory project for beginning programmers learning a new language. It is relatively simple to code, but allows developers to practice key programming concepts like loops, functions, conditionals, and two-dimensional arrays.

In this comprehensive guide, we will walk through how to code a Tic Tac Toe game in Python using 2D lists to represent the game board. We will cover core topics like:

By the end, you will have a fully functional Tic Tac Toe game in Python that runs in the Jupyter notebook environment. This serves as an excellent introduction to foundational Python programming skills that can be built upon for more complex projects.

Overview of Tic Tac Toe Rules and Gameplay

Tic Tac Toe is a two player game played on a 3x3 grid. Each player takes turns placing their symbol, either X or O, on an open square on the grid. The first player to get 3 of their symbols in a row, either horizontally, vertically, or diagonally, wins the game. If all 9 squares are filled and no player has achieved 3 in a row, the game results in a tie.

Basic gameplay follows these rules:

This straightforward setup makes Tic Tac Toe an ideal pedagogical example for teaching core programming techniques like nested lists, functions, and game state logic in Python.

Initializing the Game Board in Python

The first step is to initialize a blank Tic Tac Toe board in Python that we can print and allow players to place their X’s and O’s on.

Since Tic Tac Toe is played on a 3x3 grid, we can represent the board using a 2D list with 3 inner lists to represent the rows, and 3 elements in each inner list to represent the columns.

# Initialize empty 3x3 board
board = [
    [" ", " ", " "],
    [" ", " ", " "],
    [" ", " ", " "]
]

This [Wikipedia page on Tic Tac Toe] provides a helpful conceptual visualization of the 3x3 grid numbering we will use:

1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9

Each inner list in our 2D board list corresponds to a row, and each element within those inner lists corresponds to columns. This allows us to index directly into the appropriate row and column we want to place a move in later.

For better code organization, we can wrap this initialization step into a initialize_board() function:

def initialize_board():

    board = [
        [" ", " ", " "],
        [" ", " ", " "],
        [" ", " ", " "]
    ]

    return board

To test, we can call the function and print the initial blank board:

board = initialize_board()
print(board)

Which outputs:

[[' ', ' ', ' '], [' ', ' ', ' '], [' ', ' ', ' ']]

Our blank Tic Tac Toe board is ready! Next we need to write the functions to print the board after each move.

Printing the Game Board

To display the current state of the board after each turn, we’ll write a print_board() function that takes the board 2D list as input, loops through it, and prints each row & column with pipes and lines in between.

def print_board(board):

    print("\n")

    for row in board:
        print(row[0] + " | " + row[1]  + " | " + row[2])

    print("\n")

To make this print formatting a bit clearer, we can also add divider lines in between each row:

def print_board(board):

    print("\n")

    for row in board:
        print(row[0] + " | " + row[1]  + " | " + row[2])

        print(row[0] + " | " + row[1]  + " | " + row[2])

    print("\n")

Calling print_board() after each move will redraw the board with the updated X and O placements.

Let’s test it by placing an X in the center square and printing:

board[1][1] = 'X'
print_board(board)

This prints:

|   |
| X |
|   |

Perfect! Now we can dynamically print an updated board after each player’s move.

Checking for a Winner

The core game logic lies in being able to check if a player has won by getting 3 X’s or O’s in a row after each move. We can write a check_win() function that takes in the current board state and checks the rows, columns, and diagonals for a winner.

To check the rows, we can loop through each inner list in the board and see if any row has all X’s or O’s:

def check_win(board):

    # Check rows
    for row in board:
        if row.count("X") == 3:
            print("X wins!")
        if row.count("O") == 3:
            print("O wins!")

We can check the columns by looping through the column index and checking vertically:

# Check columns
for col in range(3):
    if board[0][col] == board[1][col] == board[2][col] != " ":
        print(board[0][col], "wins!")

Finally, we check the two diagonals corner to corner:

# Check diagonals
if board[0][0] == board[1][1] == board[2][2] != " ":
    print(board[0][0], "wins!")

if board[0][2] == board[1][1] == board[2][0] != " ":
    print(board[0][2], "wins!")

We can also check if the game was a tie if all squares are filled with no winner:

# Check for tie
if " " not in board[0] and " " not in board[1] and " " not in board[2]:
    print("Tie!")

Our full check_win() function now looks like:

def check_win(board):

    # Check rows
    for row in board:
        if row.count("X") == 3:
            print("X wins!")
        if row.count("O") == 3:
            print("O wins!")

    # Check columns
    for col in range(3):
        if board[0][col] == board[1][col] == board[2][col] != " ":
            print(board[0][col], "wins!")

    # Check diagonals
    if board[0][0] == board[1][1] == board[2][2] != " ":
        print(board[0][0], "wins!")
    if board[0][2] == board[1][1] == board[2][0] != " ":
        print(board[0][2], "wins!")

    # Check for tie
    if " " not in board[0] and " " not in board[1] and " " not in board[2]:
        print("Tie!")

We can test it by setting up a winning diagonal for X:

board[0][0] = "X"
board[1][1] = "X"
board[2][2] = "X"

check_win(board)

This will correctly print out:

X wins!

Our check_win() function is now able to accurately detect all winning conditions after each move!

Handling Player Moves

Up until now, we’ve manually placed X’s and O’s on the board ourselves. To actually play, we need to implement logic for alternating turns between two players.

We’ll track whose turn it is with a current_player variable that switches between “X” and “O”. Inside the game loop, we can prompt the current player to enter their move location from 1-9 based on our board numbering scheme earlier.

# Game loop

current_player = "X"

while True:

    print_board(board)

    print(f"Turn: {current_player}")
    move = int(input("Enter your move 1-9: "))

    # Make move on board
    board[int((move-1)/3)][(move-1)%3] = current_player

    # Check win or tie
    winner = check_win(board)

    # Switch players
    if current_player == "X":
        current_player = "O"
    else:
        current_player = "X"

We calculate the row and column indexes to update based on the user’s numeric move input, place their X or O on the board, check for a winner, and finally switch active players.

This allows us to naturally alternate moves between the two players until someone wins or a tie occurs.

To improve the user experience, we can add some validation to prevent selecting an already taken square:

# Inside player move prompt

if board[int((move-1)/3)][(move-1)%3] != " ":
    print("Invalid move, already taken!")
    continue

Now the game will properly handle player moves and alternate turns. Next we can implement restarts and game loops.

Restarting and Looping the Game

To allow players to continually play new games without having to restart the Python kernel, we can wrap our main game logic in a loop that prompts if players want to play again.

restart = "Y"
while restart == "Y":

    # Main game loop and logic

    restart = input("Play again? Y/N")

print("Thanks for playing!")

We initialize restart to “Y” to enter the first game loop. Inside the loop we have our full game logic handling player moves, win conditions, and printing the board.

After a game finishes, we prompt if the player wants to play again, setting restart based on their input. The outer loop continues as long as restart remains “Y”, allowing quick resets for new games.

To fully reset the board on each new game, we can call our initialize_board() function:

while restart == "Y":

    board = initialize_board()

    # Rest of game logic & loops

print("Thanks for playing!")

Now we have a full playable Tic Tac Toe game in Python complete with move handling, turn alternation, win condition checking, and restarts!

Putting It All Together

We can put together everything we’ve covered so far into a complete, playable Tic Tac Toe game in Python:

def is_space_empty(board, row, col):
    return board[row][col] == " "


def initialize_board():
    return [
        [" ", " ", " "],
        [" ", " ", " "],
        [" ", " ", " "]
    ]


def print_board(board):
    print("\n")
    print(board[0][0] + " | " + board[0][1] + " | " + board[0][2])
    print(board[1][0] + " | " + board[1][1] + " | " + board[1][2])
    print(board[2][0] + " | " + board[2][1] + " | " + board[2][2])
    print("\n")


def check_win(board):
    # Check rows
    for row in board:
        if row.count("X") == 3:
            return "X"
        if row.count("O") == 3:
            return "O"

    # Check columns
    for col in range(3):
        if board[0][col] == board[1][col] == board[2][col] != " ":
            return board[0][col]

    # Check diagonals
    if board[0][0] == board[1][1] == board[2][2] != " ":
        return board[0][0]
    if board[0][2] == board[1][1] == board[2][0] != " ":
        return board[0][2]

    # Check for tie
    if " " not in board[0] and " " not in board[1] and " " not in board[2]:
        return "Tie"

    return ""


def player_move(board, current_player, icon):
    if icon == "X":
        number = 1
    elif icon == "O":
        number = 2

    print("Your turn player {}".format(number))

    while True:
        choice = int(input("Enter your move (1-9): ").strip())
        row = int((choice - 1) / 3)
        col = (choice - 1) % 3

        if not is_space_empty(board, row, col):
            print()
            print("That space is taken!")
        else:
            board[row][col] = icon
            break


def choose_first_player():
    print("Who wants to go first? (X/O)")
    player_choice = input().strip().upper()

    while player_choice not in ["X", "O"]:
        print("Invalid choice. Please enter X or O")
        player_choice = input().strip().upper()

    return player_choice


def reset_board(board):
    for row in range(3):
        for col in range(3):
            board[row][col] = " "


def main():
    board = initialize_board()
    current_player = choose_first_player()

    while True:
        print_board(board)

        player_move(board, current_player, icon=current_player)

        winner = check_win(board)

        if winner != "":
            print(f"{winner} wins! Game over.")
            break

        if current_player == "X":
            current_player = "O"
        else:
            current_player = "X"

    restart = input("Play again? (Y/N)")
    if restart.upper() == "N":
        print("Bye!")
        return

    reset_board(board)
    current_player = choose_first_player()


if __name__ == "__main__":
    main()

And that’s it! We now have a fully playable Tic Tac Toe game in Python complete with 2D lists to represent the board, alternating player moves, win condition checking, and restarts.

The full code can be run in a Jupyter notebook cell to allow playing the game interactively. This serves as a solid foundation for practicing core Python programming concepts.

Summary

In this comprehensive guide, we walked through how to code the classic paper and pencil game Tic Tac Toe in Python.

Key topics covered included:

The full code example provides a complete Tic Tac Toe game playable in Jupyter notebook that serves as a beginner-friendly introduction to foundational Python programming skills.

Some ways this game can be extended or improved on include:

Overall, coding a basic game like Tic Tac Toe allows beginning Python programmers to get hands-on experience applying core language features like functions, loops, lists, and logic flow. The full code example above can serve as a template to build off for more advanced versions.