Multiplayer Chess application using NodeJs and Socket.io

This blog post I posted after two weeks since I posted my previous blog post. This time we will develop a big hit and world-famous indoor game that is “CHESS”

In this programming example, We are going to develop a multiplayer chess application. It is helpful for a newbie who is learning NodeJs and Socket.io.

Prerequisites

  1. Basic knowledge about NodeJs and ES2015/ES2016
  2. HandleBarJs template engine
  3. Basic knowledge of socket.io
  4. Understand Chess Rules

Code Parts

  1. Templates
  2. Socket.io server end controller
  3. Socket.io client end functions.

Required NodeJs Modules

  1. socket.io
  2. hbs
  3. express
  4. nodemon => it is just for development mode only.

Directory Structure

Index File

root/index.js

const app = require('./app/aap');
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const socketSever = require('./app/controllers/socketServer');
socketSever(io);

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

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

Application initiliaztion

root/app/index.js

const express = require('express');
const app = express();
const path = require('path');
const hbs = require('hbs');
// set the view engine to use handlebars
app.set('view engine', 'hbs');
app.set('views', path.join(__dirname, 'views'));
app.use('/', express.static(path.join(__dirname, '../public')));

app.get('/', (req, res) => {
    //res.send('Chess Application')
    res.render('index', {
        layout: 'layout',
        title: 'Chess Application',
        page_title: 'Chees Board'

    })
});

module.exports = app;

Template Layouts

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" type="text/css" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"/>
  <link rel="stylesheet" href="/lib/chessboardjs/css/chessboard-1.0.0.min.css"/>
  <link rel="stylesheet" type="text/css" href="/css/style.css"/>
</head>
<body>
  <div class="container">
    {{{body}}}
  </div>
  <script src="/socket.io/socket.io.js"></script>
  <script>
    const socket = io();
  </script>
  <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.2/chess.js"></script>
  <script src="/lib/chessboardjs/js/chessboard-1.0.0.min.js"></script>
  <script src="/js/chess.js"></script>
  <script src="/js/socketClient.js"></script>
  </body>
  </html>

app/view/index.hbs

<div class="row">
    <div class="col-md-9">
        <h1>{{page_title}}</h1>
        <form class="form-inline" id="userNameForm">
            <div class="form-group mx-sm-3 mb-2">
                <label for="userNameInput" class="mr-2">Your Name</label>
                <input type="text" name="userNameInput" id="userNameInput" class="form-control"/>
            </div>
            <button type="submit" class="btn btn-success mb-2">Add Your Name</button>
        </form>
        <h3 id="userName"></h3>
        <div class="notification"></div>
        
        <div id="chessBoard" style="width:450px;padding-top:50px; margin:auto;"></div>
    </div>
    <div class="col-md-3">
        <h2>Online Players</h2>
        <ul class="list-group" id="onlinePlayers">
            <li class="list-group-item">
                <button type="button" class="btn btn-primary btn-sm">Play Against CPU</button>
            </li>
        </ul>
    </div>
</div>

Socket.io server Controller

app/controllers/socketServer.js

