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
- Basic knowledge about NodeJs and ES2015/ES2016
- HandleBarJs template engine
- Basic knowledge of socket.io
- Understand Chess Rules
Code Parts
- Templates
- Socket.io server end controller
- Socket.io client end functions.
Required NodeJs Modules
- socket.io
- hbs
- express
- 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.