Build a Multiplayer Game with Twilio Sync: Part 1

Developers working on a game
June 22, 2023
Written by
Carlos Mucuho
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

This blog post is the first part of a two-part tutorial where you will learn how to create a multiplayer Tic Tac Toe game with the Twilio Sync Javascript SDK.

Tic Tac Toe is a two-player game played on a 3x3 grid. The objective of the game is to be the first player to get three of their symbols (either X or O) in a row, either horizontally, vertically, or diagonally.

In the first part, you will learn how to create your own Tic Tac Toe game, including creating a game’s user interface, adding the controls, and implementing the game logic. Additionally, you will also learn how to add an AI player to make the game more challenging and engaging.

In the second part of the series, you will take your game to the next level by adding multiplayer functionality with the help of Twilio Sync Javascript SDK. So stay tuned for the second part, where you will learn how to leverage Twilio Sync to add real-time multiplayer functionality to the game!

Twilio Sync Javascript SDK is a software development kit that allows you to add real-time data synchronization functionality to your web and mobile applications.

By the end of this part, you will have a game that looks like the following:

Complete game demo

Tutorial Requirements

To follow this tutorial you will need the following:

  • A free Twilio account
  • A basic understanding of how to use Twilio and Javascript;
  • Node.js v12+, NPM, and Git installed;

Creating the Game UI

In this section, you will create the project directory where you will store your project files and create the game UI.

Open a terminal window and navigate to a suitable location for your project. Run the following commands to create the project directory and navigate into it:

mkdir tic-tac-toe
cd tic-tac-toe

Create a subdirectory named public where you will store this project’s static files and navigate into it:

mkdir public
cd public

Create a file named index.html inside the public subdirectory and add the following code to it:

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Tic Tac Toe</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="styles.css">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css"   rel="stylesheet" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous">
</head>
<body class="" style="background-color: #362746;">
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
  <script src="index.js"></script>
  <script src="game.js"></script>
</body>
</html>

The code uses the link tag to include Bootstrap CSS and styles.css files in the head section, and the script tag to include Bootstrap JS, index.js, and game.js files. Index.js will contain the game controls while game.js will contain the game logic.

Furthermore, the background color of the body section is set to dark purple.

Add the following code in the body section above the script tags:


 <body style="background-color: #362746;">
  <div class="row" style="background-color: #685187; height: 10vh;">
    <div class="col-2 pt-3">
      <h4 class="text-light text-end" id="txtPlayerOName">Player 1</h4>
    </div>
    <div class="col-1 pt-3 text-start">
      <span class="smallCircle " id="OSign"></span>
    </div>
    <div class="col-1 pt-3">
      <h4 class="text-light text-start" id="txtPlayerOScore">0</h4>
    </div>
    <div class="col-4 pt-2 text-center">
      <button class="btn text-light" type="button" id="btnStart" style="background-color:                                     #362746;"> Start
      </button>
    </div>
    <div class="col-1 pt-3">
      <h4 class="text-light text-end" id="txtPlayerXScore">0</h4>
    </div>
    <div class="col-1 pt-3 text-end">
      <span class="smallCross " id="XSign"></span>
    </div>
    <div class="col-2 pt-3">
      <h4 class="text-light text-start" id="txtPlayerXName">Player 2</h4>
    </div>
  </div>
  </div>

  …
</body>

The code creates a toolbar for the game with player names, scores, and symbols for X and O and a Start button.

When the game begins, the human player's name is set to "You," and the AI player's name is set to "Player 2."

Add the following code below the game’s toolbar–the code you just added above–and above the script tags:


<body style="background-color: #362746;">
  … 

  <div class="container mt-3 grid col-6 text-light " id="board">
    <div class="card" style="background-color: #685187;">
      <div class="card-body">
        <div class="row ">
          <div class="col-4">
            <div class="cell">
            </div>
          </div>
          <div class="col-4">
            <div class="cell">
            </div>
          </div>
          <div class="col-4">
            <div class="cell">
            </div>
          </div>
        </div>
        <div class="row mt-4">
          <div class="col-4">
            <div class="cell"></div>
          </div>
          <div class="col-4">
            <div class="cell"></div>
          </div>
          <div class="col-4">
            <div class="cell"></div>
          </div>
        </div>
        <div class="row mt-4">
          <div class="col-4">
            <div class="cell"></div>
          </div>
          <div class="col-4">
            <div class="cell"></div>
          </div>
          <div class="col-4">
            <div class="cell">
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>

  …
</body>

Here, the code creates a 3x3 grid to represent a Tic Tac Toe game board using div elements. The grid is organized into rows and columns, with each column containing a cell. The grid is enclosed within a card element that has a lighter purple background color.

