Tic-Tac-Toe vs Computer — Free HTML CSS JS Snippet

Tic-Tac-Toe vs Computer · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Flat 9-cell board array with all 8 win lines hardcoded as index triplets (rows, columns, diagonals)
checkWinner() returns both the winning mark and the exact 3 winning indices for precise highlighting
Heuristic AI priority chain: win now, block opponent, take center, take corner, take edge
findWinningMove(mark) is reused for both offensive (win) and defensive (block) move detection
450ms setTimeout delay before the computer moves so its response feels deliberate, not instant
Disabled-cell and turn-lock guards prevent moves out of turn or on already-filled cells
Persistent session scoreboard (X wins / O wins / draws) that survives across New Game resets
Pop-in animation on newly placed marks and a distinct highlight class on the winning line

About this UI Snippet

Tic-Tac-Toe vs Computer — Heuristic AI Opponent, Win-Line Detection & Session Scoreboard

Screenshot of the Tic-Tac-Toe vs Computer snippet rendered live

Tic-Tac-Toe is small enough to reason about completely, which makes it an ideal vehicle for teaching a genuinely useful pattern: a rule-based heuristic AI that plays competently without the complexity of a full minimax search. This snippet implements a real, playable game where the human is X and the computer is O, with a four-tier decision heuristic, accurate win/draw detection across all eight lines, and a running scoreboard that persists across rounds within the session.

Representing the board and detecting wins

The board state is a flat array of nine values, cellState, where each entry is 'X', 'O', or null. All eight possible winning lines — three rows, three columns, and two diagonals — are hardcoded as index triplets in the WIN_LINES array, e.g. [0,4,8] for the main diagonal. checkWinner() iterates every line and checks whether all three cells share the same non-null value; if so it returns the winning mark and the specific line indices so the UI can highlight exactly those three cells. If no line matches and every cell is filled, the function returns a draw result instead. This flat-array-plus-index-triplet approach avoids any 2D coordinate math entirely — the nine cells and eight lines are small enough that hardcoding is clearer than deriving them algorithmically.

The four-tier heuristic AI: win, block, center, corners, edges

Rather than implementing minimax or another exhaustive search, the computer opponent in computerMove() follows a well-known Tic-Tac-Toe heuristic that plays correctly (or draws) against any human opponent in the vast majority of real games, and is far easier to read and extend than a recursive search. The priority order is: (1) win nowfindWinningMove('O') scans every line for one where the computer already has two marks and the third cell is empty, and takes it immediately; (2) block the human — if no winning move exists, the same function is called with 'X' instead of 'O' to find and occupy the cell that would let the human complete a line next turn; (3) take the center — cell index 4 is part of four different winning lines (one row, one column, both diagonals), making it statistically the most valuable opening cell; (4) take a corner — corners (indices 0, 2, 6, 8) are each part of three lines and are chosen randomly among the available ones; (5) take an edge — edges (1, 3, 5, 7) are each part of only two lines and are the last resort. This ordering is what makes the opponent feel "smart" without ever running a deep search: it always secures an immediate win or blocks an immediate loss, and otherwise falls back to positionally sound cells.

findWinningMove() as a shared building block

The same function powers both the "can I win" and "must I block" checks — the only difference is which mark is passed in. For each of the eight lines, it counts how many cells already hold the target mark and how many are still empty. A line with exactly two marks of the target and exactly one empty cell is a move that completes (or would complete) that line; the function returns that empty cell's index immediately. Reusing one function for both offense and defense keeps the AI logic compact and makes the priority order in computerMove() read almost like plain English: try to win, else try to block, else fall through the positional preferences.

Turn sequencing and the scoreboard

humanMove() places an X, checks for a winner, and — if the game continues — disables the board, updates the turn indicator to "Computer's turn...", and schedules computerMove() after a 450ms setTimeout so the computer's response feels like a deliberate move rather than an instant snap. Win detection happens through the same endTurnCheck() path after both human and computer moves, keeping the win/draw logic in exactly one place. The scores object ({ X, O, D }) accumulates across rounds and is rendered to the scoreboard on every completed game; calling newGame() resets the board and turn state but deliberately leaves scores untouched, so the tally persists across as many rounds as the player wants within the session, similar to the running win/loss tracking used in the Word Guess Game.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to trace through computerMove() move by move against a specific board you describe, confirming exactly which tier of the win/block/center/corner/edge priority chain fires and why. It's also worth asking the assistant to construct a board position where a human fork would beat this heuristic, to make concrete why it is not a fully unbeatable minimax AI. Beyond understanding the current logic, use the assistant to extend the game: ask for a full minimax (with or without alpha-beta pruning) implementation as a selectable "Hard" difficulty, a local two-player pass-and-play mode, a persisted scoreboard using localStorage instead of in-memory state, or animated confetti on a human win. Treat the current heuristic as a deliberately simple, readable baseline rather than the ceiling of what the game can do.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a playable Tic-Tac-Toe game against a computer opponent in plain HTML, CSS, and JavaScript, with the human playing X and the computer playing O.

