How to Build Tic-Tac-Toe in Python (Complete Beginner Tutorial)
Tic-tac-toe is the perfect first game to program. The rules fit in five lines. The whole project fits in around a hundred. And along the way, you'll touch nearly every fundamental Python concept: lists, loops, conditions, functions, user input, and a little bit of AI.
This tutorial assumes you know Python basics β variables, lists, functions, if statements, for loops. You don't need to know anything about game development or algorithms. We'll build the whole thing from scratch.
What we're building
A command-line tic-tac-toe game in Python 3 with three pieces:
- A working 2-player mode where two humans play on the same computer.
- A "dumb" AI that plays random moves.
- An "unbeatable" AI using the minimax algorithm.
You'll end up with a program that looks something like this when you run it:
Welcome to Tic-Tac-Toe!
1 | 2 | 3
-----------
4 | 5 | 6
-----------
7 | 8 | 9
X, enter your move (1-9): 5
1 | 2 | 3
-----------
4 | X | 6
-----------
7 | 8 | 9
O, enter your move (1-9): _
Let's get started.
Step 1: Representing the board
Open a new file called tictactoe.py. The first thing we need is some way to represent the board.
The simplest approach: a list of 9 strings, one per cell. Cells start as a space character (which renders as blank) and become "X" or "O" as moves happen.
board = [" "] * 9
We'll use index positions 0 through 8 internally, but display them to the user as 1 through 9 (like a phone keypad β top-left is 1, top-right is 3, bottom-right is 9). The mapping is just position - 1.
Now we need a function to print the board:
def print_board(board):
print()
print(f" {board[0]} | {board[1]} | {board[2]} ")
print("-----------")
print(f" {board[3]} | {board[4]} | {board[5]} ")
print("-----------")
print(f" {board[6]} | {board[7]} | {board[8]} ")
print()
That's it. Run it with a test board and you'll see your tic-tac-toe grid render in the terminal.
Step 2: Making moves
To make a move, we need to ask the player which cell, validate their answer, and update the board. Here's the function:
def get_move(board, player):
while True:
choice = input(f"{player}, enter your move (1-9): ")
if not choice.isdigit():
print("Please enter a number between 1 and 9.")
continue
position = int(choice) - 1
if position < 0 or position > 8:
print("That's not a valid square. Try 1-9.")
continue
if board[position] != " ":
print("That square is already taken.")
continue
return position
This function keeps asking until the player gives valid input β a digit between 1 and 9 that refers to an empty cell. It returns the chosen position as a 0-indexed integer.
Step 3: Detecting wins
To know when the game is over, we need to check the eight winning lines (three rows, three columns, two diagonals).
WIN_LINES = [
(0, 1, 2), (3, 4, 5), (6, 7, 8), # rows
(0, 3, 6), (1, 4, 7), (2, 5, 8), # columns
(0, 4, 8), (2, 4, 6), # diagonals
]
def check_win(board, player):
for line in WIN_LINES:
if all(board[i] == player for i in line):
return True
return False
def is_draw(board):
return all(cell != " " for cell in board)
The check_win function returns True if the specified player has three in a row. The is_draw function returns True when no empty cells remain. (You should check for wins before checking for a draw β a board can be full but with the last move being a winning one.)
Step 4: The full game loop
Now let's wire everything together. Here's the main loop:
def play_two_player():
board = [" "] * 9
current = "X"
print("Welcome to Tic-Tac-Toe!")
print_board(board)
while True:
position = get_move(board, current)
board[position] = current
print_board(board)
if check_win(board, current):
print(f"{current} wins!")
return
if is_draw(board):
print("It's a draw!")
return
current = "O" if current == "X" else "X"
if __name__ == "__main__":
play_two_player()
Save the file and run python tictactoe.py. You should be able to play a full two-player game in the terminal. Test it with yourself a few times β try a winning game, a losing game, and a draw. Make sure the win-detection catches all three rows, both diagonals, and so on.
Step 5: A random AI
Two-player mode is fun, but the real challenge is playing against a computer. Let's start with the simplest possible AI: one that picks moves at random.
import random
def get_random_move(board):
available = [i for i, cell in enumerate(board) if cell == " "]
return random.choice(available)
This is a bad AI. It misses obvious wins, fails to block, and generally plays like a six-year-old who's never seen the game before. But it's a useful starting point β you can verify your game loop works against an AI opponent before adding sophistication.
Modify your main loop to call get_random_move for O instead of asking the human:
def play_vs_random():
board = [" "] * 9
current = "X"
print("You are X. The computer is O (random).")
print_board(board)
while True:
if current == "X":
position = get_move(board, current)
else:
position = get_random_move(board)
print(f"Computer plays {position + 1}")
board[position] = current
print_board(board)
if check_win(board, current):
print(f"{current} wins!")
return
if is_draw(board):
print("It's a draw!")
return
current = "O" if current == "X" else "X"
You should be able to easily beat this AI most of the time.
Step 6: A rule-based AI (smarter, still beatable)
Before jumping to minimax, let's build an AI that follows the priority list from our strategy guide. This kind of "rule-based" AI is a useful exercise β it forces you to think about how a human plays the game.
def get_smart_move(board, ai_player):
opponent = "O" if ai_player == "X" else "X"
# Priority 1: Win if possible
for line in WIN_LINES:
cells = [board[i] for i in line]
if cells.count(ai_player) == 2 and cells.count(" ") == 1:
for i in line:
if board[i] == " ":
return i
# Priority 2: Block opponent's win
for line in WIN_LINES:
cells = [board[i] for i in line]
if cells.count(opponent) == 2 and cells.count(" ") == 1:
for i in line:
if board[i] == " ":
return i
# Priority 3: Take the center
if board[4] == " ":
return 4
# Priority 4: Take a corner
for i in [0, 2, 6, 8]:
if board[i] == " ":
return i
# Priority 5: Take an edge
for i in [1, 3, 5, 7]:
if board[i] == " ":
return i
return None # No moves left
This AI plays reasonably well β it'll never miss an immediate win or fail to block. But it doesn't look more than one move ahead, so it misses fork setups. A determined human player can beat it.
Step 7: The unbeatable AI (minimax)
For an AI that never loses, we need minimax β the algorithm we explain in depth in our minimax article. The short version: the AI explores every possible sequence of moves to the end of the game, scores each outcome, and picks the move that gives the best score assuming the opponent plays optimally.
Here's the implementation:
def minimax(board, current, ai_player, depth=0):
opponent = "O" if ai_player == "X" else "X"
if check_win(board, ai_player):
return 10 - depth, None
if check_win(board, opponent):
return depth - 10, None
if is_draw(board):
return 0, None
available = [i for i, cell in enumerate(board) if cell == " "]
best_move = None
if current == ai_player:
best_score = -float('inf')
for move in available:
board[move] = current
score, _ = minimax(board, opponent, ai_player, depth + 1)
board[move] = " "
if score > best_score:
best_score = score
best_move = move
else:
best_score = float('inf')
for move in available:
board[move] = current
score, _ = minimax(board, ai_player, ai_player, depth + 1)
board[move] = " "
if score < best_score:
best_score = score
best_move = move
return best_score, best_move
def get_perfect_move(board, ai_player):
_, move = minimax(board, ai_player, ai_player)
return move
That's the entire algorithm. About 25 lines of code, and you have a tic-tac-toe player that has never lost a game (a fact verified mathematically β it's proven, not just observed).
Now plug it into your game loop:
def play_vs_perfect():
board = [" "] * 9
current = "X"
ai = "O"
print("You are X. The computer is O (perfect play).")
print_board(board)
while True:
if current != ai:
position = get_move(board, current)
else:
position = get_perfect_move(board, ai)
print(f"Computer plays {position + 1}")
board[position] = current
print_board(board)
if check_win(board, current):
print(f"{current} wins!")
return
if is_draw(board):
print("It's a draw!")
return
current = "O" if current == "X" else "X"
Try to beat it. You can't. With the best you can possibly do, you'll draw.
Why subtract the depth?
In the minimax code, I wrote return 10 - depth and return depth - 10 instead of just 10 and -10. Why?
Without the depth adjustment, the AI treats all wins as equally valuable β whether it wins in 2 moves or 6 moves. That can lead to weird behavior, like the AI dragging out a win when it could end immediately. Subtracting depth makes faster wins more valuable and slower losses less painful. The AI now prefers to win quickly and to lose slowly (giving the opponent more chances to slip up if it's in a losing position). Small change, big improvement in how the AI feels to play against.
Where to go from here
You now have a complete, working tic-tac-toe game with three AI levels. Some ideas for extending it:
- Difficulty levels. Mix the perfect AI with the random AI β pick the optimal move some percentage of the time, otherwise play randomly. This gives you Easy/Medium/Hard the same way our site implementation does.
- Add a GUI. Try
tkinter(built into Python) orpygamefor a graphical version with mouse input. - Try Misère mode. Three in a row loses instead of wins. Change exactly one line: flip the signs in the terminal score returns. Suddenly your AI plays a completely different game.
- Try larger boards. 4Γ4 or 5Γ5 with "need four in a row" rules. Full minimax becomes too slow β you'll need to add a depth limit and a heuristic evaluation function for non-terminal positions.
- Port it to JavaScript. Our JavaScript tutorial walks through the same project for the web.
Tic-tac-toe is a tiny project. But every concept you used here β game state, input loops, win detection, recursive search, optimization β scales up to almost every game AI ever written. Deep Blue and AlphaGo are not fundamentally different from what you just built. They just have a lot more numbers and a lot more compute. The skeleton is the same.
Play the JavaScript Version β
Related reading
- How the AI Thinks: Minimax Explained Simply β the math behind your unbeatable AI
- How to Never Lose at Tic-Tac-Toe β the human version of what your AI is doing
- How to Build Tic-Tac-Toe in JavaScript β same project, web version