Add the following code below the game board:


<body style="background-color: #362746;">
  …

   <div id="modalGameOver" class="modal" tabindex="-1">
    <div class="modal-dialog modal-dialog-centered">
      <div class="modal-content text-light text-center">
        <div class="modal-header">
          <h3 class="modal-title">Game over</h3>
          <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
        </div>
        <div class="modal-body">
          <h4 id="txtGameStatus">Game status</h4>
        </div>
        <div class="modal-footer ">
          <button type="button" class="btn text-light" data-bs-dismiss="modal"
            style="background-color: #362746;">Close</button>
        </div>
      </div>
    </div>
  </div>

  …
</body>

The code creates a modal dialog box to be displayed at the end of the game with the title "Game over", a body for displaying the game status, and a footer with a Close button.

Save your index.html file. Then, create a file named styles.css inside your public directory and add the following code to it:

#btnStart:hover {
  background-color: #EC407A !important;
  transition: 0.7s;
}

.grid {
  width: 20vw;
  margin: auto;
}


.cell {
  background-color: #362746;
  height: 10vw;
}

.cell:hover {
  background-color: #362746;
  height: 10vw;
}

.circle {
  margin-top: 8%;
  margin-left: 15%;
  height: 80%;
  width: 70%;
  border: 1vw solid #ffebef;
  box-shadow:
    inset 0 0 0.55em 0.5em #ee2a1e,
    0 0 0.55em 0.5em #ee2a1e;

  border-radius: 50%;
  display: inline-block;
}

