DEV Community

Abimanyu P
Abimanyu P

Posted on

Tic-Tac-Toe

Understanding the Flow of a Tic-Tac-Toe Game in JavaScript

A Tic-Tac-Toe game is a good project for understanding how JavaScript works with the DOM, event listeners, functions, conditions, and arrays. In this example, two players, X and O, take turns clicking on a 3×3 board. After every move, JavaScript checks whether a player has won or whether the game has ended in a tie.

Setting Up the Players

const players = ["X", "O"];
let currentPlayer = players[0];
Enter fullscreen mode Exit fullscreen mode

The players array stores the two players. Since array indexing starts from 0, players[0] is "X".

Therefore, the game starts with:

currentPlayer = "X";
Enter fullscreen mode Exit fullscreen mode

The currentPlayer variable keeps track of whose turn it is.

Getting the Game Squares

let cells = document.getElementsByClassName("square");
Enter fullscreen mode Exit fullscreen mode

This gets all the HTML elements that have the class square.

For example, a Tic-Tac-Toe board can have nine squares:

0 | 1 | 2
---------
3 | 4 | 5
---------
6 | 7 | 8
Enter fullscreen mode Exit fullscreen mode

The cells collection contains these nine squares so that JavaScript can work with them.

Defining the Winning Combinations

const winningCombinations = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6]
];
Enter fullscreen mode Exit fullscreen mode

There are eight possible ways to win in Tic-Tac-Toe.

For example:

[0, 1, 2]
Enter fullscreen mode Exit fullscreen mode

represents the first row, while:

[0, 3, 6]
Enter fullscreen mode Exit fullscreen mode

represents the first column.

The diagonal is represented by:

[0, 4, 8]
Enter fullscreen mode Exit fullscreen mode

These combinations are later used by the calculateWin() function.

Handling a Player's Click

The handleEvents() function is responsible for adding a click event to every square.

const handleEvents = () => {
    for (let i of cells) {
        i.addEventListener("click", () => {
            // game logic
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

The for...of loop goes through every square in cells. An event listener is then attached to each square.

This means whenever a player clicks a square, the code inside the click event runs.

Checking Whether the Square Is Already Filled

The first thing the click event checks is whether the square already contains a player.

if (i.textContent === "X" || i.textContent === "O") {
    return alert("This cell is already filled!");
}
Enter fullscreen mode Exit fullscreen mode

If the square contains either "X" or "O", the function stops using return.

This prevents a player from replacing an existing move.

Placing the Player's Symbol

If the square is empty, the current player's symbol is placed inside it.

i.textContent = currentPlayer;
Enter fullscreen mode Exit fullscreen mode

If it is X's turn:

currentPlayer = "X";
Enter fullscreen mode Exit fullscreen mode

clicking a square changes it to:

X
Enter fullscreen mode Exit fullscreen mode

After the move, the game needs to check whether X has won or whether the board is full.

Checking for a Winner

The code calls:

const iswin = calculateWin();
Enter fullscreen mode Exit fullscreen mode

The calculateWin() function checks every winning combination.

for (let i of winningCombinations) {
    const [a, b, c] = i;
Enter fullscreen mode Exit fullscreen mode

Suppose the current combination is:

[0, 1, 2]
Enter fullscreen mode Exit fullscreen mode

Array destructuring assigns:

a = 0;
b = 1;
c = 2;
Enter fullscreen mode Exit fullscreen mode

The function then gets the three corresponding squares:

const square1 = document.getElementById(`square${a}`);
const square2 = document.getElementById(`square${b}`);
const square3 = document.getElementById(`square${c}`);
Enter fullscreen mode Exit fullscreen mode

It checks whether all three contain the same player:

if (
    square1.textContent &&
    square1.textContent === square2.textContent &&
    square1.textContent === square3.textContent
) {
    return true;
}
Enter fullscreen mode Exit fullscreen mode

The first condition:

square1.textContent
Enter fullscreen mode Exit fullscreen mode

makes sure the square isn't empty.

The other two conditions check whether all three squares contain the same symbol.

If a winning combination is found, calculateWin() returns true. Otherwise, after checking all combinations, it returns false.

Checking for a Tie

After checking for a winner, the game checks whether the board is full.

const isTie = gameTie();
Enter fullscreen mode Exit fullscreen mode

The gameTie() function goes through every square:

const gameTie = () => {
    for (let i of cells) {
        if (i.textContent === "") {
            return false;
        }
    }

    return true;
}
Enter fullscreen mode Exit fullscreen mode

If even one square is empty, the game is not a tie, so it returns false.

If every square is filled, it returns true.

Handling the Winner or Tie

After placing a symbol, two checks are made:

if (iswin) {
    return alert(`${currentPlayer} wins!`);
}

if (isTie) {
    return alert("It's a tie!");
}
Enter fullscreen mode Exit fullscreen mode

If iswin is true, the game displays the winner.

For example:

X wins!
Enter fullscreen mode Exit fullscreen mode

If nobody has won but the board is full, the game displays:

It's a tie!
Enter fullscreen mode Exit fullscreen mode

The return stops the rest of the click function from running.

Switching Between Players

If there is no winner and no tie, the game switches to the other player.

currentPlayer =
    currentPlayer === players[0]
        ? players[1]
        : players[0];
Enter fullscreen mode Exit fullscreen mode

This uses the ternary operator.

It can be understood as:

if (currentPlayer === "X") {
    currentPlayer = "O";
} else {
    currentPlayer = "X";
}
Enter fullscreen mode Exit fullscreen mode

So the flow becomes:

X's turn
   ↓
X clicks
   ↓
Check winner
   ↓
Check tie
   ↓
Switch to O
   ↓
O's turn
   ↓
O clicks
   ↓
Check winner
   ↓
Check tie
   ↓
Switch to X
Enter fullscreen mode Exit fullscreen mode

Starting the Game

At the bottom of the code, we have:

document.addEventListener("DOMContentLoaded", handleEvents);
Enter fullscreen mode Exit fullscreen mode

DOMContentLoaded runs when the HTML document has been loaded.

Only after the page has loaded does JavaScript call:

handleEvents();
Enter fullscreen mode Exit fullscreen mode

This is important because handleEvents() needs to find the HTML elements containing the square class.

Complete Game Flow

The overall flow of the program is:

HTML loads
    ↓
DOMContentLoaded
    ↓
handleEvents()
    ↓
Add click events to all squares
    ↓
Player clicks a square
    ↓
Check if square is already filled
    ↓
Place X or O
    ↓
calculateWin()
    ↓
gameTie()
    ↓
Is there a winner?
    ├── Yes → Show winner → Stop
    └── No
         ↓
    Is it a tie?
         ├── Yes → Show tie → Stop
         └── No
              ↓
        Switch player
              ↓
        Wait for next click
Enter fullscreen mode Exit fullscreen mode

Restart Function

There is also a restartButton() function:

const restartButton = () => {
    for (let i of cells) {
        i.textContent = "";
    }
}
Enter fullscreen mode Exit fullscreen mode

It removes the contents of all squares, effectively clearing the board.

However, in the current code, this function is only defined and is not connected to a button or called anywhere. Also, if you use it as a real restart function, you would normally reset currentPlayer as well.

Top comments (0)