Use a board you can inspect

Three in a Row is a local game for two people sharing one device. It does not need an animation loop or a computer opponent. The board is an array of nine strings. An empty string means an available square, and X or O records the player who claimed it. The screen is drawn from that array after each valid move.

The array starts at index zero. The first row uses 0, 1, and 2; the second uses 3, 4, and 5; the third uses 6, 7, and 8. Write those indexes on paper before adding features. A mistaken diagonal is easier to catch in that sketch than in a long chain of conditions.

List the eight winning lines

const wins = [
  [0, 1, 2], [3, 4, 5], [6, 7, 8],
  [0, 3, 6], [1, 4, 7], [2, 5, 8],
  [0, 4, 8], [2, 4, 6]
];
const winner = wins.find(line =>
  line.every(n => cells[n] === turn)
);

This is the template's line list and winner expression, formatted for reading. every checks all three positions in one line against the current player. find returns the first completed line, or undefined when none is complete. Because the check runs immediately after the current player moves, there is no need to test the other player on that turn.

The returned indexes also support a visual improvement: store the winning line and add a class to those three squares when drawing. Reset that stored line when a new round begins, so the next board does not inherit an old highlight.

Check a win before a draw

A valid selection writes the current mark and increments moves. Then the game checks for a winning line. Only if there is no winner does it check whether nine moves have been played. This order handles a win on the final square. Checking the full board first would incorrectly announce a draw.

If the round continues, the turn switches from X to O or O to X, and the status text updates. Do not switch the player before checking the board: the win expression would inspect the wrong mark. Keep these steps together so the move's effect is easy to trace.

Protect the rules at the input boundary

The first condition in pick returns if the round is over or the chosen cell is occupied. Buttons also become disabled when appropriate. Keeping the rule in the function as well as in the interface protects against accidental extra calls and future changes to the controls.

Each square is a normal HTML button with a row, column, and value in its accessible label. That makes this a useful starting point for learning keyboard interaction. After a move, the template focuses an available square. If you add decorative marks, preserve the button labels and visible focus indicator.

Test the endings deliberately

Use fresh rounds to try all three rows, all three columns, and both diagonals. Test an O win as well as an X win. For a draw, select these one-based square numbers in order: 1, 2, 3, 5, 4, 6, 8, 7, 9. X starts. Neither player should have a line, and the ninth move should produce a draw.

To test a final-move win, use 1, 2, 5, 3, 6, 4, 7, 8, 9. X completes the 1–5–9 diagonal on move nine. The result must be a win, despite the board also being full.

For your next feature, keep a local match score across restarts. Separate “new round” from “reset match”: the former clears cells and moves, while the latter also clears the totals. Make that distinction visible in the button labels so players know which action they are taking.

Put this lesson into practice: download Three in a Row or play the original before making your changes.

Keep building