.cross {
  position: relative;
  padding-top: 6%;
  padding-left: 15%;
  height: 100%;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.cross::before,
.cross::after {
  position: absolute;
  content: '';
  width: 70%;
  height: 14%;
  background-color: #ffebef;
  box-shadow: 0 0 0.55em 0.3em #2aaaf3;

}

.cross::before {
  transform: rotate(45deg);
}

.cross::after {
  transform: rotate(-45deg);
}


.smallCircle {
  margin-top: 0.1vw;
  height: 2vw;
  width: 2vw;
  border: 0.4vw solid #ffebef;
  box-shadow:
    inset 0 0 0.25vw 0.3vw #ee2a1e,
    0 0 0.3vw 0.4vw #ee2a1e;

  border-radius: 50%;
  display: inline-block;
}

.smallCross {
  position: relative;
  padding-bottom: 2%;
  padding-left: 50%;
  height: 70%;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.smallCross::before,
.smallCross::after {
  position: absolute;
  content: '';
  width: 2.5vw;
  height: 0.3vw;
  background-color: #ffebef;
  box-shadow: 0 0 0.15vw 0.26vw #2aaaf3;

}

.smallCross::before {
  transform: rotate(45deg);
}

.smallCross::after {
  transform: rotate(-45deg);
}

.blink {
  cursor: pointer;
  animation: blinker 1.5s linear infinite;
}

@keyframes blinker {
  50% {
    opacity: 0;
  }
}

.shrink-grow {
  cursor: pointer;
  animation: grow 1.5s linear infinite;
}


@keyframes grow {
  50% {
    transform: scale(0.8);
  }
}

.modal-content {
  background-color: #685187 !important;
}

This code defines the visual styles for the Tic Tac Toe game, which include the Start button appearance, board width, and margin, cell height, and background color, symbols used in the game, and animation effects when a cell is clicked or hovered over. Additionally, it sets the background color of the game's modal window.

Save styles.css and then open the index.html file in your preferred browser and your game UI should similar to the following:

Game UI

Creating the game controls

In this section, you will set a sign for both the human and AI player and implement the game controls. These controls will allow you to start a game and make a move on the board.

Create a file named index.js and add the following code to it:

const btnStart = document.getElementById('btnStart');
const cells = document.getElementsByClassName('cell');
const txtPlayerOName = document.getElementById('txtPlayerOName');
const txtPlayerXName = document.getElementById('txtPlayerXName');

The code sets up variables for the Start button, game cells, and player name elements.

The Start button initiates the game, the game cells make up the Tic Tac Toe grid, and the player name elements indicate which symbol the human player is using.

Add the following code variables to the bottom of your index.js file:

let player = 'O';
const AI = 'X';
let currentPlayer = player;
let addedListenersToCells = false;
let isGameOver = false;

Here the code initializes new variables to help manage the game:

  • player for the human player
  • AI for the AI player
  • currentPlayer to keep track of whose turn it is
  • addedListenersToCells to check if the cells have been assigned event listeners
  • isGameOver to determine if the game has ended.

Add the following code to the bottom of your index.js file:

function indicatePlayerSign() {
  if (player === 'O') {
    txtPlayerOName.textContent = 'You';
  } else if (player === 'X') {
    txtPlayerXName.textContent = 'You';
  }
}

indicatePlayerSign();

This code defines a function named indicatePlayerSign() that is used to set the player name elements on the game board and indicate which sign the human player is using.

It sets the text content of the corresponding player name element to You based on the player variable.

Finally, the code calls this function to set the player name at the beginning of the game.

Add the following code below the indicatePlayerSign() function call:

function addListenersToCells() {
  for (let i = 0; i < cells.length; i++) {
    cells[i].addEventListener('mouseover', () => {
      if (currentPlayer === player && !isGameOver && cells[i].children.length === 0) {
        cells[i].classList.add('shrink-grow');
      }
    });

    cells[i].addEventListener('mouseout', () => {
      if (currentPlayer === player && !isGameOver && cells[i].children.length === 0) {
        cells[i].classList.remove('shrink-grow');
      }
    });
  }
}

The addListenersToCells() function defined above adds event listeners to all cells in the game board to allow player interaction via clicking and hovering.

The function iterates through each cell using a for loop and attaches two event listeners to each cell: mouseover and mouseout.

These event listeners will only run if it’s the human player’s turn to play, the game is not over, and the cell is empty.

The mouseover event listener adds a pulsating effect to the cell, while the mouseout removes it.

Add the following highlighted code inside the addListenersToCells() function, below the mouseout event listener:


function addListenersToCells() {
  for (let i = 0; i < cells.length; i++) {
    …
    cells[i].addEventListener('click', () => {
      if (currentPlayer === player && !isGameOver && cells[i].children.length === 0) {
        cells[i].setAttribute('disabled', true);
        cells[i].classList.remove('shrink-grow');
        markCell(cells[i]);
      }
    });
  }
}

Here the code adds a click event listener to all cells on the board to detect when a human player clicks a cell.

It checks if it is the human player’s turn, if the game is not over yet, and if the cell is empty. If all conditions are met, it disables the cell, removes the pulsating effect, and calls the markCell() function with the cell element as an argument.

Add the following code below the addListenersToCells() function:

function markCell(cell) {
  cell.classList.add(currentPlayer);
  const sign = document.createElement('span');
  sign.className = currentPlayer === 'O' ? 'circle' : 'cross';
  cell.appendChild(sign);
}

The code defines a function named markCell that takes one argument, cell.

This function updates the game board by marking the cell that was passed as an argument with the current player's symbol and visually representing that symbol on the cell using a span element with a class name based on the player's symbol.

Next, add the following below all the existing code in index.js:

btnStart.addEventListener('click', () => {
  startGame();
});

function startGame() {
  isGameOver = false;

  if (!addedListenersToCells) {
    addListenersToCells();
    addedListenersToCells = true;
  }

  btnStart.disabled = true;
}

Here the code adds a click event listener to the Start button and defines a function named startGame() that will be called when this button is clicked.

The startGame() function initializes the game by setting isGameOver to false and calling the addListenersToCells() function to attach event listeners to the cells if they are not attached already. Additionally, it disables the Start button to prevent the player from starting a new game before finishing the current one.

Go back to the browser tab where you opened the index.html file, and refresh the page. Once the page refreshes click on the Start to start a new game and then interact with the cells on the board:

Game controls demo

Adding the game logic

In this section, you will start working on the game logic and integrating the AI player. The code you will implement here will handle various tasks such as storing the conditions for winning the game, keeping track of whose turn it is, switching between player turns, and allowing the AI player to make a move.

Create a file named game.js inside the public directory, and add the following code to it:

const xSign = document.getElementById('XSign');
const OSign = document.getElementById('OSign');
const txtPlayerOScore = document.getElementById('txtPlayerOScore');
const txtPlayerXScore = document.getElementById('txtPlayerXScore');
const modalGameOver = document.getElementById('modalGameOver');
const txtGameStatus = document.getElementById('txtGameStatus');

let playerOScore = 0;
let playerXScore = 0;

The code initializes several variables that will be used in the Tic Tac Toe game. These include xSign and OSign for displaying the X and O symbols, txtPlayerOScore and txtPlayerXScore for displaying the players' scores, modalGameOver for displaying the game over modal, and txtGameStatus for displaying the game status. Additionally, playerOScore and playerXScore are used for keeping track of the players' scores.

Add the following variables below the playerXScore variable at the bottom of the file:

const winningCombos = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6],
];

