Flutter Tic-Tac-Toe Part 3: Add an Unbeatable AI with Minimax (2026)

Your Tic-Tac-Toe game is playable and polished. The logic is solid (Part 1), the animations and score tracker are in place (Part 2). The one thing missing: something to play against when there is no second human around.

Part 3 adds a full single-player mode with three difficulty levels. Easy is trivially beatable — it plays random moves. Medium adds defensive awareness: it wins when it can, blocks when it must. Hard is unbeatable: it uses the minimax algorithm to explore every possible future game state and always pick the optimal move.

Everything from Parts 1 and 2 stays intact. This post is purely additive: a new mode screen, two enums, three AI functions, and a small delay to make the AI feel like it’s thinking.

📚 This is Part 3 of a 3-part series
Part 1: Core game logic — StatefulWidget, GridView, win/draw detection, AlertDialog
Part 2: Animations, score tracker, CustomPaint, InkWell, winning-cell highlight
Part 3 (this post): Single-player AI — Easy, Medium, and Hard (minimax)
📋 Prerequisites
  • Complete Part 1 and Part 2 first — this post adds AI on top of the working polished game
  • Comfortable with Dart functions and recursion — minimax calls itself repeatedly
  • Understand StatefulWidget and setState() — AI moves trigger the same setState flow as human moves
  • Know Navigator.push — used for the new game mode screen

1. What Makes an AI Unbeatable?

Classic Tic-Tac-Toe is a two-player, zero-sum game: one player’s gain is always the other’s loss. That makes it a perfect fit for the minimax algorithm, which works by assuming both players always play optimally and then pre-computing the best possible move from any board position.

The reason minimax produces an unbeatable AI is simple: it considers every possible future game before making a move. If the human plays perfectly, the best outcome the AI can guarantee is a draw. If the human makes any mistake, minimax finds and exploits it immediately.

Difficulty Strategy Can you beat it? Algorithm
EasyPicks a random empty cellYes, almost alwaysdart:math Random
MediumWins if it can, blocks if it must, else randomYes, with the right sequence1-move lookahead
HardExhaustive game-tree searchNo — best you can do is drawMinimax

2. Step 1 — GameMode and Difficulty Enums

Before any AI logic, add two enums at the top of main.dart. Enums make the code self-documenting and prevent magic strings like 'vsAi' or 'hard' scattered through the logic:

Update TicTacToePage to accept these as constructor parameters:

3. Step 2 — GameModeScreen: Choose 2P or vs AI

Add a GameModeScreen as the new entry point. This is a simple StatefulWidget (needed for the difficulty selector) that navigates to TicTacToePage with the chosen configuration:

4. Step 3 — Difficulty Selector UI

The difficulty selector uses Material 3’s SegmentedButton — a clean three-way toggle that maps perfectly to Easy / Medium / Hard:

5. Step 4 — Easy AI: Random Moves

The Easy AI scans for all empty cells and picks one at random using Dart’s dart:math library. It is completely trivial to beat — it will miss wins and ignore threats — but it is a useful building block because both Medium and Hard fall back to it when no better move exists:

6. Step 5 — Medium AI: Win, Block, Random

Medium difficulty adds a one-move lookahead: simulate each possible move on a copy of the board, check if it creates a win, and act accordingly. The key rule: never mutate the real board inside the AI logic — always work on List.from(board):

💡 Medium is beatable — here’s how
Medium only looks one move ahead. You can beat it by creating a fork: a board position where you have two different ways to win simultaneously. Medium will block one but can’t block both. Setting up a fork requires thinking 2–3 moves ahead, which Medium cannot do. Hard (minimax) blocks all forks.

7. Step 6 — Minimax Explained: How the Hard AI Thinks

Before writing any minimax code, understand the mental model. Minimax builds a complete decision tree of the game and scores every leaf node (terminal game state). It then propagates those scores back up the tree, with each level choosing the best move for the player whose turn it is.

The algorithm is recursive. It stops recursing when it hits a terminal state (win, loss, or full board), scores it, and returns that score up the call stack. Each level then picks the max or min of its children’s scores:

8. Step 7 — Implementing Minimax in Pure Dart

