TL;DR
  • 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 typeBoard indicesHow many
Rows0-1-2, 3-4-5, 6-7-83
Columns0-3-6, 1-4-7, 2-5-83
Diagonals0-4-8, 2-4-62
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:

  1. Wire bestMove(board) up so it plays automatically on the AI's turn.
  2. Play a full game against it and try to force a draw.
  3. 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.

OutcomeFormulaScore at 3 moves deepScore at 7 moves deep
AI wins10 - depth+7+3
AI losesdepth - 10-7-3
Draw000

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.

Tic tac toe has only 255,168 possible complete games. That's small enough for plain minimax to check the whole tree in a blink, no pruning required.

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.