Building a Simple JavaScript Interactive Tic-Tac-Toe Game: A Beginner’s Guide

Written by

in

In the world of web development, JavaScript reigns supreme for adding interactivity to websites. One of the best ways to learn JavaScript is by building small, engaging projects. Today, we’ll dive into creating a classic game: Tic-Tac-Toe. This project is perfect for beginners because it introduces fundamental JavaScript concepts in a fun and accessible way. By the end of this guide, you’ll have a fully functional Tic-Tac-Toe game and a solid foundation for more complex JavaScript projects.

Why Build a Tic-Tac-Toe Game?

Tic-Tac-Toe is an ideal project for several reasons:

  • It’s Simple: The game’s rules are straightforward, allowing you to focus on the JavaScript code.
  • It’s Engaging: The interactive nature keeps you motivated throughout the learning process.
  • It Covers Key Concepts: You’ll learn about variables, functions, event listeners, DOM manipulation, and conditional statements – all essential for JavaScript development.
  • It’s Rewarding: Seeing your game come to life is a satisfying experience.

This project will not only teach you the basics but also give you a taste of how games are built on the web. Let’s get started!

Setting Up Your Project

Before we start coding, let’s create the basic HTML structure for our game. This will provide the visual layout and allow us to interact with the game elements using JavaScript. Create three files: index.html, style.css, and script.js.

index.html: This file will contain the HTML markup for our game board and any other UI elements like a game over message.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Tic-Tac-Toe</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Tic-Tac-Toe</h1>
        <div class="board">
            <div class="cell" data-cell-index="0"></div>
            <div class="cell" data-cell-index="1"></div>
            <div class="cell" data-cell-index="2"></div>
            <div class="cell" data-cell-index="3"></div>
            <div class="cell" data-cell-index="4"></div>
            <div class="cell" data-cell-index="5"></div>
            <div class="cell" data-cell-index="6"></div>
            <div class="cell" data-cell-index="7"></div>
            <div class="cell" data-cell-index="8"></div>
        </div>
        <div class="status"></div>
        <button class="restart">Restart</button>
    </div>
    <script src="script.js"></script>
</body>
</html>

style.css: This file will hold the CSS styles to make our game visually appealing. You can customize the look and feel as much as you like. A basic example is shown below:

.container {
    display: flex;
    flex-direction: column;
    align-items: center;
    font-family: sans-serif;
}

.board {
    display: grid;
    grid-template-columns: repeat(3, 100px);
    grid-template-rows: repeat(3, 100px);
    margin-top: 20px;
}

.cell {
    width: 100px;
    height: 100px;
    border: 1px solid black;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: 60px;
    cursor: pointer;
}

.cell:nth-child(1), .cell:nth-child(2), .cell:nth-child(3) {
    border-top: none;
}

.cell:nth-child(1), .cell:nth-child(4), .cell:nth-child(7) {
    border-left: none;
}

.cell:nth-child(3), .cell:nth-child(6), .cell:nth-child(9) {
    border-right: none;
}

.cell:nth-child(7), .cell:nth-child(8), .cell:nth-child(9) {
    border-bottom: none;
}

.status {
    margin-top: 20px;
    font-size: 20px;
}

.restart {
    margin-top: 20px;
    padding: 10px 20px;
    font-size: 16px;
    cursor: pointer;
}

script.js: This is where the magic happens! We’ll write the JavaScript code to make the game functional. We’ll start with the basic structure and add more functionality step by step.

// Get the necessary elements from the DOM
const board = document.querySelector('.board');
const cells = document.querySelectorAll('.cell');
const status = document.querySelector('.status');
const restartButton = document.querySelector('.restart');

// Define game variables
let currentPlayer = 'X';
let gameActive = true;
let gameState = ['', '', '', '', '', '', '', '', '']; // Represents the game board

// Define 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]
];

// Function to handle a cell click
const handleCellClick = (clickedCellEvent) => {
    const clickedCell = clickedCellEvent.target;
    const clickedCellIndex = parseInt(clickedCell.getAttribute('data-cell-index'));

    // Check if the cell has already been played or if the game is over
    if (gameState[clickedCellIndex] !== '' || !gameActive) {
        return;
    }

    // Update the game state
    gameState[clickedCellIndex] = currentPlayer;
    clickedCell.textContent = currentPlayer;

    // Check for a win or a draw
    checkGameResult();

    // Switch to the next player
    switchPlayer();
}