The implementation has three parts: base cases (when to stop recursing), the maximizing branch (AI’s turn), and the minimizing branch (human’s turn). Each recursive call works on a fresh copy of the board — this is the most important rule:

⚠️ The most important rule: always copy the board
Never do board[i] = ai inside minimax. Always create a copy first: final temp = List<String>.from(board); temp[i] = ai;. If you mutate the real board, recursive calls overwrite each other’s work, scores become meaningless, and the AI either crashes or plays randomly. This is the single most common minimax bug.

9. Step 8 — Choosing the Best Move

_minimax evaluates a board position and returns a score. A separate wrapper function iterates all empty cells, calls _minimax for each, and returns the index of the cell with the highest score:

10. Step 9 — AI Thinking Delay with Future.delayed

Without a delay, the AI responds so fast after a human move that both marks appear to happen simultaneously — the human’s tap and the AI’s response are in the same setState. A 400ms pause makes the AI feel deliberate and gives players time to see their own move first:

11. Step 10 — Wiring All Three Levels into _handleTap

The updated _handleTap adds two new guard conditions: ignore taps when the AI is thinking, and ignore taps on the AI’s turn (so the human can’t play for the AI). After a valid human move, it calls _maybeTriggerAiMove() to chain the AI response:

💡 Why isAiThinking must be a state variable
If you skip the _isAiThinking flag, a fast tap during the 400ms delay will fire _handleTap successfully — placing the human’s symbol on what should be the AI’s turn. The flag prevents this race condition and ensures turns alternate correctly even when the human taps rapidly.

12. Bonus: Alpha-Beta Pruning

Minimax on a 3×3 board is fast — it takes microseconds. But on a 4×4 board, the game tree grows exponentially (more cells, more branching) and plain minimax starts to lag. Alpha-beta pruning is an optimisation that cuts branches of the tree that cannot possibly affect the final decision.

13. Full main.dart — Copy-Paste Ready

The complete Part 3 file combines everything: enums, GameModeScreen, all AI helpers, and the updated TicTacToePage with the full Part 2 polish intact:

14. Common Beginner Mistakes

Mistake 1: Mutating the board inside minimax — wrong scores, wrong moves

Mistake 2: AI moves on the human’s turn — no guard in _handleTap

Mistake 3: Wrong isMaximizing initial value in _pickBestMove

Mistake 4: Checking win/block in wrong order in Medium AI

15. Series Wrap-Up: Where to Go from Here

You have now built a complete Flutter game from scratch across three posts. Here is what the series covered end-to-end:

Post Core concepts learned
Part 1: Core game logicStatefulWidget, setState, GridView.builder, GestureDetector, win/draw detection, AlertDialog, addPostFrameCallback
Part 2: Animations & polishAnimatedContainer, AnimatedScale, Curves, InkWell, CustomPaint, score tracking, widget extraction, SnackBar vs AlertDialog
Part 3: AI (this post)Enums, Navigator.push with args, SegmentedButton, dart:math, 1-move lookahead, minimax recursion, alpha-beta pruning, Future.delayed

Where to go next:

Challenge What you will learn
Expand to a 4×4 boardWhy minimax slows down, how alpha-beta pruning helps, depth-limited search
Persist scores with SharedPreferencesAsync data storage, the same pattern as dark mode persistence
Add dark modeColorScheme roles, ThemeMode — covered in our dark mode guide
Apply minimax to another gameConnect Four, Reversi — same algorithm, larger state space, heuristic evaluation functions
Explore the Flutter Casual Games ToolkitFlutter’s official game templates and audio packages for more complex casual games
Post Why it’s relevant
Part 1: Build a Flutter Tic-Tac-Toe from ScratchThe foundation — all game logic and the _winningCombos constant used throughout this post.
Part 2: Animations, Score Tracker & UI Glow-UpThe polish layer — AnimatedContainer, ScoreBoard, CustomPaint, and InkWell all carried forward here.
Flutter Widgets: Stateless vs StatefulThe core mental model for why _isAiThinking must be a state variable and how setState drives AI move rendering.
Flutter Navigation: Push, Pop, and Named RoutesThe Navigator.push() pattern used by GameModeScreen to open the game with the chosen configuration.
Flutter Dark Mode ToggleThe next natural enhancement — the game’s ColorScheme already supports it; just add a themeMode toggle.
Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply