Back to Articles
Python12 min read

Teaching Technology to the Next Generation: Tic-Tac-Toe

Build a complete Tic-Tac-Toe game in Python while learning initialization, loops, state management, and error handling.

By Quang Duong

North Garland H.S. Chapter

Have you ever wondered how your favorite games are made? Computers cannot read English as humans do. Instead, they read machine code, composed of strings of binary 1's and 0's that stand for instructions that the computer can understand. Programmers write code that goes through a compiler or interpreter application that translates it into machine code.

In this article, we will write a simple game: Tic-Tac-Toe. Don't worry if you have never written any lines of code before; by the end of the article, you will have a working game and will understand the underlying principles of how it works.

How to Make Tic-Tac-Toe

Initialization

To start, when making a program, you need to define your starting variables and properties. This concept is known as initialization. In Tic-Tac-Toe, you would need to define a board, a list of win conditions, game state variables, and input handler variables.

#initialization
board = [(i+1) for i in range(9)]

winconditions = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

win = False
turn = "X"
turns = 0
choice = None

First, we need to create the Tic-Tac-Toe 3 x 3 (9-square) grid. We can do this by making a list that is 9 elements long.

In Python, you can create a list of any length using list = [<Value> for i in range(num)], which creates a list composed of <Value> with length num.

So, to create a board of 9 squares, you can do:

board = [(i+1) for i in range(9)]

which creates a board list of [1, 2, 3, 4, 5, 6, 7, 8, 9] to represent every tile on the board.

After that, we need to define every possible win condition in Tic-Tac-Toe. We can do this using a list of every winning 3-tile combination:

winconditions = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

This list contains the list indexes of every 3 in a row, 3 in a column, and 3 in a diagonal that are possible in a 3 x 3 Tic-Tac-Toe grid.

It is also important to define important game state variables inside the initialization part of our program:

win = False
turn = "X"
turns = 0
choice = None

Displaying the board

For a user to be able to run our program, it is important to display the board.

print("Tic-Tac-Toe")

def displayBoard():
    print(str(board[0]) + " | " + str(board[1]) + " | " + str(board[2]))
    print("--+---+--")
    print(str(board[3]) + " | " + str(board[4]) + " | " + str(board[5]))
    print("--+---+--")
    print(str(board[6]) + " | " + str(board[7]) + " | " + str(board[8]))

The first print statement simply outputs "Tic-Tac-Toe" to the console.

Throughout the game, you will need to output the Tic-Tac-Toe board multiple times. To avoid having excessively long code, it is recommended to define a function that handles displaying it.

This code displays the current state of the board, showing which grid squares are taken by checking their corresponding index in the list.

The Game Loop

Now, this is starting to get interesting. The game logic needs to run every time a player makes a move. Therefore, it is important to create a simple state machine so that the game runs until there is a winner or there is a draw.

#gameloop
while not (win or turns == 9):

    displayBoard()
    print("Type the corresponding number for where you want to move!")

    #safe input
    while True:
        try:
            choice = int(input("Player-" + turn + " Turn:"))
        except ValueError:
            print("Invalid choice")
        else:
            if choice-1 in range(9):
                if str(board[choice-1]) not in ["X", "O"]:
                    turns += 1
                    break
                else:
                    print("Invalid choice")
            else:
                print("Invalid choice")

    #update board
    board[choice-1] = turn

    #check win condition
    for condition in winconditions:
        if board[condition[0]] == board[condition[1]] == board[condition[2]] == turn:
            win = True
            break

    #change turn
    if not (win or turns == 9):
        if turn == "X":
            turn = "O"
        elif turn == "O":
            turn = "X"

To run until certain conditions are met, we can use a while loop. In this case, we need to run until there is a winner (win == True), or a draw (turns == 9).

Next, we update the board with the function we defined earlier, prompt the players for their move, and protect the input so the program does not crash.

What is a try-except-else statement?

A try-except-else statement allows us to protect our code against errors. You put code that might produce an error inside the "try," respond to specific errors inside the "except," and run code that you only want to run if there are no errors inside the "else."

Because of the way we prompt the user, we can only accept numbers. So if a player typed a word or a decimal, we would get a ValueError, and the program would crash. Furthermore, if a player types a number that does not exist on the board, we could get an IndexError later on when we attempt to access items from the board list whose index does not exist.

We can use try-except-else by putting the error-prone input code inside the "try," and protecting against the ValueError by checking for it in the "except." When we encounter the error, we can output "Invalid choice."

There are also logical errors that do not necessarily cause crashes. We still need to check if the input's corresponding board index is not occupied, and if the input number is from 1 to 9. Nest this block of code in a while True loop so that we keep checking for invalid inputs until the player enters an allowed input.

Controlling Player Turns and Checking If a Player Won or Drew

#update board
board[choice-1] = turn

#check win condition
for condition in winconditions:
    if board[condition[0]] == board[condition[1]] == board[condition[2]] == turn:
        win = True
        break

#change turn
if not (win or turns == 9):
    if turn == "X":
        turn = "O"
    elif turn == "O":
        turn = "X"

As you probably know, Tic-Tac-Toe has 2 players, "X" and "O," who alternate turns. Recall that we set the turn to "X" in the initialization part of our code. We need to update the board list to show that the player whose turn it was has control of that square:

board[choice-1] = turn

Also, it is important to check if a player has won yet. We can do this by checking all of our win conditions and seeing if a player occupies all squares with indices that match any case of the wincondition 3-pairs. If a player has met one of the winning conditions, we can end the game loop by setting win to True.

However, if the game has not ended yet, it is important to advance the game by changing whose turn it is. All we need to do is switch between "X" and "O," so we can just use an if-then-elif statement to switch between the 2 states.

After the Game Ends

When the game ends, there are still a couple of things we need to do to wrap up our program. We still need to display the final state of the board, and whether the game is a draw or if somebody has won.

displayBoard()
if win:
    print(str(turn) + " IS THE WINNER!")
else:
    print("DRAW!")

All we need to do is call our displayBoard function again and use an if statement to check if there is a winner.

Conclusion

Congratulations! You have just coded a game. Even seemingly simple games such as Tic-Tac-Toe require many lines of code and many important concepts in computer science, such as initialization, state management, iteration, and error handling. Even relatively small projects are important in learning the building blocks of programming to take on harder projects in the future.

Complete Code

#initialization
board = [(i+1) for i in range(9)]

winconditions = [(0,1,2),(3,4,5),(6,7,8),(0,3,6),(1,4,7),(2,5,8),(0,4,8),(2,4,6)]

win = False
turn = "X"
turns = 0
choice = None

print("Tic-Tac-Toe")

def displayBoard():
    print(str(board[0]) + " | " + str(board[1]) + " | " + str(board[2]))
    print("--+---+--")
    print(str(board[3]) + " | " + str(board[4]) + " | " + str(board[5]))
    print("--+---+--")
    print(str(board[6]) + " | " + str(board[7]) + " | " + str(board[8]))

#gameloop
while not (win or turns == 9):

    displayBoard()
    print("Type the corresponding number for where you want to move!")

    #safe input
    while True:
        try:
            choice = int(input("Player-" + turn + " Turn:"))
        except ValueError:
            print("Invalid choice")
        else:
            if choice-1 in range(9):
                if str(board[choice-1]) not in ["X", "O"]:
                    turns += 1
                    break
                else:
                    print("Invalid choice")
            else:
                print("Invalid choice")

    #update board
    board[choice-1] = turn

    #check win condition
    for condition in winconditions:
        if board[condition[0]] == board[condition[1]] == board[condition[2]] == turn:
            win = True
            break

    #change turn
    if not (win or turns == 9):
        if turn == "X":
            turn = "O"
        elif turn == "O":
            turn = "X"

#End Game
displayBoard()
if win:
    print(str(turn) + " IS THE WINNER!")
else:
    print("DRAW!")