How to Create a Tic Tac Toe AI With Minimax
October 1, 2026 · TicTacToe.now
- Minimax works by trying every possible move, scoring the outcome, and picking the best one for the current player.
- You need three pieces: a way to list open moves, a way to check for a winner, and the recursive scoring function itself.
- Scores are simple: +10 for an AI win, -10 for a loss, 0 for a draw, adjusted by how many moves it took.
- A full minimax tic tac toe AI is unbeatable - It will never lose, only win or draw.
What We're Actually Building
This is a build guide, not a theory lecture - By the end you'll have working pseudocode you can turn into real code in almost any language. If you want the conceptual "why does this even work" explanation first, our math and game theory page covers that side of it. If you've never written down the rules and winning lines yourself, our rules page is worth a quick look too, since the code below leans on them directly.
What minimax actually means
Minimax is the idea that the AI imagines every possible way the rest of the game could play out. It scores each ending. Then it picks the move that leads to its best guaranteed outcome, assuming you also play your best possible move back at it. It's basically an AI playing out the entire rest of the game in its head before it ever touches the board.
The three pieces you need
Every minimax tic tac toe AI is built from the same three parts. Get these three right and the rest of this guide is just wiring them together.
- A move lister: a small function that returns every open square on the board.
- A winner checker: a function that looks at the board and reports who, if anyone, has won.
- The minimax function: the recursive function that scores every possible ending and passes that score back up the chain.
The next four steps build each of these pieces in order, then put them to work picking real moves.
Step 1: Represent the Board
The board array
Start with the board as a simple array of 9 values, indexed 0 through 8. Each slot holds "X", "O", or an empty value. This flat structure makes it easy to loop through every square when you're checking for winners or open moves.
board = ["", "", "",
"", "", "",
"", "", ""]
Listing the open moves
Next, write a small function that scans the array and returns every empty index. Minimax will call this function on almost every step of every branch, so keep it short and simple.
function getOpenMoves(board):
moves = []
for i in 0 to 8:
if board[i] == "":
moves.append(i)
return moves
Nothing fancy yet - Just a list and a function that tells you which squares are still free to play. Everything else in this guide builds on top of these two pieces.
Step 2: Check for a Winner
The eight winning lines
Tic tac toe only has eight ways to win: three rows, three columns, and two diagonals. Store them as a simple list of index triples, so your code can check every one of them with a single loop.
| Line type | Board indices | How many |
|---|---|---|
| Rows | 0-1-2, 3-4-5, 6-7-8 | 3 |
| Columns | 0-3-6, 1-4-7, 2-5-8 | 3 |
| Diagonals | 0-4-8, 2-4-6 | 2 |
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
]
The checkWinner function
With the lines defined, checkWinner just loops through them and checks whether all three squares on any line match. Minimax calls this constantly, so keep it fast rather than clever.
function checkWinner(board):
for line in WIN_LINES:
a, b, c = line[0], line[1], line[2]
if board[a] != "" and board[a] == board[b] and board[b] == board[c]:
return board[a] // "X" or "O"
if getOpenMoves(board) is empty:
return "draw"
return null // game still going
Step 3: The Minimax Function Itself
This is the core of the whole build. Minimax calls itself on every possible next board state until it hits a finished game, then works its way back up, scoring each branch as it goes. One player is trying to raise the score as high as possible. The other is trying to push it as low as possible. That back-and-forth is exactly where the name "minimax" comes from.
function minimax(board, depth, isMaximizing):
winner = checkWinner(board)
if winner == "X": return 10 - depth
if winner == "O": return depth - 10
if winner == "draw": return 0
if isMaximizing:
best = -Infinity
for move in getOpenMoves(board):
board[move] = "X"
score = minimax(board, depth + 1, false)
board[move] = "" // undo the move
best = max(best, score)
return best
else:
best = Infinity
for move in getOpenMoves(board):
board[move] = "O"
score = minimax(board, depth + 1, true)
board[move] = "" // undo the move
best = min(best, score)
return best
The base cases
Every recursive function needs a stopping point, called a base case, or it will call itself forever. The first few lines of minimax check whether the game already ended. A finished game scores +10 for an AI win, -10 for a loss, or 0 for a draw, adjusted slightly by depth (how many moves deep the recursion has gone). These lines are what stop the recursion and hand a real number back up to whichever branch called it.
The maximizing branch
When it's X's simulated turn (isMaximizing is true), the function tries every open move, plays it, and calls itself again one level deeper. It keeps whichever result scores highest, because X is trying to maximize the score. Then it undoes the move before trying the next one.
The minimizing branch
When it's O's simulated turn, the logic flips. The function still tries every open move and recurses the same way. This time it keeps whichever result scores lowest, because O is trying to minimize the score. That constant switch between maximizing and minimizing is the entire algorithm.
Undo the move, every time
Notice the "undo the move" line after every recursive call. This is what lets one board array explore thousands of possible futures without ever needing to copy it. Skip that step and your AI will scramble every game it evaluates.
Step 4: Picking the Best Move
The bestMove function
The minimax function above tells you the score of a position, but you still need one more function to actually choose a move. This one tries every open square, runs minimax on each resulting board, and keeps whichever move produced the highest score for the AI.
function bestMove(board):
bestScore = -Infinity
move = null
for m in getOpenMoves(board):
board[m] = "X"
score = minimax(board, 0, false)
board[m] = ""
if score > bestScore:
bestScore = score
move = m
return move
Putting it to work
Call bestMove(board) whenever it's the AI's turn, and it returns the single best square to play. This is the same logic that powers our own Hard mode, which never loses a game.
Test your build with a short checklist:
- Wire
bestMove(board)up so it plays automatically on the AI's turn. - Play a full game against it and try to force a draw.
- Check that it never lets you win, only draw or lose.
Why This AI Literally Cannot Lose
It already knows your best response
Minimax explores every possible line of play before choosing a move. That means it never gets surprised. It already knows exactly what happens if you play your best possible response to anything it does, because it picked its own move assuming you would.
That's the whole reason it's unbeatable rather than just "pretty good." It isn't guessing. It isn't following rules of thumb like always take the center. It's calculating the entire rest of the game, every single turn. Compare that to Easy mode, which plays weaker moves on purpose so newer players have a fair chance to win.
It also means the AI never needs to be taught what a fork is. A fork just falls out of the math on its own, since any move that opens two winning lines scores better than a move that only opens one.
Why the depth adjustment matters
Look back at the base case scores: 10 - depth and depth - 10, instead of a flat 10 and -10. That small adjustment does a lot of work.
It makes the AI prefer a fast win over a slow one. It also makes the AI prefer a slow loss over a fast one. So if the AI is ever stuck in a truly losing spot, it will at least drag the game out instead of handing you a quick win.
| Outcome | Formula | Score at 3 moves deep | Score at 7 moves deep |
|---|---|---|---|
| AI wins | 10 - depth | +7 | +3 |
| AI loses | depth - 10 | -7 | -3 |
| Draw | 0 | 0 | 0 |
Bigger scores always beat smaller ones during the maximizing step. A +7 always wins out over a +3 - The depth number is just what tells the two apart.
Making It Faster: Alpha-Beta Pruning
Why tic tac toe doesn't need it
Tic tac toe's board is small enough that plain minimax runs instantly. Still, it's worth knowing the next technique exists: alpha-beta pruning.
When pruning starts to matter
Alpha-beta pruning skips exploring branches that can't possibly change the outcome. It cuts the number of positions checked dramatically, without changing the final answer at all. You won't need it for a 3x3 board. But if you ever stretch this same approach onto a bigger grid, like our 4x4 variant, pruning stops being optional and becomes the only way the AI can respond in reasonable time.
Where to go from here
Build the plain version first. Get it beating you consistently, then look into pruning once you understand why the basic version works. And if you'd rather sharpen your own play than write code, our how-to-win guide covers the same ideas from a human player's side.