TL;DR
Implementing Multiplayer Tic-Tac-Toe Game using Angular, Nodejs and socket can be complex and time-consuming, But it doesn’t have to be.In this article, we will implement the Multiplayer Tic-Tac-Toe Game using Angular and Nodejs as the Page title suggests. Basically, we will write the client side of the Game in Angular and server side will be in Nodejs. Out of the box here we will learn how to use rooms in socket.io and how to consume it in the Angular application. Note that, The winner of the game will be calculated on the server only.
1. Why Multiplayer Tic-Tac-Toe Game using Angular, Nodejs
A few months ago when I publishedprivate chat application in angular, I received a lot of suggestions to use socket.io rooms and namespace feature within that application. So I decided to write this article, In this article, we will be using socket.io’s Rooms feature which is very easy to use. I have divided this application into two parts. Each part of this article covers unique part in building the Multiplayer Tic-Tac-Toe Game.
Part 1=>We will create a Nodejs server for the Game.
Part 2 =>Here we will implement the Actual Game using Angular.
So without further ado, Let’s get to the work.
2. Creating a new Nodejs project
1. Let’s start off by creating a new Nodejs project by usingnpm init
command.This command will create a new package.json file.
2.As you can see in below package.json file we have installed Redis as a dependency module, So I think you have already know that we will be using Redis database. Here the role of Redis Database is just to store and fetch the data from database nothing more than that. Below is my package.json file for this application.
package.json:
{ "name": "tic-tac-toe-socket-api", "version": "1.0.0", "description": "This is socket API for Multiplayer Tic-Tac-Toe Game using Angular, Nodejs", "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "Shashank Tiwari", "license": "MIT", "dependencies": { "body-parser": "1.18.2 ", "cors": "2.8.4", "express": "4.16.2", "redis": "2.8.0", "socket.io": "2.0.4" } }
3. Creating a NodeJs Server
1.Before writing our Nodejs Server let’s take look at our project directory structure shown below.
2.Let’s start from the top, The first folder is/node_modules
and after that, there is/utils
folder, where all the magic will happen. Basically, in utils folder, we have three files as you can see in below image.
db.js=> In this file we will write a code to connect Redis database.
routes.js=> This file will hold the code for HTTP routes hence the name routes.js.
socket.js=> Here we will write the socket events for the multiplayer game.
server.js=> In this file we will write a code to start a Nodejs server and we will initiate the socket events and routes of the application.
3.Create a server.js in the root of the project, which will be our entry point for the project.Here we will define our route and we will start the server.
=>TheappConfig()
method will set the application configuration.
=>InincludeRoutes()
method we will execute the application route.
=>TheappExecute()
method will call theappConfig()
as well asappExecute()
and it will run the nodejs server.
server.js:
/* * @author Shashank Tiwari * Multiplayer Tic-Tac-Toe Game using Angular, Nodejs */'use strict'; const express = require("express"); const http = require('http'); const socketio = require('socket.io'); const bodyParser = require('body-parser'); const cors = require('cors'); const socketEvents = require('./utils/socket'); const routes = require('./utils/routes'); const redisDB = require("./utils/db").connectDB(); class Server{ constructor(){ this.port = process.env.PORT || 4000; this.host = `localhost`; this.app = express(); this.http = http.Server(this.app); this.socket = socketio(this.http); } appConfig(){ this.app.use( bodyParser.json() ); this.app.use( cors() ); } /* Including app Routes starts*/ includeRoutes(){ new routes(this.app,redisDB).routesConfig(); new socketEvents(this.socket,redisDB).socketConfig(); } /* Including app Routes ends*/ appExecute(){ this.appConfig(); this.includeRoutes(); this.http.listen(this.port, this.host, () => { console.log(`Listening on http://${this.host}:${this.port}`); }); } } const app = new Server(); app.appExecute();
4. Connecting Redis database with the application
1.Here I assume you have installed the Redis on your working machine and you are ready with your Redis server to connect with this application.
2.Create adb.js
file inside the/utils
folder and write down the below code.In the below code, we are connecting the Redis database by using the redis module which is available on NPM.
3.The first reason for using Redis is we would require soocket.io room’s data in different stages of the application and second It’s very fast when it comes to storing and retrieving the data from the database.
db.js:
/* * @author Shashank Tiwari * Multiplayer Tic-Tac-Toe Game using Angular, Nodejs */"use strict"; class redisDB{ constructor(){ this.redis = require("redis"); } connectDB(){ const client = this.redis.createClient({ host : '127.0.0.1', post : 6379 }); client.on("error", (err) => { console.log("Error " + err); }); client.on("ready", (err) => { console.log("Ready "); }); require('bluebird').promisifyAll(client); return client; } } module.exports = new redisDB();
5. Creating a Socket Events
1.Now that we have connected our application to Redis Database let’s use it in action.
2.Createsocket.js
inside the/utils
folder and write down the below code. In Thick words, the code will receive the socket event from the client and emit the events from the server.
Explanation:
=>The first method is constructor which expects two parameters. First is socket object and the second parameter is object of Redis database connection. Then we have Tic-Tac-Toe game’s win combination in the form of an array which we will use down the road.
=>Then just below to that line we are storing the initial room count and the empty rooms and full rooms inside the Redis database.
=>ThesocketEvent()
method puts the life into this file, this method does all the work. In this method, we will listen and emit the socket event. So let’s start from the top to bottom.
create-room
:This event will be called when a user will click on the create new room button. Here we will fetch the data from Redis and after creating a new room, we update the data into Redis and socket will emit an updated data.join-room
:In this event, a player can join an Existing Room, so we do the same operation as above.send-move
:This event decides which player is a winnerby comparing the array sent from the client with win combination arraythat we have already defined inside the constructor method.disconnecting
:In this event, we will remove the user from a socket room and we will notify the opponent.
=>As I said above thesend-move
will decide the winner of the Game, once the winner is determined then we will send the winner’s message we will restart the Game. This will be handled on the client side, which means we will do this in Angular.
socket.js:
/* * @author Shashank Tiwari * Multiplayer Tic-Tac-Toe Game using Angular, Nodejs */'use strict'; class Socket{ constructor(socket,redisDB){ this.io = socket; this.redisDB = redisDB; /* Win combination to check winner of the Game.*/ this.winCombination = [ [1, 2, 3], [4, 5, 6], [7, 8, 9], [1, 4, 7], [2, 5, 8], [3, 6, 9], [1, 5, 9], [7, 5, 3] ]; /* Inserting the values into the radis Database start */ redisDB.set("totalRoomCount", 1); redisDB.set("allRooms", JSON.stringify({ emptyRooms: [1], fullRooms : [] })); /* Inserting the values into the radis Database ends */ } socketEvents(){ const IO = this.io; const redisDB = this.redisDB; let allRooms = null; let totalRoomCount = null; IO.on('connection', (socket) => { socket.setMaxListeners(20); /* Setting Maximum listeners */ /* * In this Event user will create a new Room and can ask someone to join. */ socket.on('create-room', (data) => { Promise.all(['totalRoomCount','allRooms'].map(key => redisDB.getAsync(key))).then(values => { const allRooms = JSON.parse(values[1]); let totalRoomCount = values[0]; let fullRooms = allRooms['fullRooms']; let emptyRooms = allRooms['emptyRooms']; /*Checking the if the room is empty.*/ let isIncludes = emptyRooms.includes(totalRoomCount); if(!isIncludes){ totalRoomCount++; emptyRooms.push(totalRoomCount); socket.join("room-"+totalRoomCount); redisDB.set("totalRoomCount", totalRoomCount); redisDB.set("allRooms", JSON.stringify({ emptyRooms: emptyRooms, fullRooms : fullRooms })); IO.emit('rooms-available', { 'totalRoomCount' : totalRoomCount, 'fullRooms' : fullRooms, 'emptyRooms': emptyRooms }); IO.sockets.in("room-"+totalRoomCount).emit('new-room', { 'totalRoomCount' : totalRoomCount, 'fullRooms' : fullRooms, 'emptyRooms': emptyRooms, 'roomNumber' : totalRoomCount }); } }); }); /* * In this event will user can join the selected room */ socket.on('join-room', (data) => { const roomNumber = data.roomNumber; Promise.all(['totalRoomCount','allRooms'].map(key => redisDB.getAsync(key))).then(values => { const allRooms = JSON.parse(values[1]); let totalRoomCount = values[0]; let fullRooms = allRooms['fullRooms']; let emptyRooms = allRooms['emptyRooms']; let indexPos = emptyRooms.indexOf(roomNumber); if(indexPos > -1){ emptyRooms.splice(indexPos,1); fullRooms.push(roomNumber); } /* User Joining socket room */ socket.join("room-"+roomNumber); redisDB.set("allRooms", JSON.stringify({ emptyRooms: emptyRooms, fullRooms : fullRooms })); /* Getting the room number from socket */ const currentRoom = (Object.keys(IO.sockets.adapter.sids[socket.id]).filter(item => item!=socket.id)[0]).split('-')[1]; IO.emit('rooms-available', { 'totalRoomCount' : totalRoomCount, 'fullRooms' : fullRooms, 'emptyRooms': emptyRooms }); IO.sockets.in("room-"+roomNumber).emit('start-game', { 'totalRoomCount' : totalRoomCount, 'fullRooms' : fullRooms, 'emptyRooms': emptyRooms, 'roomNumber' : currentRoom }); }); }); /* * This event will send played moves between the users * Also Here we will calaculate the winner. */ socket.on('send-move', (data) => { const playedGameGrid = data.playedGameGrid; const movesPlayed = data.movesPlayed; const roomNumber = data.roomNumber; let winner = null; /* checking the winner */ this.winCombination.forEach(singleCombination => { if (playedGameGrid[singleCombination[0]] !== undefined && playedGameGrid[singleCombination[0]] !== null && playedGameGrid[singleCombination[1]] !== undefined && playedGameGrid[singleCombination[1]] !== null && playedGameGrid[singleCombination[2]] !== undefined && playedGameGrid[singleCombination[2]] !== null && playedGameGrid[singleCombination[0]]['player'] === playedGameGrid[singleCombination[1]]['player'] && playedGameGrid[singleCombination[1]]['player'] === playedGameGrid[singleCombination[2]]['player'] ) { winner = playedGameGrid[singleCombination[0]]['player'] + ' Wins !'; } else if (movesPlayed === 9) { winner = 'Game Draw'; } return false; }); if(winner === null){ socket.broadcast.to("room-"+roomNumber).emit('receive-move', { 'position' : data.position, 'playedText' : data.playedText, 'winner' : null }); }else{ IO.sockets.in("room-"+roomNumber).emit('receive-move', { 'position' : data.position, 'playedText' : data.playedText, 'winner' : winner }); } }); /* * Here we will remove the room number from fullrooms array * And we will update teh Redis DB keys. */ socket.on('disconnecting',()=>{ const rooms = Object.keys(socket.rooms); const roomNumber = ( rooms[1] !== undefined && rooms[1] !== null) ? (rooms[1]).split('-')[1] : null; if(rooms !== null){ Promise.all(['totalRoomCount','allRooms'].map(key => redisDB.getAsync(key))).then(values => { const allRooms = JSON.parse(values[1]); let totalRoomCount = values[0]; let fullRooms = allRooms['fullRooms']; let emptyRooms = allRooms['emptyRooms']; let fullRoomsPos = fullRooms.indexOf(parseInt(roomNumber)); if( fullRoomsPos > -1 ){ fullRooms.splice(fullRoomsPos,1); } if (totalRoomCount > 1) { totalRoomCount--; }else{ totalRoomCount = 1; } redisDB.set("totalRoomCount", totalRoomCount); redisDB.set("allRooms", JSON.stringify({ emptyRooms: emptyRooms, fullRooms : fullRooms })); IO.sockets.in("room-"+roomNumber).emit('room-disconnect', {id: socket.id}); }); }//if ends }); }); } socketConfig(){ this.socketEvents(); } } module.exports = Socket;
6. Last but not the least A HTTP Call
When user will open the home page of the application we will show the available rooms to play and the total number of Rooms created on the server. So for that, we will call HTTP request which will fetch the records from Redis Data and throw the response from the server to the client.
=>Createroutes.js
file inside the/utils
folder, and write down the below code.
=>In the below code we have/getRoomStats
GET request which will send the available rooms and total rooms by fetching the records from the Redis database.
routes.js:
/* * @author Shashank Tiwari * Multiplayer Tic-Tac-Toe Game using Angular, Nodejs */'use strict'; class Routes{ constructor(app,redisDB){ this.redisDB = redisDB; this.app = app; } appRoutes(){ const redisDB = this.redisDB; /* Getting the total room count and Avaialable rooms to chat */ this.app.get('/getRoomStats', (request,response) => { Promise.all(['totalRoomCount','allRooms'].map(key => redisDB.getAsync(key))).then(values => { const totalRoomCount = values[0]; const allRooms = JSON.parse(values[1]); response.status(200).json({ 'totalRoomCount' : totalRoomCount, 'fullRooms' : allRooms['fullRooms'], 'emptyRooms': allRooms['emptyRooms'] }); }); }); } routesConfig(){ this.appRoutes(); } } module.exports = Routes;
Well, till now you have completed the first of the Multiplayer Tic-Tac-Toe Game using Angular, Nodejs, and socket and now your Game server ready to rock n roll. Now the only thing left is to create Game in Angular, Which we will discuss and build together in the next part of this article.
Till now If you have any doubts or suggestions Please do let me know in below comment box, I will be happy to reply you. So, I will meet in the next part of this article.
ng init command is not working anymore.
I want to follow the tutorial but I don’t know how to start.
Hi Noor,
Am extremely sorry, it is a typo. The command is
npm init
to create new node server.I have updated the article.
Thanks
Thank you!
I am trying to run the client side code but it shows some errors with the rxjs and other libraries I think it is because of the old version of the angular CLI.
I tried to run ng update @angular/cli, but still does not fix the problem.
also, I tried updating the rxjs to the latest version but did not get to solve the issue.
Sorry, I am a beginner 🙁