Creating Tic-Tac-Toe using Vanilla HTML/CSS/Javascript

Creating Tic-Tac-Toe using Vanilla HTML/CSS/Javascript

·

13 min read

Tic Tac Toe is a classic two-player game that uses a 3X3 board layout. The players choose between two symbols - either the X or the O. The end goal of the game is to ensure that one's symbols are arranged in a straight line - whether that's horizontally, diagonally or vertically.

I was mistaken to think that this project would be a walk-through. It is, however, an excellent challenge for anyone looking to sharpen their Vanilla Javascript skills.

Prerequisites

To create this game, the prerequisites are a good understanding of:

- HTML

- CSS

- Vanilla Javascript

I decided to blog about my approach to this challenge, in part because it took me a lot more time than I expected. The thing about the coding journey, as many have said before, is to keep building projects. This is something I have struggled with, especially in terms of doing consistently, and yet I am also well aware that this is the only way to get through to the 'other side'. Through disillusionment, discouragement and doubt. I also hope it can provide some insights for anyone who's just getting started with Vanilla Javascript.

In this post I will share what I did, and the bugs I ran into. I first wrote down pseudocode for this game. Here's how I went about it.

My approach

My HTML mark-up

In this section, I will simply share what I included in my HTML. I used the Emmet Abbreviation ! to get the HTML template that includes:

<!DOCTYPE html> is a declaration for the document type, which is written in HTML5

html lang="en"> is the start of the HTML document and specifies the use of English as the language

<head> this is the opening tag for the head section. The head section contains meta-information about the document eg links to the CSS file, title and viewport settings. Meta-information simply refers to data about the document and is NOT displayed on the browser, but ensures the page is rendered correctly.

<meta charset='UTF-8'> refers to the encoding used, in this case UTF-8.

<meta name = "viewport" content="width=device-width, initial-scale=1.0"> is a key section for responsive designs. It simply sets the viewport width to that of the device in use, and the initial scale to 1.0.

<link rel="stylesheet" href="style.css"> allows me to link to an external resource, in this case my CSS file, and the 'rel' is for the relationship that the linked file is a stylesheet

<title>Tic-Tac-Toe in Vanilla JS</title> is the title of my Document and is usually displayed on the title bar.

<body> ... </body> represents the tags of the body tag. These tags house the content of the web page. Just before the closing body tag, I linked my script file.

The HTML markup:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link rel="stylesheet" href="style.css">
  <title>Tic-Tac-Toe in Vanilla JS</title>
</head>

<body>
  <main>
    <h2>Tic Tac Toe</h2>
    <h3 class="display-player">Click on any square to start the game</h3>
    <section class="game-container">

      <div class="game-board">
        <div class="0 game-square"></div>
        <div class="1 game-square"></div>
        <div class="2 game-square"></div>
        <div class="3 game-square"></div>
        <div class="4 game-square"></div>
        <div class="5 game-square"></div>
        <div class="6 game-square"></div>
        <div class="7 game-square"></div>
        <div class="8 game-square"></div>
      </div>

      <button class="reset-button">Reset Game</button>
    </section>
  </main>

  <script src="scripts.js"></script>
</body>

</html>

I added a simple heading for the game, and an h3 that would initially display a message: 'Click on any square to start the game'.

I also added a div, game-container that would house the game board. Within the game board, I created 9 divs all with the class game-square. In the CSS styling section below, I would arrange these divs to create a 3x3 grid.

To be able to keep track of the changes clicked on the page, I also included a class for each of the divs, numbered 0 to 8. This was an important part of my project because it provided me with an easy way to keep track of the state of the game as it'd match the hypothetical board mentioned in the next section.

Think of it this way: If a player clicks on the third div, it would give me the class 2, and on my hypothetical board, this would be the same placement.

CSS Styling

body {
    display: flex;
    justify-content: center;
}

.game-container {
    width: 20rem;
}

.game-board {
    height: 100%;
    display: flex;
    flex-wrap: wrap;
}

.game-square {
    border: 2px solid white;
    flex: 1 1 5rem;
    height: 5rem;
    display: flex;
    justify-content: center;
    align-items: center;
    font-size: xx-large;
    background-color: rgb(137, 137, 185);
}

.game-square:hover {
    background-color: aliceblue;
    cursor: pointer;
}

.game-square.clicked {
    opacity: 0.5;
}

.reset-button {
    padding: 0.5rem;
    border-radius: 5px;
    background-color: rgb(9, 117, 9);
    border: 2px solid white;
    color: white;
    cursor: pointer;
}

.reset-button:hover {
    border: 2px solid #4caf50;
    background-color: white;
    color: rgb(3, 54, 3);
}

As for my styling, I wanted everything in the body to be at the center, so I added flex to it and then gave the game-container a width of 20rem so that it wouldn't span in different devices. I set the game-board containing the 9 divs as a flex item, with the option of wrapping the divs.

