Tic Tac Toe game development using Nodejs, socket.io and HandlebarsJS

Start code with fun. It will give you the ultimate results. In this blog post, we are trying to do something similar. We are going to build a very hit game Tic Tac Toe.

The game is between the Human and Machine. The code is using NodeJs with socket.io. For template integration, we are using HandlebarJs.

Programmed for fun and learning. I hope it will help you to learn something new.

So let’s dive into the coding ocean. 😄

The code is divided into three different parts.

  • Frontend grid box styling and template setup
  • NodeJs setup with socket.io module
  • Develop a winner and draw logic

HandlebarJs Template: This is the main template HTML code for HandlebarJs. We are using jQuery and socket.io so the main template before the body tag close. We will add “/socket.io/socket.io.js” and “https://code.jquery.com/jquery-3.4.1.min.js” script.
This template has a socket.io code. I will explain it in the next step.

app/views/layouts.hbs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <title>{{title}}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
  <link rel='stylesheet' href='/static/css/style.css'>
</head>
<body>
  {{{body}}}
<body>
</html>

app/view/index.hbs

The game board HTML code.

<div class="container">
    <h1 class="page-title"> Tic Tac Toe</h1>
    <div class="game-board">
        {{#each grid}}
            <div id="game-box-{{this}}" 
            class="game-box active" 
            data-index={{this}}></div>
        {{/each}}
    </div>

    <div class="game-indicator">
        <h3 class="humanPlayer">
           <span>{{humanPlayer}}</span> For Human Player
        </h3>
        <h3 class="machinePlayer">
            <span>{{machinePlayer}}</span> For Machine Player
        </h3>
        <h2 class="game-status"></h2>
        <button class="game-restart">Restart Game</button>
    </div>
</div>

app/config/config.js

Config file. It includes the player code symbol and winning patterns.

const Player = {
    HumanPlayer: 'X',
    MachinePlayer:  'O'
};

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

module.exports ={
    Player,
    winningCombinations
};

Following NodeJs modules are required.

a. socket.io
b. hbs
c. express
d. nodemon => it is just for development mode only.

Now the important code file. It is the main trigger point of the application.

a. includes all the required modules.

const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const hbs = require('hbs');
const config = require('./app/config/config');

b. game grid box array

We start it with 0 and store to a const variable.

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

c. Assign the current player

In our game logic. The current player is Human. So we get it player symbol from the config file.

const currentPlayer = config.Player.HumanPlayer;

d. Setup handlebar view.

For more detail. Check official documentation.

e. start the io connection.

Check the code below. It’s a self explainer. We created two major socket.io events in this file.
one is Human Player Turn and Check Winner. If you are a beginner, it might be a little complex for you. But don’t worry, give a little bit of time.

server.js

const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const hbs = require('hbs');
const config = require('./app/config/config');
const gameBoxGrid = [0,1,2,3,4,5,6,7,8];
const currentPlayer = config.Player.HumanPlayer;
// set the view engine to use handlebars
app.set('view engine', 'hbs');
app.set('views', __dirname + '/app/views');

//Defining middleware to serve static files
app.use('/static',express.static(__dirname +'/public'));
var blocks = {};

hbs.registerHelper('extend', function(name, context){
    var block = blocks[name];
    if(!block){
        block = blocks[name] = [];
    }

    block.push(context.fn(this)); // this is older version of handlebars

})

app.get('/', (req, res) => {
       
    //res.send('page loaded');
    res.render('index', { 
        layout: 'layouts', 
        title: 'Tic Tac Toe Page', 
        grid: gameBoxGrid,
        humanPlayer: config.Player.HumanPlayer,
        machinePlayer: config.Player.MachinePlayer,
        currentPlayer: currentPlayer
    });
});


// socket io  connection
io.on('connection', (socket) => {

    socket.on('humanPlayerTurn', (clickedBoxData) => {
        let activeBoxes = clickedBoxData.activeBoxes;
        let randomBoxId = Math.floor(Math.random() * activeBoxes.length);
        let status = ( activeBoxes.length > 0 
            &&  clickedBoxData.id != activeBoxes[randomBoxId] ) 
        ? 'active' : 'finish';
        let machinePlayerBox = clickedBoxData.id != activeBoxes[randomBoxId] 
        ? activeBoxes[randomBoxId] : '';
        //console.log(clickedBoxData, machinePlayerBox, status, randomBoxId);
       //console.log('grid-index: ' + msg);
      
        io.emit('machinePlayerTurn', { 
          id: machinePlayerBox,
          value: config.Player.MachinePlayer,
          gameStatus: status
        });

    });

    socket.on('checkWinner', (clickedBoxData) => {
        let result = false;
        //console.log(clickedBoxData.data)
        if(clickedBoxData.player == 'X'){
            let winConds = config.winningCombinations;
            let resultBoxes = [];
            for(i=0; i < winConds.length; i++){
                let winCond = winConds[i];
                //console.log(winCond);
                let a = clickedBoxData.data[winCond[0]];
                let b = clickedBoxData.data[winCond[1]];
                let c = clickedBoxData.data[winCond[2]];
               // console.log(a,b,c);
               
                if ( winCond[0] === clickedBoxData.data[winCond[0]] 
                    && winCond[1] === clickedBoxData.data[winCond[1]] 
                    && winCond[2] === clickedBoxData.data[winCond[2]] ) {
                    //console.log(a,b,c);
                    result = true;
                    resultBoxes = [a,b,c];
                    break
                } else {
                    continue;
                }
            }

            if(result){

                io.emit('resultDeclare', {
                    resultBoxes: resultBoxes,
                    player: 'X',
                    gameStatus: 'winner'
                });
            }     

        } else if( clickedBoxData.player == 'O') {
            let winConds = config.winningCombinations;
            let resultBoxes = [];
            for(i=0; i < winConds.length; i++){
                let winCond = winConds[i];
                //console.log(winCond);
                let a = clickedBoxData.data[winCond[0]];
                let b = clickedBoxData.data[winCond[1]];
                let c = clickedBoxData.data[winCond[2]];
               // console.log(a,b,c);
               
                if ( winCond[0] === clickedBoxData.data[winCond[0]] 
                    && winCond[1] === clickedBoxData.data[winCond[1]] 
                    && winCond[2] === clickedBoxData.data[winCond[2]] ) {
                    //console.log(a,b,c);
                    result = true;
                    resultBoxes = [a,b,c];
                    break
                } else {
                    continue;
                }
            }

            if(result){

                io.emit('resultDeclare', {
                    resultBoxes: resultBoxes,
                    player: 'O',
                    gameStatus: 'winner'
                });
            }  
        }
       
    });

});

//set port and listen request
const PORT = process.env.PORT || 8080;

http.listen(PORT, () => {
    console.log('current server PORT: '+ PORT);
});

f. In layouts.hbs file.

There are two socket.io events. One is for the Machine player and declares the result.

 $(function(){
    var socket = io();
    var currentActiveBox = [];
    
   // var activeBoxesArr = [];
      $('.game-restart').click(function(){
        /*$('.game-box').each(function(index, val){
          $(this).removeAttr('data-player');
          $(this).html('');
          $(this).addClass('active');
          $(this).removeClass('X-winner O-winner');
        });
        $('.game-status').html('');*/
        location.reload();
      })
     
      $('.game-box').click(function(e){
        if ($(this).hasClass('active')) {
          $('.game-box.active').each(function(index, val){
            currentActiveBox[index] = $(this).data('index');
          });

          let indexOfBox = currentActiveBox.indexOf($(this).data('index'));
          
          currentActiveBox.splice(indexOfBox,1);
          
          //console.log($(this).data('index'), currentActiveBox);
          socket.emit('humanPlayerTurn',{ 
            id:$(this).data('index'), 
            activeBoxes: currentActiveBox 
          } );

          $(this).html('{{currentPlayer}}');
          $(this).removeClass('active');
          $(this).attr('data-player', '{{currentPlayer}}');

          let currentPlayerBoxes = [];
          $('.game-box').each(function(index, val){
            
            if( $(this).data('player') == '{{currentPlayer}}' ){
              currentPlayerBoxes.push($(this).data('index'));
            } else {
              currentPlayerBoxes.push('');
            }
          });

          ///console.log(currentPlayerBoxes);

          socket.emit('checkWinner', {
            player: '{{currentPlayer}}',
            data: currentPlayerBoxes
          });

        }
        
      });

      socket.on('machinePlayerTurn', function(gridBoxData){
        
        if(gridBoxData.gameStatus == 'active' && gridBoxData.id >= 0){
          $('#game-box-'+gridBoxData.id).text(gridBoxData.value);
          $('#game-box-'+gridBoxData.id).removeClass('active');
          $('#game-box-'+gridBoxData.id).data('player', gridBoxData.value);
          currentActiveBox = [];

          $('.game-box.active').each(function(index, val){
            currentActiveBox[index] = $(this).data('index');
          });

          let machinePlayerBoxes = [];
          $('.game-box').each(function(index, val){
            
            if( $(this).data('player') == '{{machinePlayer}}' ){
              machinePlayerBoxes.push($(this).data('index'));
            } else {
              machinePlayerBoxes.push('');
            }
          });
          ///console.log(machinePlayerBoxes);

          socket.emit('checkWinner', {
            player: '{{machinePlayer}}',
            data: machinePlayerBoxes
          });

        } else {
          currentActiveBox = [];
          $('.game-status').html('Game Finished!!');
        }
       
        
        
      });

      socket.on('resultDeclare', function(resultData){
        console.log(resultData);
        let winMessage = "Player "+resultData.player+" Win the game!!";
        $('.game-status').html(winMessage);
        for(i =0; i < resultData.resultBoxes.length; i++){
          $('#game-box-'+resultData.resultBoxes[i]).addClass(resultData.player+'-winner');
        }

        $('.game-box').removeClass('active');
        currentActiveBox = [];
      });
  })

You can access the full source code from the GitHub repository.