Taking Turns
Right now every click is an x. A real game has to
remember whose turn it is. Today you add a second piece
of state alongside the board, and you learn the most important
rule about ordering your code: when to flip the turn.
The new idea: state beyond the board
Until now, the board array held the entire game. But "whose turn is it?" is a fact the board can't tell you reliably. It needs its own home. That's our first taste of a real principle:
A game's state is everything you'd need to perfectly resume it: the board, the current player, the score, whether it's over. Each of these is a variable. Tic-tac-toe's second variable is the current player.
Add it at the top, next to board:
let currentPlayer = "x"; // x always goes first
Using it, then flipping it
Two changes to handleClick. First, place
the current player's mark instead of a hardcoded
"x". Second, after a successful move, switch to the
other player:
function handleClick(i) {
if (board[i] !== "") return; // taken? bail
board[i] = currentPlayer; // place this player's mark
// flip to the other player
currentPlayer = currentPlayer === "x" ? "o" : "x";
render();
}
The ternary, decoded
That one line is JavaScript's conditional (ternary) operator. Read it as a question:
currentPlayer = (currentPlayer === "x") ? "o" : "x";
// condition if true if false
"Is the current player x? If yes, the new value is o. Otherwise, x." It's the same as this longer form — use whichever reads clearer to you:
if (currentPlayer === "x") {
currentPlayer = "o";
} else {
currentPlayer = "x";
}
The ordering trap (this is the real lesson)
Order matters enormously here, and it's a perfect example of a bug that looks fine. Watch what happens if you flip the player before placing the mark:
// WRONG ORDER
currentPlayer = currentPlayer === "x" ? "o" : "x"; // flip first
board[i] = currentPlayer; // place AFTER flip
X clicks, but the flip already happened, so you place an
o. The mark on screen is always the
wrong player. The guard clause still works, clicks
still register, the board still fills — it just plays
wrong. Use the value, then change it. Read
before you advance. That sequencing instinct will save you in
every game you build, especially when turns, scores, and timers
all update in one handler.
Show whose turn it is
A player can't see currentPlayer in a variable.
Surface it. Add a status line above the board in
index.html:
<div id="status"></div>
<div id="board"></div>
And update it inside render(), since render is your
single source of truth for what's on screen:
const statusEl = document.querySelector("#status");
function render() {
// ... existing cell-drawing loop ...
statusEl.textContent = "Turn: " + currentPlayer.toUpperCase();
}
Same discipline as the board: render() paints
everything the player sees, from current state. You
change state, call render() once, and the whole
screen — board and status — updates
together. One place to look when something's wrong.
Live demo
Turns alternate. Watch the status line and the state readout
track currentPlayer flipping after each move.
Build it yourself
-
Add
let currentPlayer = "x";at the top, byboard. -
In
handleClick: placecurrentPlayerinstead of"x", then flip with the ternary. -
Add the
<div id="status">to your HTML and the status line torender().
First make it work: alternate x and o, with the status
showing whose turn it is. Then
deliberately break it — move the flip
line above board[i] = currentPlayer,
refresh, and play a few moves. Watch the wrong marks land.
Predict what you'll see before you click, then
confirm. Put it back when you've seen it. Breaking it on
purpose is how the ordering rule stops being a sentence and
becomes instinct.
What you just learned
- State is plural. A game tracks several variables; the board is just one. Whose turn it is gets its own.
-
constvsletin practice:const board(same array, mutated contents) vslet currentPlayer(reassigned to a new value). - The ternary operator for compact either/or assignment.
- Sequencing: use a value before you change it. Order of operations in a handler is a real source of bugs.
- render() owns the whole screen, board and status alike, from current state.