For each of the divs, I used the shorthand: flex: 1 1 5rem. This is shorthand for flex-grow flex-shrink flex-basis which simply means that each div can grow to take available space and shrink if there isn't enough space, but the initial size set for each div is 5rem.

Each of the divs is also a flex item and will house the players' symbols, X or O - for which o have set the font to extra large.

I also added pseudo classes to the divs and the reset button. CSS pseudo-classes are used when we need to distinguish a separate state of the specific element, in this case, when we hover over it.

Javascript Logic - Creating a hypothetical game board

After the HTML writeup and CSS styling, I started working on the script file. The idea was to have a hypothetical board that informs what all the possible winning combinations look like. This will be a 2D array - an array with nested arrays.

//my hypothetical board to rep the 3X3 grid
 [0, 1, 2],
 [3, 4, 5],
 [6, 7, 8],

Creating variables

I then created the variables I would need, including variables for:

- the gameboard to attach an eventListener to

- to display the winner message/ whose turn it is to play

- the 8 possible winning combinations for a 3X3 tic tac toe game

- a counter (initialized to 0), playerTurn ( a boolean where if true, it is player X turn to play) and two arrays to reflect the position of the players.

const gameBoard = document.querySelector('.game-board');

const displayPlayerTurn = document.querySelector('.display-player');

const listWinningCombi = [
    //rows
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    //cols
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    //diagonals
    [0, 4, 8],
    [2, 4, 6],
];

let playerCount = 0,
    playerTurn = true,
    playerXArr = [],
    playerYArr = [];

Add an Event Listener to the game-board

I also need to add an event listener onto the game board so that when clicked, I can keep track of the game's state. The addSymbol function is an event handler that's triggered when I click on the gameboard. The event listener passes on the event object ev automatically to the function. Remember, the ev parameter represents the object associated with the click. This is how I can access the target element using ev.target.

With this in mind, first I check if the clicked box is empty, and if it is, check whose turn it is. If playerTurn=== true player turn is strictly equal to true, then set X to the game square so that effectively, it is player X. I then pushed its position into the specific player's array, increased the counter, checked whose turn it was next and finally checked the game score. I do this for second player as well, and make sure to change the boolean check on playerTurn to false so that it is player O's turn.

//function to add player's symbol on click
const addSymbol = (ev) => {
    let gameSquare = ev.target;
    //if the grid box is empty, AND playerTurn is true, it is X turn to play. so display X
    if (gameSquare.textContent === '' && playerTurn === true) {
        gameSquare.textContent = 'X';
        let playerPosition = +gameSquare.classList[0];

        playerXArr.push(playerPosition);
        incrCounter();
        checkPlayerTurn();
        checkScore();
    }

    if (gameSquare.textContent === '' && playerTurn === false) {
        gameSquare.textContent = 'O';
        let playerPosition = +gameSquare.classList[0];

        playerOArr.push(playerPosition);
        incrCounter();
        checkPlayerTurn();
        checkScore();
    }
};

gameBoard.addEventListener('click', addSymbol);

NOTE: In my code, the classes provided for each of the squares, 0 through 8 are exactly similar to the positions I am checking for on the hypothetical game board. This way, when a game square is clicked, its first class will be equivalent to the position I need to track on the grid box.

To do this, I extract the first class name from the classList property of the specific game square I have clicked, this is index[0], and assign it to the variable playerPosition to reflect the state of the game at that time, and to corelate with the hypothetical game board. Since the class is a string, I use + ,the unary plus operator just before the operand to convert it to a number, which I then push into the array.

Check the game score - win or draw?

The next step is to check for the game score. A game draw/tie occurs when the game ends with no winner. That is, none of the players had their symbols in a row. Logic-wise, this is the easiest place to start because it means that all of the game squares are filled, the counter is 9 and there is no winning combination.

To check for the scores: for a win, I need to loop through the array containing the possible combinations and then check, whether, for each nested array, the player's array contains every index in the winning combination array. If so, that particular player has won.

//function to check the score
const checkScore = () => {
    //To check for a win loop thru all of the possible combinations
    for (let i = 0; i < listWinningCombi.length; i++) {
    //this represents each subarray for a win that could be horizontal, vertical, diagonal
        let singleWinCombi = listWinningCombi[i];
        //Player X wins
        //a return KW in my .every() function was cracking my code. it is either explicit or implied. i hadn't included either in my earlier code
        if (singleWinCombi.every((winIndex) => playerXArr.includes(winIndex))) {
            displayPlayerTurn.textContent = 'Game Over - Player X wins the game!';
            gameBoard.removeEventListener('click', addSymbol);
            return; //remember to add the return to ensure that the funtion exits and does not check for a draw
        }
        //Player O wins
        if (singleWinCombi.every((winIndex) => playerOArr.includes(winIndex))) {
            displayPlayerTurn.textContent = 'Game Over - Player O wins the game!';
            gameBoard.removeEventListener('click', addSymbol);
            return;
        }
    }

    //game draw, must be outside the for-loop so that ifno player wins and all the game-squares are filled, then it is a draw
    if (playerCount === 9) {
        displayPlayerTurn.textContent =
            'Game Over - XOXO!! The game ends in a draw';
    }
};