let gameWinningCombo = [];

The code above defines two variables related to the game's winning combinations:winningCombos and gameWinningCombo.

The winningCombos variable is an array of arrays representing the possible winning combinations.

The gameWinningCombo variable is an empty array that will store the winning combination when a player wins.

Add the following code below the gameWinningCombo variable:

function switchPlayer() {
  currentPlayer = currentPlayer === 'X' ? 'O' : 'X';
  indicatePlayerTurn();
}

function indicatePlayerTurn() {
  if (currentPlayer === 'O') {
    OSign.classList.add('blink');
    xSign.classList.remove('blink');
  } else {
    xSign.classList.add('blink');
    OSign.classList.remove('blink');
  }
}

The code above defines two functions : switchPlayer() and indicatePlayerTurn()

The switchPlayer() function swaps the current player with the other player after every move and calls indicatePlayerTurn() to show whose turn it is.

The indicatePlayerTurn() function adds a blink effect to the current player's symbol and removes it from the other player's symbol to visually indicate the player's turn.

Add the following code below the indicatePlayerTurn() function:

function AIPlayer() {
  const emptyCells = [...cells].filter((cell) => {
    return !cell.classList.contains('X') && !cell.classList.contains('O');
  });
  const randomIndex = Math.floor(Math.random() * emptyCells.length);
  const randomCell = emptyCells[randomIndex];
  randomCell.setAttribute('disabled', true);

  markCell(randomCell)
}

Here, the code defines a function named AIPlayer() that represents the behavior of the AI player in the game.

This function selects a random empty cell on the game board, disables it to prevent further selection, and marks it with the symbol of the AI player using the markCell() function.

Go back to the index.js file and add the following highlighted code to the bottom of the  markCell() function (located around line 46):


function markCell(cell) {
  …

  switchPlayer();
  if (currentPlayer === AI) {
    setTimeout(() => {
      AIPlayer();
    }, 1000);
  }
}

The code you added to the markCell() function, first, calls the switchPlayer() function to switch to the next player.

Then, it checks if the current player is the AI player. If so, the function sets a timeout to delay the AI player's turn by 1 second.

In the same file, add the following highlighted code to the bottom of the Start button click event listener (located around line 60):


btnStart.addEventListener('click', () => {
  startGame();
  indicatePlayerTurn();
});

Here, you added a code to the Start button click event listener that calls the indicatePlayerTurn() function to indicate whose turn it is to play right after the game starts.

Save all of your working files. Then, go back to the browser tab where you opened the index.html file, and refresh the page. Once the page refreshes click on the Start to start a new game, notice the player’s turn indicator is now functional, then click on any random cell and also notice how now there is an AI player as well.

Game logic with AI player demo

Building the end game logic

In this section, you will create several functions in the game.js that will work together to determine when the game is over, who the winner is, and how to reset the board for a new game.

Go back to your game.js file and add the following code to the bottom of this file:

function checkWin() {
  return winningCombos.some((combo) => {
    return combo.every((index) => {
      if (cells[index].classList.contains(currentPlayer)) {
        gameWinningCombo = combo;
        return true;
      }
    });
  });
}

This code defines a function named checkWin(), which checks whether any of the predefined winning combinations have been achieved by the current player.

If there is a winning combination the gameWinningCombo variable is assigned that winning combination and the function returns true. Otherwise, the function returns false.

Add the following code below the checkWin() function:

function checkTie() {
  return [...cells].every((cell) => {
    return cell.classList.contains('X') || cell.classList.contains('O');
  });
}

Here the code defines a function named checkTie(), which determines if the game has ended in a tie by checking if all the cells on the board have been marked with either X or O.

Add the following code below the checkTie() function:

function checkGameStatus() {
  if (checkWin()) {
    gameOver('win');
  } else if (checkTie()) {
    gameOver('tie');
  } else {
    switchPlayer();
    if (currentPlayer === AI) {
      setTimeout(() => {
        AIPlayer();
      }, 1000);
    }
  }
}

The code above defines a function named checkGameStatus(), which is responsible for checking the current game status and determining the appropriate action to take.

First, it checks if the current player has won the game by calling the checkWin() function. If checkWin() returns true, the game is over and a function named gameOver() is called with the argument win.

If the game is not won, it checks if the game is tied by calling the checkTie() function. If checkTie() returns true, the game is over and a function named gameOver() is called with the argument 'tie'.

If neither of the above conditions is met, the function switches to the next player by calling the switchPlayer() function. Additionally, if the current player is the AI player, the function sets a timeout to delay the AI player's turn by 1 second.

Add the following gameOver()  function below the checkGameStatus() function:

function gameOver(gameOutcome) {
  if (gameOutcome === 'win') {
    setTimeout(() => {
      currentPlayer === 'O' ? playerOScore += 1 : playerXScore += 1;
      txtPlayerOScore.innerText = playerOScore;
      txtPlayerXScore.innerText = playerXScore;
      showGameOverModal('win');
      gameWinningCombo.forEach((combo) => {
        cells[combo].classList.add('blink');
      });
      btnStart.disabled = false;
    }, 300);
  } else {
    showGameOverModal('tie');
    btnStart.disabled = false;
  }
  isGameOver = true;
}

The gameOver() function defined above is called when the game is over. It takes a parameter gameOutcome which is either win or tie meaning that the game ended in a win or tie.

if the game ended in a win, the function checks which player won the game and updates their score. Then, it displays the updated scores on the web page by setting the innerText properties of the txtPlayerOScore and txtPlayerXScore HTML elements.

Next, it calls a function named showGameOverModal(), created in the next step, and passes the gameOutcome as an argument, enables the Start button, and sets isGameOver to true.

Add the following code below the gameOver() function:

function showGameOverModal(gameOutcome) {
  if (gameOutcome === 'win') {
    const sign = currentPlayer === 'O' ? '<span class="text-danger"> O </span>'
      : '<span class="text-primary"> X </span>';
    txtGameStatus.innerHTML = `Player ${sign} won`;
  } else {
    txtGameStatus.innerText = "It's a tie";
  }
  
  const modal = new bootstrap.Modal(modalGameOver);
  setTimeout(() => {
    modal.show();
  }, 300);
}

The code above defines a function named showGameOverModal(), which is responsible for showing a modal dialog box when the game is over. This function takes in a parameter named gameOutcome with either a win or tie value.

If the game ends in a win, the function sets the text of the txtGameStatus element to indicate the winner, and if the game ends in a tie, it sets the text to show that it ended in a tie.

To display the modal, the function creates and calls an instance of the Modal class from the Bootstrap library after a brief delay.

Add the following code below the showGameOverModal() function:

function reset() {
  for (const cell of cells) {
    cell.classList.remove('X');
    cell.classList.remove('O');
    cell.classList.remove('blink');
    cell.replaceChildren([]);
    cell.setAttribute('disabled', false);
  }
  gameWinningCombo = [];
  switchPlayer();
}

The code above defines a function named reset() that prepares the game board for a new game.

This function first, ensures that all cells on the game board are cleared of any markings or other modifications and that they are clickable again.

Next, it clears the value stored on the gameWinningCombo variable and calls the switchPlayer() function to ensure that the player who made the final move in the previous game isn’t the one who makes the first move in the next game.

Save and close game.js.

Go back to the index.js file and replace all the code below the line cell.appendChild(sign) inside the markCell() function (around line 51) with the following highlighted line of code:


function markCell(cell) {
 …
  checkGameStatus();
}

Here you replaced the previous code with a checkGameStatus() function call. Now every time a player makes a move on the board this function will be called to check if the game has ended and if not switch to another player.

Go to the Start button click event listener and replace the indicatePlayerTurn() function call (around line 57) with the following highlighted code:


btnStart.addEventListener('click', () => {
  startGame();
  if (currentPlayer === AI) {
    setTimeout(() => {
      AIPlayer();
    }, 1000);
  }
});

When a game starts, the code added checks if it is the AI player’s turn to play, and if that is the case, the code sets a timeout to delay the AI player's turn by 1 second.

Go to the startGame() function and add the following highlighted code to the bottom of this function (located around line 64):


function startGame() {
  …

  reset();
}

With the code added, when the startGame() function is triggered the code calls the reset() function to prepare the board for a new game.

Save and close index.js.

Go back to the browser tab where you opened the index.html file, and refresh the page. Once the page refreshes click on the Start to start a new game, and notice that now when a player achieves a winning combination the game ends, the game over modal appears and the Start button is enabled.

Complete game demo

Conclusion

In this part of a two-part tutorial, you learned how to build a tic tac toe game from scratch using HTML, CSS, and Javascript. First, you created the game’s user interface and added controls for player interaction. Then, you implemented game logic to track player turns, enable the AI player to make moves, and check for game ending conditions.

The code for this part of the tutorial is available in the following repository https://github.com/CSFM93/twilio-multiplayer-game-part1.

Now it's time to begin to the second part of this project: adding multiplayer functionality with Twilio Sync.

Carlos Mucuho is a Mozambican geologist turned developer who enjoys using programming to bring ideas into reality. https://twitter.com/CarlosMucuho