const users = [];
function randomRoomId(){
    let roomId = '';
    let length = 12;
    let randomChar = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    for(counter = 0; counter < length; counter++){
        roomId += randomChar.charAt(Math.floor(Math.random() * randomChar.length));
    }

    return roomId;

}
exports = module.exports = function(io){

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

            let userName = formData.name;
            let room = randomRoomId();

            users.push({
                id: socket.id,
                name: userName,
                room: room
            });

           // console.log(users);

            socket.join(room);
            socket.broadcast.emit("roomDetail", {
                users: users,
            });

        });

        socket.emit('existingUsers', {
            users:users,
            currentUserId: socket.id
        });

        socket.on('sendJoinRequest', (requestData) => {
            //console.log(requestData.room);
            let user = users.filter(user=>user.id == socket.id)[0];
            socket.broadcast.to(requestData.room).emit('joinRequestRecieved', {
                id: user.id,
                name: user.name,
                room: user.room
            });
        });

        socket.on('acceptGameRequest', (requestData) => {
            let user = users.filter(user=>user.id == socket.id)[0];
           
            socket.broadcast.to(requestData.room).emit('gameRequestAccepted', {
                id: user.id,
                name: user.name,
                room: user.room,
                
            });
            
        });

        socket.on('setOrientation', (requestData) => {
            let user = users.filter(user=>user.id == socket.id)[0];
            socket.broadcast.to(requestData.room).emit('setOrientationOppnt', {
                color: requestData.color,
                id: user.id,
                name: user.name,
                room: user.room,
            });
        });

        socket.on('chessMove', (requestData) => {
            console.log(requestData);
            socket.broadcast.to(requestData.room).emit('oppntChessMove',{
                color: requestData.color,
                from: requestData.from,
                to: requestData.to,
                piece: requestData.piece,
                promo: requestData.promo||''

            });
        });

        socket.on('gameWon', (requestData) => {
            socket.broadcast.to(requestData.room).emit('oppntWon');
        });
        
        socket.on('disconnect', () => {
            for(i = 0; i< users.length; i++){
                if(users[i].id == socket.id){
                    users.splice(i,1);
                    break;
                }
            }
        });
    });

}

Socket.io client-end functionality

public/js/socketClient.js

$(function(){
    $(document).on('submit', '#userNameForm', function(event){
        event.preventDefault();
        socket.emit('submitName', {
            name: $('#userNameInput').val(),
        });
        $('#userName').text('Hi '+$('#userNameInput').val());
        $('#userNameForm').hide();
        $('#userNameInput').val('');
    });
    socket.on('roomDetail', (roomData) => {
       // $('#onlinePlayers').html('');
        roomData.users.forEach(user => {
            $('#onlinePlayers')
            .append($('<li class="list-group-item" id="'+user.id+'">')
            .html('<button type="button" data-room="'+user.room+'" class="btn btn-primary btn-sm joinGameRequest">'+user.name+'</button>'));
        });
    });

    socket.on('existingUsers', (userData) => {
       // $('#onlinePlayers').html('');
        userData.users.forEach(user => {
            if(userData.currentUserId != user.id){

                $('#onlinePlayers')
                .append($('<li class="list-group-item" id="'+user.id+'">')
                .html('<button type="button" data-room="'+user.room+'" class="btn btn-primary btn-sm joinGameRequest">'+user.name+'</button>'));
            }
        });
    });

    socket.on('joinRequestRecieved', (userData) => {
        //console.log(userData);
        $('.notification')
        .html('<div class="alert alert-success">Recieved a game request from <strong>'+userData.name+'</strong>. <button data-room="'+userData.room+'" class="btn btn-primary btn-sm acceptGameRequest">Accept</button></div>')
    });

    $(document).on('click', '.joinGameRequest', function(){
        socket.emit('sendJoinRequest', {
            room: $(this).data('room')
        });
        $('.notification').html('<div class="alert alert-success">Game request sent.</div>');
    });

    $(document).on('click', '.acceptGameRequest', function(){

        socket.emit('acceptGameRequest', {
            room: $(this).data('room')
        });
        $('.notification')
        .html('<div class="alert alert-success">Please wait for game initialize from host.</div>');
    });

    socket.on('gameRequestAccepted', (userData) => {
        //console.log(userData);
        $('.notification')
        .html('<div class="alert alert-success">Game request accepted from <strong>'+userData.name+'</strong>.</div>');
        $('.notification')
        .append($('<div class="text-center">'))
        .append('Choose rotation. <button data-room="'+userData.room+'" data-color="black" type="button" class="btn btn-primary btn-sm setOrientation">Black</button> or <button data-room="'+userData.room+'" data-color="white" type="button" class="btn btn-primary btn-sm setOrientation">White</button>');

        $('#onlinePlayers li#'+userData.id).addClass('active');
    });

    socket.on('opponentDisconnect',function(){
        $('.notification')
        .html('<div class="alert alert-success">Opponent left the room</div>');
        board.reset();
        chess.reset();
    });
}(jQuery));

public/js/chess.js

var board = null;
var chess = new Chess();