Requirements:
- A 3x3 clickable grid where clicking an empty cell places the human's X; clicking an already-filled cell or clicking while it is not the human's turn must do nothing.
- After the human's move, if the game has not ended, the computer must make a move using this exact priority order, checked in sequence: (1) if the computer can complete three-in-a-row this turn, take that winning move; (2) otherwise if the human would complete three-in-a-row on their next turn, take that cell to block it; (3) otherwise take the center cell if it is free; (4) otherwise take a random available corner cell; (5) otherwise take a random available edge cell.
- Detect a win by checking all 8 possible lines (3 rows, 3 columns, 2 diagonals) after every move, and when a line is completed, visually highlight the exact three winning cells distinctly from the rest of the board.
- Detect a draw when all 9 cells are filled with no winning line, and display a clear draw message.
- Clearly display whose turn it is at all times, including a brief "computer is thinking" state between the human's move and the computer's response (a short artificial delay before the computer plays is fine).
- Maintain and display a running scoreboard of X wins, O wins, and draws that persists and accumulates across multiple rounds played in the same session, only resetting when the page is reloaded.
- Provide a "New Game" button that resets the board and turn state for a fresh round without resetting the accumulated scoreboard.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Make your moveClick any empty cell to place an X. humanMove() rejects clicks on filled cells or while it is not the human's turn (cellState[index] or !humanTurn guards).
  2. 2
    Watch the computer respondAfter your move, the board disables and the turn indicator reads "Computer's turn..." for 450ms before computerMove() runs findWinningMove('O'), then findWinningMove('X') to block, then falls back to center, corners, and edges.
  3. 3
    Try to beat the heuristicBecause the AI always takes an immediate win or block, the only way to beat it is to create a "fork" — two simultaneous winning threats in one move — which this heuristic does not defend against, unlike a full minimax search.
  4. 4
    Read the win-line highlightOn a completed game, checkWinner() returns the exact three winning indices, and endTurnCheck() adds the .win-cell class to just those cells so the winning row, column, or diagonal is visually highlighted.
  5. 5
    Track the running scoreboardThe scores object accumulates X wins, O wins, and draws across every round played in the session. Clicking "New Game" resets the board via newGame() but intentionally does not reset scores, so the tally keeps growing.
  6. 6
    Export and extendClick HTML or JSX to export. Swap findWinningMove's priority order, add a difficulty toggle that occasionally skips the block step for an easier opponent, or replace the heuristic with a full minimax function for a genuinely unbeatable AI.

Real-world uses

Common Use Cases

Teaching example for rule-based heuristic AI vs. minimax
This is a clear, readable introduction to game AI for learners before they tackle minimax or alpha-beta pruning. The win-block-center-corner-edge priority chain demonstrates that a strong-feeling opponent does not require exhaustive search — it is a good stepping stone toward explaining why this specific heuristic is beatable via forking, motivating the jump to a full minimax implementation.
Standalone single-player game widget for a portfolio or games page
Drop this in alongside a Sliding Number Puzzle or Whack-a-Mole Game as a quick, self-contained diversion. No backend or multiplayer infrastructure is required since the "opponent" is entirely client-side logic.
Themeable card-style game component for a design system
The board is wrapped in a single elevated card with a scoreboard footer, making it easy to restyle with different accent colors (swap #6366f1 and #f97316 for brand colors) and drop into a dashboard, kiosk, or marketing page without additional layout work.
Starting point for a difficulty-selectable or unbeatable AI mode
Because the win/block/positional logic is cleanly separated into named functions, it is straightforward to add a difficulty selector: an "Easy" mode that randomly skips the blocking step, a "Medium" mode using the current heuristic, and a "Hard" mode that swaps computerMove() for a full minimax search for a mathematically unbeatable opponent.
Local two-player mode extension exercise
The turn-sequencing logic (humanTurn flag, alternating placeMark calls) is a natural base for converting the game into local two-player pass-and-play by removing the setTimeout-delayed computerMove() call and instead alternating humanMove() between two tracked players.
Accessibility and keyboard-navigation extension for the game grid
The board currently uses button elements for each cell, which are already keyboard-focusable and clickable via Enter/Space; extending it with aria-live announcements of whose turn it is and the game result would make the experience fully usable for screen reader users.

Got questions?

Frequently Asked Questions

Yes, but only through a specific tactic called forking. The AI always takes an immediate winning move and always blocks an immediate loss, so it can never be beaten by a straightforward three-in-a-row attempt. However, because it does not look more than one move ahead, a human who creates two simultaneous winning threats in a single move (a fork) can win, since the heuristic can only block one of the two threats on its next turn. A full minimax search would close this gap entirely.

findWinningMove(mark) loops through all 8 win lines and, for each one, counts how many cells already contain the given mark and how many cells are still empty. A line with exactly 2 cells of that mark and exactly 1 empty cell means playing that empty cell would complete the line. Calling it with 'O' finds the computer's own winning move; calling it with 'X' finds the move that would let the human win, which the computer then takes instead to block it. The same function powers both checks — only the mark argument changes.

This ordering reflects how many of the 8 winning lines pass through each cell type. The center cell (index 4) belongs to 4 lines (its row, its column, and both diagonals), making it the single most valuable cell. Each corner belongs to 3 lines (its row, its column, and one diagonal). Each edge belongs to only 2 lines (its row or column, no diagonal). Taking cells with more line membership keeps more future winning paths open, which is why the fallback priority is center, then a random corner, then a random edge.

The scores object ({ X, O, D }) is declared outside of newGame() and is intentionally never reset by it — newGame() only resets cellState, gameOver, and humanTurn. This lets players track a running tally of wins, losses, and draws across as many rounds as they want to play in one sitting. Refreshing the page does reset the scoreboard since it is only held in memory, not persisted to localStorage.

Yes. To make the computer move first, call computerMove() once at the start of newGame() before rendering, and set humanTurn = false initially. To make it play randomly instead of with the heuristic, replace the body of computerMove() with a single line that picks a random index from the empty cells: the four-tier priority chain (win, block, center, corners, edges) can be bypassed entirely or made probabilistic for an adjustable difficulty setting.