// Function to switch players
const switchPlayer = () => {
    currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
    status.textContent = `Player ${currentPlayer}'s turn`;
}

// Function to check the game result
const checkGameResult = () => {
    let roundWon = false;
    for (let i = 0; i < winningCombinations.length; i++) {
        const winCondition = winningCombinations[i];
        const a = gameState[winCondition[0]];
        const b = gameState[winCondition[1]];
        const c = gameState[winCondition[2]];
        if (a === '' || b === '' || c === '') {
            continue;
        }
        if (a === b && b === c) {
            roundWon = true;
            break;
        }
    }

    if (roundWon) {
        status.textContent = `Player ${currentPlayer} has won!`;
        gameActive = false;
        return;
    }

    // Check for a draw
    let roundDraw = !gameState.includes('');
    if (roundDraw) {
        status.textContent = "Game ended in a draw!";
        gameActive = false;
        return;
    }
}

// Function to restart the game
const restartGame = () => {
    currentPlayer = 'X';
    gameActive = true;
    gameState = ['', '', '', '', '', '', '', '', ''];
    status.textContent = `Player ${currentPlayer}'s turn`;
    cells.forEach(cell => cell.textContent = '');
}

// Add event listeners
cells.forEach(cell => cell.addEventListener('click', handleCellClick));
restartButton.addEventListener('click', restartGame);

// Initialize the game status
status.textContent = `Player ${currentPlayer}'s turn`;

Understanding the Code: Step by Step

Let’s break down the JavaScript code and understand each part.

1. Getting Started: Selecting Elements

At the beginning of script.js, we select the HTML elements we will interact with using document.querySelector and document.querySelectorAll. These elements include the game board, individual cells, the status display, and the restart button.

const board = document.querySelector('.board');
const cells = document.querySelectorAll('.cell');
const status = document.querySelector('.status');
const restartButton = document.querySelector('.restart');

This code finds the elements in your HTML based on their class names. For example, document.querySelector('.board') finds the first element with the class “board”. document.querySelectorAll('.cell') finds all elements with the class “cell” and returns a NodeList (an array-like object) of these elements.

2. Initializing Game Variables

We declare and initialize several variables to manage the game state:

let currentPlayer = 'X';
let gameActive = true;
let gameState = ['', '', '', '', '', '', '', '', ''];
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]
];
  • currentPlayer: This variable holds the symbol of the current player (‘X’ or ‘O’).
  • gameActive: This boolean indicates whether the game is currently in progress.
  • gameState: This array represents the game board. Each element corresponds to a cell, and its value is either ‘X’, ‘O’, or an empty string (”) if the cell is empty.
  • winningCombinations: This array contains all possible winning combinations of cell indices.

3. Handling Cell Clicks

The handleCellClick function is the core of the game’s interaction. It’s triggered when a player clicks on a cell. Here’s a breakdown:

const handleCellClick = (clickedCellEvent) => {
    const clickedCell = clickedCellEvent.target;
    const clickedCellIndex = parseInt(clickedCell.getAttribute('data-cell-index'));

    if (gameState[clickedCellIndex] !== '' || !gameActive) {
        return;
    }

    gameState[clickedCellIndex] = currentPlayer;
    clickedCell.textContent = currentPlayer;

    checkGameResult();
    switchPlayer();
}
  1. Get the Clicked Cell: clickedCellEvent.target gives us the HTML element that was clicked.
  2. Get the Cell Index: clickedCell.getAttribute('data-cell-index') retrieves the index of the cell from its data-cell-index attribute. This attribute is set in the HTML.
  3. Check if the Move is Valid: Before proceeding, the function checks two things: If the cell is already occupied (gameState[clickedCellIndex] !== '') or if the game is over (!gameActive). If either condition is true, the function returns, preventing the move.
  4. Update the Game State: If the move is valid, the gameState array is updated with the currentPlayer‘s symbol at the clicked cell’s index.
  5. Update the Display: The content of the clicked cell (clickedCell.textContent) is set to the currentPlayer‘s symbol, making it visible on the board.
  6. Check for a Win or Draw: The checkGameResult() function is called to determine if the move resulted in a win or a draw.
  7. Switch Players: Finally, the switchPlayer() function is called to change the current player.

4. Switching Players

The switchPlayer function is simple:

const switchPlayer = () => {
    currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
    status.textContent = `Player ${currentPlayer}'s turn`;
}

It toggles the currentPlayer between ‘X’ and ‘O’ using a ternary operator and updates the status message to indicate whose turn it is.

5. Checking the Game Result

The checkGameResult function determines if the current move has resulted in a win or a draw. It does this by checking the gameState against the winningCombinations.

const checkGameResult = () => {
    let roundWon = false;
    for (let i = 0; i < winningCombinations.length; i++) {
        const winCondition = winningCombinations[i];
        const a = gameState[winCondition[0]];
        const b = gameState[winCondition[1]];
        const c = gameState[winCondition[2]];
        if (a === '' || b === '' || c === '') {
            continue;
        }
        if (a === b && b === c) {
            roundWon = true;
            break;
        }
    }

    if (roundWon) {
        status.textContent = `Player ${currentPlayer} has won!`;
        gameActive = false;
        return;
    }

    let roundDraw = !gameState.includes('');
    if (roundDraw) {
        status.textContent = "Game ended in a draw!";
        gameActive = false;
        return;
    }
}
  1. Check for Wins: The function iterates through the winningCombinations array. For each winning combination, it checks if the cells at those indices in the gameState array are occupied by the same player. If a winning condition is met, it updates the status message to declare the winner and sets gameActive to false to end the game.
  2. Check for Draws: If no winning condition is met, the function checks if all cells are filled (!gameState.includes('')). If so, it declares a draw and sets gameActive to false.

6. Restarting the Game

The restartGame function resets the game to its initial state.

const restartGame = () => {
    currentPlayer = 'X';
    gameActive = true;
    gameState = ['', '', '', '', '', '', '', '', ''];
    status.textContent = `Player ${currentPlayer}'s turn`;
    cells.forEach(cell => cell.textContent = '');
}

It resets currentPlayer to ‘X’, sets gameActive to true, clears the gameState array, resets the status message, and clears the text content of all the cells. This function is called when the restart button is clicked.

7. Adding Event Listeners

Event listeners are crucial for making the game interactive. We attach event listeners to the cells and the restart button.

cells.forEach(cell => cell.addEventListener('click', handleCellClick));
restartButton.addEventListener('click', restartGame);

The code uses forEach to loop through each cell in the cells NodeList and adds a ‘click’ event listener to each one. When a cell is clicked, the handleCellClick function is executed. We also add a ‘click’ event listener to the restartButton, which calls the restartGame function when clicked.

Common Mistakes and How to Fix Them

Here are some common mistakes beginners make when building a Tic-Tac-Toe game and how to avoid them:

  • Incorrect DOM Element Selection: Make sure you are selecting the correct HTML elements. Double-check your class names and IDs in your HTML and JavaScript code. Use the browser’s developer tools to inspect the elements and verify that they are being selected.
  • Logic Errors: Carefully review your game logic, especially the win condition checking. Make sure your code correctly identifies winning combinations and handles draws. Test your game thoroughly by playing multiple rounds.
  • Incorrect Event Handling: Ensure your event listeners are correctly attached to the cells and the restart button. Check that your event handling functions are called when they should be.
  • Scope Issues: Be mindful of variable scope. Variables declared inside functions are only accessible within those functions. If you need to access a variable from multiple functions, declare it outside those functions (global scope) or pass it as an argument.
  • Forgetting to Prevent Moves on Occupied Cells: Without this check, players could overwrite existing marks, breaking the game. Always validate each move.

Enhancements and Next Steps

Once you have a working Tic-Tac-Toe game, you can add further enhancements:

  • Add a Scoreboard: Keep track of the number of wins for each player.
  • Implement an AI Opponent: Make the computer play against the user.
  • Improve the UI: Add animations, custom styles, and a more visually appealing design.
  • Add Sound Effects: Play sounds when a player makes a move or when the game ends.
  • Make it Responsive: Ensure the game looks good on different screen sizes.

Key Takeaways

This tutorial has walked you through creating a simple, yet complete, Tic-Tac-Toe game using JavaScript. You’ve learned about the basic structure of a web game, how to interact with HTML elements, handle user input, and manage the game’s state. You’ve also learned about common pitfalls and how to avoid them. By building this game, you’ve gained practical experience with essential JavaScript concepts. Now you have a solid foundation to build more complex and interesting web applications. The skills you’ve acquired in this project are transferable to many other web development projects. Remember to practice and experiment to solidify your understanding. The more you code, the better you’ll become!

The journey of a thousand lines of code begins with a single click. Keep experimenting, keep learning, and keep building. Your first game is just the beginning; the possibilities for what you can create with JavaScript are endless.