One of the things that was confusing me earlier on was how to use the .every() method. What would I call this method - would it be the single winning combination or the dynamic player array?

The essence of the .every() method is to check if, the elements of the array on which we call the method, are ALL present in the player's array. Since the player's array is dynamic and will contain other positions on the hypothetical board, we check that it includes each element from the winning combination using the .includes() method. If these methods return true, then display the winner message and remove the event listener from the gameboard.

Interestingly, on the day I was trying to get the logic for checking the game score, it hit me that afternoon that a draw occurs when the counter gets to 9 because that would mean that no player has a straight streak of the rows, columns or diagonals. I know, I knowwwww...

Restart the game

The last thing that I did for the most basic version of this Tic-Tac-Toe game was to activate the 'Restart Game' button. When a user clicks this button, it invokes the resetGame function. This resets the game variables and restarts the game as shown in the code below.

const resetGame = () => {
    //get all the game squares and set text to empty string
    const gameSquares = document.querySelectorAll('.game-square');
    gameSquares.forEach((gameSquare) => {
        gameSquare.textContent = '';
    });

    //reset the player arrays to empty arrays
    playerXArr = [];
    playerOArr = [];

    //reset the counter and player turn and display message
    playerCount = 0;
    playerTurn = true;
    displayPlayerTurn.textContent = 'Click on any square to start the game';

    //reattach the EListener to the gameboard
    gameBoard.addEventListener('click', addSymbol);
};

resetGameBtn.addEventListener('click', resetGame);

In the code above, we're simply returning it to the initial settings by:

  • getting all the game squares using .querySelectorAll() to get an iterable Nodelist

  • Loop through each of them and set the text to an empty string

  • Reset the player arrays

  • Reset the counter, playerTurn and display the message

  • Reattach the event listener

Here's the full Javascript Code

const gameBoard = document.querySelector('.game-board');
const displayPlayerTurn = document.querySelector('.display-player');
const resetGameBtn = document.querySelector('.reset-button');

/*
my hypothetical board to rep the 3X3 grid
  [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
*/

const listWinningCombi = [
    //rows
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    //cols
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    //diagonals
    [0, 4, 8],
    [2, 4, 6],
];

let playerCount = 0,
    playerTurn = true,
    playerXArr = [],
    playerOArr = [];

const incrCounter = () => {
    playerCount++;
};

const checkPlayerTurn = () => {
    if (playerTurn === true) {
        playerTurn = false;
    } else {
        playerTurn = true;
    }
};

const checkScore = () => {
    //Game ends in a win
    for (let i = 0; i < listWinningCombi.length; i++) {
        let singleWinCombi = listWinningCombi[i];

        if (singleWinCombi.every((winIndex) => playerXArr.includes(winIndex))) {
            displayPlayerTurn.textContent = 'Game Over - Player X wins the game!';
            gameBoard.removeEventListener('click', addSymbol);
            return;
        }

        if (singleWinCombi.every((winIndex) => playerOArr.includes(winIndex))) {
            displayPlayerTurn.textContent = 'Game Over - Player O wins the game!';
            gameBoard.removeEventListener('click', addSymbol);
            return;
        }
    }

    //game draw
    if (playerCount === 9) {
        displayPlayerTurn.textContent =
            'Game Over - XOXO!! The game ends in a draw';
    }
};

const addSymbol = (ev) => {
    let gameSquare = ev.target,
        playerPosition;

    if (gameSquare.textContent === '' && playerTurn === true) {
        gameSquare.textContent = 'X';
        displayPlayerTurn.textContent = "It is now Player O's turn";

        playerPosition = +gameSquare.classList[0];
        playerXArr.push(playerPosition);
        console.log(playerXArr);

        incrCounter();
        checkPlayerTurn();
        checkScore();
    } else if (gameSquare.textContent === '' && playerTurn === false) {
        gameSquare.textContent = 'O';
        displayPlayerTurn.textContent = "It is now Player X's turn";

        playerPosition = +gameSquare.classList[0];
        playerOArr.push(playerPosition);

        incrCounter();
        checkPlayerTurn();
        checkScore();
    } else if (gameSquare.textContent === 'X' || gameSquare.textContent === 'O') {
        return;
    }
};

const resetGame = () => {
    console.log('reset');

    const gameSquares = document.querySelectorAll('.game-square');
    gameSquares.forEach((gameSquare) => {
        gameSquare.textContent = '';
    });

    playerXArr = [];
    playerOArr = [];
    playerCount = 0;
    playerTurn = true;
    displayPlayerTurn.textContent = 'Click on any square to start the game';

    gameBoard.addEventListener('click', addSymbol);
};

gameBoard.addEventListener('click', addSymbol);
resetGameBtn.addEventListener('click', resetGame);

If you made it this far in this post, thank you for reading!

Here's the Github repo containing the code.

I would love to hear your feedback too! Let's connect on Github and Twitter.