const boardConfig = {
    draggable: true,
    dropOffBoard: 'trash',
    onDragStart: onDragStart,
    onDrop: onDrop,
}
var isMachinePlayer = false;

board = Chessboard('chessBoard', boardConfig);

function onDragStart (source, piece, position, orientation) {
    // 
   // console.log(chess.turn());
    if(chess.in_checkmate()){
        let confirm = window.confirm("You Lost! Reset the game?");
        let room = $('#onlinePlayers li.active button').data('room');
        if(confirm){
            if(isMachinePlayer){
                chess.reset();
                board.start();
            } else {

                //socket.requestNewGame();
                socket.emit('gameWon', { 
                    room: room,
                });
            }
        }
    }
    // do not pick up pieces if the game is over
    // or if it's not that side's turn
    if ( chess.game_over() || 
        (chess.turn() === 'w' && piece.search(/^b/) !== -1) ||
        (chess.turn() === 'b' && piece.search(/^w/) !== -1)) {
      return false
    }
}

function onDrop(source, target, piece, newPos, oldPos, orientation){
    
    // see if the move is legal
    let turn = chess.turn();
    let room = $('#onlinePlayers li.active button').data('room');
    let move = chess.move({
        color: turn,
        from: source,
        to: target,
        
        //promotion: document.getElementById("promote").value
    });

    // illegal move
    if (move === null) return 'snapback';
    updateStatus();
    //player just end turn, CPU starts searching after a second
    if(isMachinePlayer){
        //window.setTimeout(chessEngine.prepareAiMove(),500);
    }
    else { 
        socket.emit('chessMove', { 
            room: room,
            color: turn, 
            from: move.from, 
            to: move.to,
            piece: move.piece
        });
    }

}

function updateStatus(){
    let status = "";
    let moveColor = "White";
    if(chess.turn()=='b'){   
        moveColor = "Black";
    }
    if(chess.in_checkmate()==true){
        status=  "You won, " + moveColor + " is in checkmate";
        window.alert(status);
        if(isMachinePlayer){
            chess.reset();
            board.start();
        }
        return; 
    } else if(chess.in_draw()){
        status = "Game Over, Drawn";
        window.alert(status);
        return;
    }
}

$(function(){
    
    $(document).on('click', '.setOrientation', function(){
        
        socket.emit('setOrientation', {
            room: $(this).data('room'),
            color: ($(this).data('color') === 'black') ? 'white': 'black'
        });
        
        board.orientation( $(this).data('color') );
        board.start();
        if($(this).data('color') == 'black'){
            $('.notification')
            .html('<div class="alert alert-success">Great ! Let\'s start game. You choose Black. Wait for White Move.</div>');
        }else{
            $('.notification')
            .html('<div class="alert alert-success">Great ! Let\'s start game. You choose White. Start with First Move.</div>');
        }
    });

    socket.on('setOrientationOppnt', (requestData) => {
        //console.log(requestData);
        board.orientation(requestData.color);
        board.start();
        $('#onlinePlayers li#'+requestData.id).addClass('active');
        if(requestData.color == 'white'){  
            $('.notification')
        .html('<div class="alert alert-success">Game is initialized by <strong>'+requestData.name+'</strong>. Let\'s start with First Move.</div>');
        } else{
            $('.notification')
        .html('<div class="alert alert-success">Game is initialized by <strong>'+requestData.name+'</strong>. Wait for White Move.</div>');
        }
        
    });

    socket.on('oppntChessMove', (requestData) => {
        console.log(requestData);
        let color = requestData.color;
        let source = requestData.from;
        let target = requestData.to;
        let promo = requestData.promo||'';


        chess.move({from:source,to:target,promotion:promo});
        board.position(chess.fen());
        //chess.move(target);
        //chess.setFenPosition();

    });

    socket.on('oppntWon', (requestData) => {
        $('.notification')
        .html('<div class="alert alert-success">You Won !!</div>');
        chess.reset();
        board.reset();
    });

});

ChessboardJS library implementation

  • Download the library files from GitHub.
  • Under public/lib/chessboardjs folder. Place JS and Css files.
  • Images put them under public/img

Get the full working source code from the GitHub repository.

Demo Video