Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Tic-Tac-Toe with AI Project

Intermediate•1–2 days•minimax, recursion, game AI

The beginner board grows a brain: an AI that can never lose.

Share on X

What you will build

Take the two-player Tic-Tac-Toe and add a computer opponent powered by minimax — the algorithm that explores every possible future and chooses the move that can't be beaten. This is step two of the Tic-Tac-Toe path, and your first real game AI.

What you will learn

Feature checklist

Starter code

#include <array>
#include <algorithm>

using Board = std::array<std::array<char, 3>, 3>;

char winner(const Board& b);   // 'X', 'O', 'D' draw, ' ' still playing

// Score from O's perspective: +10 win, -10 loss, 0 draw.
int minimax(Board& b, bool aiTurn) {
    char w = winner(b);
    if (w == 'O') return 10;
    if (w == 'X') return -10;
    if (w == 'D') return 0;

    int best = aiTurn ? -100 : 100;
    for (int r = 0; r < 3; ++r)
        for (int c = 0; c < 3; ++c)
            if (b[r][c] == ' ') {
                b[r][c] = aiTurn ? 'O' : 'X';
                int s = minimax(b, !aiTurn);
                b[r][c] = ' ';
                best = aiTurn ? std::max(best, s) : std::min(best, s);
            }
    return best;
}

std::pair<int,int> bestMove(Board& b) {
    int bestScore = -100; std::pair<int,int> move{-1, -1};
    for (int r = 0; r < 3; ++r)
        for (int c = 0; c < 3; ++c)
            if (b[r][c] == ' ') {
                b[r][c] = 'O';
                int s = minimax(b, false);
                b[r][c] = ' ';
                if (s > bestScore) { bestScore = s; move = {r, c}; }
            }
    return move;
}

Full walkthrough

Step 1: Start from the beginner game — literally

Reuse the Board type (std::array<std::array<char, 3>, 3>), draw(), and wins() from the beginner Tic-Tac-Toe unchanged. This project is step 2 of the learning path precisely because the board machinery is solved; the ONLY new problem is choosing the computer's move.

Step 2: Score a finished board

Write a helper that returns +10 if O (the AI) has won, -10 if X has won, 0 otherwise (draw or unfinished). Minimax needs a number to maximise; these values are the entire definition of 'good' the AI will ever know.

Step 3: Write minimax(): the AI imagines every future

minimax(board, isMaximising) — if the board is terminal, return its score. Otherwise: for every empty cell, place the current side's mark, recurse for the other side, undo the mark, and keep the best score (max for O, min for X). The undo step is backtracking again — Sudoku's move in a new costume. On a 3×3 board the whole game tree is tiny, so no pruning is needed to be instant.

int minimax(Board& b, bool isMax) {
    int s = score(b);
    if (s != 0 || full(b)) return s;
    int best = isMax ? -100 : 100;
    for each empty cell {
        b[r][c] = isMax ? 'O' : 'X';
        int v = minimax(b, !isMax);
        b[r][c] = ' ';
        best = isMax ? std::max(best, v) : std::min(best, v);
    }
    return best;
}

Step 4: bestMove(): try every move, keep the best score

bestMove() loops over empty cells, tentatively places 'O', calls minimax for the human's reply, undoes, and remembers the cell with the highest value. First-best wins ties, which is fine. This function is the whole AI: everything else is plumbing you already wrote.

Step 5: Add Easy mode by sometimes not thinking

Easy mode: with probability ~50% (use your mt19937), pick a random empty cell instead of bestMove(). The AI becomes beatable without becoming stupid on every turn — difficulty as a dial on how OFTEN the AI plays perfectly is a trick real games use constantly.

Step 6: Wire the round loop and score tally

Alternate human (X) and AI (O) turns with wins() checked after each, tracking score across rounds and letting the player choose who moves first (moving first is a real advantage the tally makes visible). If the AI goes first, it opens in a corner or centre — minimax will find those itself; watch it happen.

Step 7: Try to beat Hard mode — you will not

Play twenty games on Hard and count your wins: the correct count is 0 (draws and losses only). If you ever win, either minimax's min/max is flipped somewhere or score() mislabels a winner — the unbeatability is a testable specification, not a boast. That is a rare and wonderful property in a program: a proof you can play.

Common mistakes

Compile and run

g++ -std=c++17 -o tictactoe_ai tictactoe_ai.cpp

Sample run

$ ./tictactoe_ai
Tic-Tac-Toe vs the computer. Difficulty (E)asy or (H)ard:
> H
You are X. Who moves first — (Y)ou or (C)omputer?
> Y
Your move, row and column:
> 1 1
Computer plays O at row 0, column 0.
O |   |  
---+---+---
  | X |  
---+---+---
  |   |  
Your move, row and column:
> 2 2
Computer plays O at row 0, column 2.
Your move, row and column:
> 0 1
Computer plays O at row 2, column 1 — blocked your column!
(...the game grinds to the only result Hard mode allows...)
It's a draw. Score: you 0 · computer 0 · draws 1
On Hard, minimax never loses.

Try it in your browser

No browser demo is included for this project. Build the C++ version below — that is the real program.

Related Intermediate projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS