DEV Community

Snevy1
Snevy1

Posted on

How to Create a Tic Tac Toe Game in Javascript.

A Tic Tac Toe game is a fun project to learn Frontend development and Javascript. In this tutorial, I will walk you through step by step on how to build a simple version of it.

Like most front-end development projects, your project should have an HTML file since this is the skeleton of a webpage. Then we add CSS that styles the HTML and finally we add interactivity by incorporating Javascript logic.

So let’s start by setting up HTML!

HTML

Set up your HTML file to include all the nine squares of the game.

Add classes and IDs since they will be used in CSS to style them and also in Javascript to add interactivity.

Don’t forget to link the CSS file in the title tag and the Javascript file just before the closing tag of the body element.

Below is my code for HTML:


<!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 Project</title>
   <link rel="stylesheet" href="index.css" />
    </head>
 <body>
   <h1>Tic Tac-Toe Project</h1>

   <section class="game-board-container scroll-container">
     <article class="game-board">
       <div class="sqrs" id="1">1</div>
       <div class="sqrs" id="2">2</div>
       <div class="sqrs" id="3">3</div>
       <div class="sqrs" id="4">4</div>
       <div class="sqrs" id="5">5</div>
       <div class="sqrs" id="6">6</div>
       <div class="sqrs" id="7">7</div>
       <div class="sqrs" id="8">8</div>
       <div class="sqrs" id="9">9</div>
     </article>

     <article class="form-sect">
       <form>
         <label for="player1">Player1 Name</label>
         <input type="text" id="player1" required />
         <label for="player2">Player2 Name</label>
         <input type="text" id="player2" required />
         <input type="submit" value="Continue" id="submit" />
       </form>
     </article>

     <article class="display-info"></article>
   </section>
   <script src="index.js"></script>

 </body>
</html>



Enter fullscreen mode Exit fullscreen mode

The code above has a form, that is not necessary but you may want to add it since it allows players to submit their names before the game begins and also it allows random selection of the first player to start.
Our project should look like this:

tic tac toe photo

CSS

Without CSS the project would look ugly.

It is time we make our project appealing.

Just as a reminder, to style elements in CSS we use CSS selectors.

To access class elements we start with .Classname, to select elements with ids we start with #idname.

You can also just select the element directly ie body.

Here is the CSS code for my HTML above:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}


html {
  overflow-x: hidden;
  max-width: 100%;
  padding-right: 0px !important;
  padding-left: 0px !important;
}

body {
  font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
    "Lucida Sans Unicode", Geneva, Verdana, sans-serif;
  font-weight: 500;
  overflow-x: hidden;
  width: 100%;
}

body h1 {
  text-align: center;
  margin-top: 2rem;
}

input,
label {
  display: block;
  font-size: larger;
  margin-top: 1rem;
}

.game-board-container {
  margin: 2rem auto;

  width: 400px;
  display: grid;
}
.form-sect {
  background: green;
  padding: 1rem;
  width: 50%;
  display: none;
  height: auto;
}

.game-board {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 2px;
  height: 50vh;

}

.sqrs {
  background: steelblue;
  display: grid;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  font-size: 30px;
}

.sqrs:hover {
  background: rgb(224, 220, 220);
}

.selected {
  pointer-events: none;
  background: rgb(224, 220, 220);
}

input {
  padding: 0.5rem;
  border: none;
  border-radius: 10px;
}

.display-info {
  margin: 5rem auto;
}

.display {
  width: 100%;
  height: 300px;
  padding: 2rem;
  font-size: 18px;
  font-family: Arial, Helvetica, sans-serif;
  display: grid;

  justify-content: center;
  align-items: center;
  background: green;
  color: rgb(8, 5, 5);
}

.restart {
  font-size: 50px;
  width: 200px;
  padding: 0.7rem;
  border-radius: 15px;
  border: transparent;
  background: white;
}
.display {
  width: 100%;
  height: 300px;
  padding: 2rem;
  font-size: 18px;
  font-family: Arial, Helvetica, sans-serif;
  display: grid;

  justify-content: center;
  align-items: center;
  background: green;
  color: rgb(8, 5, 5);
}

.restart {
  font-size: 50px;
  width: 200px;
  padding: 0.7rem;
  border-radius: 15px;
  border: transparent;
  background: white;
}


Enter fullscreen mode Exit fullscreen mode

To make our project responsive on both desktop and mobile phones, we add media queries.

Take note of how to use max-width and min-width in media queries.

Below is how I implemented this:


@media only screen and (max-width: 500px) {
  .game-board-container {
    position: relative;
    width: 100%;
  }

  .game-board {
    grid-template-columns: repeat(3, 1fr);
    gap: 2px;
    padding: 2rem;
    height: 30vh;
  }

  .form-sect {
    background: green;
    padding: 1rem;
    width: 100%;

    height: auto;
  }

  .form-sect form {
    padding: 2rem;
    background: red;
    width: 100%;
  }

  input {
    padding: 0.9rem;
    margin: 1rem;
    border: none;
    width: 100%;
    border-radius: 10px;
  }
}
@media only screen and (min-width: 500px) {
  .game-board-container {
    width: 100%;
    position: relative;
  }
  .game-board {
    margin-top: 8rem;
    grid-template-columns: repeat(3, 1fr);
    gap: 5px;
    height: 50vh;
    padding: 2rem;
  }

  .form-sect {
    margin: 5rem auto;
    background: green;
    padding: 1rem;
    width: 50%;

    height: auto;
  }

  .form-sect form {
    padding: 2rem;
    background: red;
    width: 100%;
  }

  input {
    padding: 0.9rem;
    margin: 1rem;
    border: none;
    width: 100%;
    border-radius: 10px;
  }
}

@media only screen and (min-width: 900px) {
  .game-board-container {
    width: 100%;
    position: relative;
  }
  .game-board {
    margin: 4rem auto;
    width: 50%;

    gap: 5px;
    padding: 2rem;
  }
}

@media only screen and (min-width: 2000px) {
  .game-board-container {
    width: 50%;
    position: relative;
  }

  .game-board {
    margin-top: 4rem;

    gap: 5px;
    padding: 4rem;
    height: 100vh;
  }

  .sqrs {
    font-size: 50px;
  }


Enter fullscreen mode Exit fullscreen mode

By this time the project looks this way:

Tic-tac-toe-css photo

Note that we have set .form-sect element to display none.We will set it to display grid after we finish the project so that the form shows up everytime we want to start a new game. Also, we have set .game-board to display grid, we will change it to display none after we finish the project.

JAVASCRIPT

This is the hardest part of the project.
Try to write javascript code that does the following and if you fail come back we walk through together

  1. On clicking a square, the inner text changes to either X or O
  2. A square cannot be clicked again after initial click
  3. Write a function that stores information about the current player and triggers relevant functions to display win, draw or a turn for another player(You can use factory functions)
  4. Write a function that changes turn from the current player to the next player.
  5. Track the number of squares selected(already clicked) and if no winner is found declare a draw
  6. Write a function that tracks the winner, it should have if…else statements to take care of all the possible outcomes i.e columns or rows aligning with the same sign(X or O)
  7. Display the winner or a draw accordingly
  8. Lastly, you can extend this functionality to create a more complex game.
  9. If feeling more ambitious you can create with an unbeatable AI using the min-max function.

Let’s start by fetching all the squares from the DOM so that we can add click event listener to them.

Just as reminder, to select more than two elements from the DOM we use querySelectorAll while to select only one we use querySelector.

let sqrs = document.querySelectorAll(".sqrs");

Next we select all the squares again but this time we select one by one.

We will use them in the if..else conditions to check if square one and square two and square three… were clicked by the same player and if they match we declare a win.

The code below illustrates this:

let sqr1 = document.getElementById("1");
let sqr2 = document.getElementById("2");
let sqr3 = document.getElementById("3");
let sqr4 = document.getElementById("4");
let sqr5 = document.getElementById("5");
let sqr6 = document.getElementById("6");
let sqr7 = document.getElementById("7");
let sqr8 = document.getElementById("8");
let sqr9 = document.getElementById("9");
Enter fullscreen mode Exit fullscreen mode

We then select other DOM elements and also declare new variables that we may need

let player1Name = "";
let player2Name = "";
let selectedSquares = [];
let formSect = document.querySelector(".form-sect");
let gameboard = document.querySelector(".game-board");
let formEl = document.querySelector("form");
let displayInfo = document.querySelector(".display-info");
Enter fullscreen mode Exit fullscreen mode

It is time we write a very important object (a factory function) that keeps track of everything that goes on in the game.

  • A factory function returns a new object.
  • This may be the hardest part but don’t worry,I will try to make you understand.
  • Let’s name this function Player(it is actually an object).This object is dynamic and stores data of the current and previous player i.e serves each player when their turn comes up. Hence it takes an argument of the name player. The value of player changes every time we change turn. i.e if the current player is player1 the next player will be player2 and invokes the same functions on the player2 as it did on player1.
  • The Player object will have a changeTurn() function that changes the value of the player argument every instance of the game.
  let playerNumber = player;

  //Change turn function changes the current player to another player after each move.

  changeTurn = function () {
    if (playerNumber == "player1") {
      playerNumber = "player2";
    } else if (playerNumber == "player2") {
      playerNumber = "player1";
    }
  };
Enter fullscreen mode Exit fullscreen mode

The other most important function in Player object is the gameData() function.

This function is the one that keeps track of which squares have been clicked, changes innerText of the squares, invokes the checkwin() every time a move is made etc.

This function is returned from the Player object and is available globally so that it can be invoked when the game start (After submission of names in the form).

Refer to the code below to understand. It comprises of all the code in the Player object


  const Player = (player) => {
  //Initialize type of player so we can use later to locate the current player and change turns.
  let playerNumber = player;

  //Change turn function changes the current player to another player after each move.

  changeTurn = function () {
    if (playerNumber == "player1") {
      playerNumber = "player2";
    } else if (playerNumber == "player2") {
      playerNumber = "player1";
    }
  };

  /*     ==============form el====================
   */

  //Main function and most important
  //Keeps track of everything that goes in on the game board and responds accordingly

  gameData = function () {
    //console.log(playerNumber);

    sqrs.forEach((sqr) => {
      sqr.addEventListener("click", () => {
        sqr.classList.add("selected");
        selectedSquares.push(sqr.id);
        //Changes sign on the squares when clicked, to  either X or O
        if (playerNumber == "player1") {
          sqr.innerHTML = "X";

          if (checkWin() === "X") {
            displayInfo.style.display = "block";
            gameboard.style.display = "none";
            displayInfo.innerHTML += `
            <div class="display"><h2> ${player1Name} has won</h2>
            <button class="restart">Restart</button>
            </div>

            `;

            //Restart button

            document.querySelector(".restart").addEventListener("click", () => {
              location.reload();
            });
          } else if (selectedSquares.length == 9 && checkWin() == false) {
            displayInfo.style.display = "block";
            gameboard.style.display = "none";
            displayInfo.innerHTML += `<div class="display">
            <h2> It is a draw!</h2>
            <button class="restart">Restart</button>
            </div>`;

            //Restart button

            document.querySelector(".restart").addEventListener("click", () => {
              location.reload();
            });
          }

          changeTurn();
        } else if (playerNumber == "player2") {
          sqr.innerHTML = "O";
          if (checkWin() === "O") {
            displayInfo.style.display = "block";
            gameboard.style.display = "none";
            displayInfo.innerHTML += `<div class="display">
            <h2> ${player2Name} has won</h2>
            <button class="restart">Restart</button>
            </div>`;

            //Restart button

            document.querySelector(".restart").addEventListener("click", () => {
              location.reload();
            });
          } else if (selectedSquares.length == 9 && checkWin() == false) {
            displayInfo.style.display = "block";
            gameboard.style.display = "none";
            displayInfo.innerHTML += `<div class="display"><h2> It is a draw!</h2>
             <button class="restart">Restart</button>
            </div>`;

            //Restart button

            document.querySelector(".restart").addEventListener("click", () => {
              location.reload();
            });
          }

          changeTurn();
        }
      });
    });
  };

  return { gameData };
};




Enter fullscreen mode Exit fullscreen mode

The next thing that we need to take care of is to check win.

We will create a global function and then invoke it where appropriate in the gameData().

const checkWin = () => {
  if (sqr1.innerHTML == sqr2.innerHTML && sqr2.innerHTML == sqr3.innerHTML) {
    return sqr1.innerHTML;
  }
  if (sqr1.innerHTML == sqr4.innerHTML && sqr4.innerHTML == sqr7.innerHTML) {
    return sqr1.innerHTML;
  }
  if (sqr1.innerHTML == sqr5.innerHTML && sqr5.innerHTML == sqr9.innerHTML) {
    return sqr1.innerHTML;
  }
  if (sqr2.innerHTML == sqr5.innerHTML && sqr5.innerHTML == sqr8.innerHTML) {
    return sqr2.innerHTML;
  }
  if (sqr3.innerHTML == sqr6.innerHTML && sqr6.innerHTML == sqr9.innerHTML) {
    return sqr3.innerHTML;
  }
  if (sqr3.innerHTML == sqr5.innerHTML && sqr5.innerHTML == sqr7.innerHTML) {
    return sqr3.innerHTML;
  }
  if (sqr4.innerHTML == sqr5.innerHTML && sqr5.innerHTML == sqr6.innerHTML) {
    return sqr4.innerHTML;
  }
  if (sqr7.innerHTML == sqr8.innerHTML && sqr8.innerHTML == sqr9.innerHTML) {
    return sqr7.innerHTML;
  }

  //If all of the above conditions are not met, then the game continues or a draw  is displayed

  return false;
};
Enter fullscreen mode Exit fullscreen mode

The following code is optional but it will be in synchrony with the other code above if you copy pasted or used most of my code, it is just a form and what happens after its submission.

/* ==========================Form info================= */

formEl.addEventListener("submit", function (event) {
  event.preventDefault();

  player1Name = document.getElementById("player1").value;
  player2Name = document.getElementById("player2").value;
  console.log(player1Name);

  //Random player to start

  let randomNumber = Math.round(Math.random() * 1);
  let selectedPlayer = "";
  randomNumber == 0
    ? (selectedPlayer = "player1")
    : (selectedPlayer = "player2");

  formSect.style.display = "none";
  displayInfo.style.display = "block";

  selectedPlayer === "player1"
    ? (displayInfo.innerHTML = `<div class="display"><h2>${player1Name} to start</h2></div>`)
    : (displayInfo.innerHTML = `<div class="display"><h2>${player2Name} to start</h2></div>`);

  setTimeout(function () {
    displayInfo.innerHTML = "";
    displayInfo.style.display = "none";
    gameboard.style.display = "grid";
  }, 1500);

  let player = Player(selectedPlayer);
  player.gameData();
});
Enter fullscreen mode Exit fullscreen mode

Congratulations!

I hope you have learnt a lot from this project.

If for some reasons you haven’t, don’t worry, it may take a while before some concepts sink in.

I wish you well in your web development journey.

Twitter
LinkedIn

Top comments (3)

Collapse
 
frankwisniewski profile image
Frank Wisniewski • Edited

When it comes to such small games, checking all possibilities is unproblematic. Essentially, you only need to check the current horizontal and vertical lines, as well as the two diagonals.

Consider the following example, which is not aiming to win any beauty prizes. It's all about the systematic approach.

<!DOCTYPE html>
<html lang="de">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0"> 
  <title>Document</title>
  <style>
    html{
      font-family: system-ui, sans-serif;
    }
    body{
      display:flex;
      flex-direction:column;
      align-items: center;
      width: 360px;
      margin:auto;
    }
    table{
      border-spacing:0px; 
      border-collapse: collapse;
    }
    td{
      box-sizing: border-box;
      display: inline-grid;
      place-items: center;
      width:100px;
      height:100px;
      background:blue;
      font-weight:bold;
      font-size: 4em;
      cursor:pointer;
      user-select: none;  
      margin:2px;
      }
    td:hover{
      background: silver;
    }
    button{
      margin: 1em 0;
      font-size:2em;
      padding: 0.25em 1em;
      font-weight:bold;
    }
    ::backdrop {
      opacity: 0.75;
      backdrop-filter: blur(3px);
    }
  </style>
</head>
<body>
  <h1>TicTacToe</h1>
  <table id=board></table>
  <button>new Game</button>
  <dialog id=dialog>
    <h2 id=winner></h2>
    <form method="dialog">
      <button>OK</button>
    </form>
  </dialog>
  <script>
    var player = "X"
    const clearBoard = () => board.innerHTML = '<tr><td><td><td>'.repeat(3)
    clearBoard()
    board.onclick = (el, cell=el.target) => {
      if (cell.tagName == 'TD' && cell.innerText == ''){
        cell.innerText = player
        cell.style.background = "silver"
        let vert = diaglr = diagrl = ''
        for (let i=0; i<3; i++){
          let row = board.rows[i].cells
          vert += row[cell.cellIndex].textContent
          diaglr += row[i].textContent
          diagrl += row[2-i].textContent
        }
        if ([cell.parentNode.textContent, vert, diaglr, diagrl].includes(player.repeat(3))){
          winner.textContent=`Winner player ${player}`
          dialog.showModal()
        } else {
          if(board.textContent.length==9){
            winner.textContent="The game ends in a tie"
            dialog.showModal()
          }
        }
        player = player === 'X' ? 'O' : 'X'
      } 
    }
    document.querySelectorAll('button').forEach( 
      el => el.addEventListener('click', () => 
      clearBoard()))
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ • Edited

Next we select all the squares again but this time we select one by one

Why would you do this? You already have them in sqrs[0] to sqrs[8]

Collapse
 
snevy1 profile image
Snevy1

Yeah sure. That was a better alternative. As I was writing, I wanted it to be as beginner friendly as possible but thanks, I will implement that