Coding a Tic-Tac-Toe Engine That Never Loses — Full Web App Included
Build an unbeatable Tic-Tac-Toe AI in C++ using recursion and minimax, with a full web app demo.
Tic-Tac-Toe might seem like a trivial childhood game, but building an unbeatable AI engine for it is an excellent exercise in recursion, algorithmic thinking, and clean game logic.
In this post, I’ll walk you through how I developed a perfect Tic-Tac-Toe engine in C++ — one that always tries to win and never loses.
(Yes, the title mentions a web app, but this article focuses on the underlying engine logic.)

Why Build a Tic-Tac-Toe Engine in C++?
Even such a simple game can teach powerful programming concepts that transfer into real AI and game-theory problems:
- Recursion and backtracking — exploring all possible future moves.
- Minimax algorithm — a classic two-player decision-making technique.
- Game state management — detecting wins, draws, and invalid moves.
- Decision-making under constraints — choosing between winning quickly and delaying an inevitable loss.
By completing this project, you don’t just create an AI that never loses —
you gain hands-on experience with recursion, algorithm design, and structuring real game logic.
Understanding the Minimax Algorithm
The backbone of the AI engine is the Minimax algorithm, a recursive strategy used in two-player, turn-based games.
Here’s the basic idea:
1. Simulate all possible future moves for both the human and the AI.
2. Evaluate each ending state:
- AI win → +10 - depth (prefer quicker wins)
- Human win → depth - 10 (delay losses)
- Draw → 0
3. The AI chooses the move with the highest score.
The depth-based scoring system is crucial.
It ensures the AI behaves intelligently — aiming for fast victories and slow defeats — making it effectively unbeatable.
The Complete C++ Tic-Tac-Toe Engine
Here’s the full engine code:
#include <iostream>
#include <vector>
using namespace std;
const char HUMAN = 'X';
const char AI = 'O';
const char EMPTY = ' ';
void printBoard(const vector<char>& board) {
for (int i = 0; i < 9; i++) {
cout << " " << board\[i\];
if (i % 3 != 2) cout << " |";
else if (i != 8) cout << "\\\\n-----------\\\\n";
}
cout << "\\\\n\\\\n";
}
char checkWinner(const vector<char>& b) {
int wins\[8\]\[3\] = {
{0,1,2},{3,4,5},{6,7,8},
{0,3,6},{1,4,7},{2,5,8},
{0,4,8},{2,4,6}
};
for (auto &w : wins) {
if (b\[w\[0\]\] != EMPTY &&
b\[w\[0\]\] == b\[w\[1\]\] &&
b\[w\[1\]\] == b\[w\[2\]\]) return b\[w\[0\]\];
}
return EMPTY;
}
bool isFull(const vector<char>& board) {
for (char c : board) if (c == EMPTY) return false;
return true;
}
int minimax(vector<char>& board, bool isMaximizing, int depth) {
char winner = checkWinner(board);
if (winner == AI) return 10 - depth;
if (winner == HUMAN) return depth - 10;
if (isFull(board)) return 0;
if (isMaximizing) {
int bestScore = -10000;
for (int i = 0; i < 9; i++) {
if (board\[i\] == EMPTY) {
board\[i\] = AI;
bestScore = max(bestScore, minimax(board, false, depth + 1));
board\[i\] = EMPTY;
}
}
return bestScore;
} else {
int bestScore = 10000;
for (int i = 0; i < 9; i++) {
if (board\[i\] == EMPTY) {
board\[i\] = HUMAN;
bestScore = min(bestScore, minimax(board, true, depth + 1));
board\[i\] = EMPTY;
}
}
return bestScore;
}
}
int bestMove(vector<char>& board) {
int bestScore = -10000;
int move = -1;
for (int i = 0; i < 9; i++) {
if (board\[i\] == EMPTY) {
board\[i\] = AI;
int score = minimax(board, false, 0);
board\[i\] = EMPTY;
if (score > bestScore) {
bestScore = score;
move = i;
}
}
}
return move;
}
int main() {
vector<char> board(9, EMPTY);
cout << "Welcome to Tic Tac Toe!\\\\n";
cout << "You are X, AI is O.\\\\n";
char first;
cout << "Who should go first? (H for Human, A for AI): ";
cin >> first;
first = toupper(first);
bool humanTurn = (first == 'H');
printBoard(board);
while (true) {
if (humanTurn) {
int pos;
cout << "Enter your move (1-9): ";
cin >> pos;
pos--;
if (pos < 0 || pos > 8 || board\[pos\] != EMPTY) {
cout << "Invalid move. Try again.\\\\n";
continue;
}
board\[pos\] = HUMAN;
printBoard(board);
if (checkWinner(board) == HUMAN) {
cout << "You win!\\\\n";
break;
}
if (isFull(board)) {
cout << "Draw!\\\\n";
break;
}
} else {
int aiMove = bestMove(board);
board\[aiMove\] = AI;
cout << "AI chooses position " << (aiMove + 1) << "\\\\n";
printBoard(board);
if (checkWinner(board) == AI) {
cout << "AI wins!\\\\n";
break;
}
if (isFull(board)) {
cout << "Draw!\\\\n";
break;
}
}
humanTurn = !humanTurn;
}
return 0;
}
🔍 How the Engine Works — Step by Step
- Board Representation:
A simple vector of char with length 9 stores the game state. - Move Validation:
The player can never overwrite an already-filled cell. - Recursive Minimax Search:
The engine evaluates every possible game state to find the best move. - Dynamic Turn Choice:
At runtime, the user chooses whether the AI or human starts first. - Depth-Based Scoring:
Faster wins get better scores; slower losses get penalized.
This combination results in an engine that plays perfectly.
It can win or draw every game, but it can never be beaten.
Final Thoughts
Building this Tic-Tac-Toe engine wasn’t just about making a simple game —
it was about creating a perfect AI opponent using recursion, scoring heuristics, and clean program structure.
Key takeaways:
- Recursive exploration of all future moves.
- Depth-based scoring to optimize strategic choices.
- A classic Minimax algorithm that guarantees an unbeatable AI.
- Flexible turn selection for AI or human.
- Clear separation of engine logic and user interaction.
And since I wanted others to try it easily, I also built a full HTML, CSS, and JavaScript version of this engine — playable directly in a browser.
👉 Play the web version: Tic-Tac-Toe Web App
👉 View the full source code: GitHub Repository — Tic-Tac-Toe Engine
What started as a small C++ experiment evolved into a complete, cross-platform game engine that works in both console apps and web interfaces.
Feel free to explore the code, experiment with new features like difficulty levels, animations, or score tracking — and most importantly, have fun learning from it!