Tic-Tac-Toe Online Game Using ReactJS and NodeJS (Socket.io)

Introduction

 
In this article, we will learn how we can make a Tic-Tac-Toe online game using React JS as the front end and Node JS (Socket.io) as the backend server.
 
First of all, for those unaware of Node JS Socket.io, Node JS is a server side programming language that runs on a V8 engine and Socket.io npm module which provides a facility for bi-directional communication between the server and client using websockets. In short, socket.io provides a way for the client to connect to the server, then afterward the client and server can communicate for a long time without the same connection.
 
For this example, you should have knowledge of Node JS and React Js. So let’s start guys.
 
Output
 
 
Prerequisite
  • Node JS : https://nodejs.org/en/
  • Socket.io : https://socket.io/
  • Getting Started React Native: https://reactnative.dev/docs/getting-started
  • Bootstrap for React JS : https://react-bootstrap.github.io/
  • Socket.io client for React JS : https://www.npmjs.com/package/socket.io-client
Project Repos
 
Server : https://github.com/myvsparth/tic-tac-toe-server
ReactJS Front End : https://github.com/myvsparth/react-js-tic-tac-toe
 
First, create a server for the tic tac toe game.
  • As you know, node js is a server side language and socket.io provides bidirectional communication. We will first create an HTTP server that will be up to handle each coming request and hold it until the client requests to leave it.
Clone my git repo for the full source code. Please go through the comment lines of the source code so you can understand how it works. Below is the index.js file commented source code:
  1. const server = require('http').createServer(); // STEP 1 ::=> HTTP Server object  
  2. const io = require('socket.io')(server); // STEP 2 ::=> Bind socket.io to http server so after http connection the bidirectional communication keep up using web sockets.  
  3. const PORT = 4444; // PORT of server  
  4. const HOST = "127.0.0.1"// Hosting Server change when you make it live on server according to your hosting server  
  5. var players = {}; // It will keep all the players data who have register using mobile number. you can use actual persistence database I have used this for temporery basis  
  6. var sockets = {}; // stores all the connected clients  
  7. var games = {}; // stores the ongoing game  
  8. var winCombinations = [  
  9.     [[0, 0], [0, 1], [0, 2]],  
  10.     [[1, 0], [1, 1], [1, 2]],  
  11.     [[2, 0], [2, 1], [2, 2]],  
  12.     [[0, 0], [1, 0], [2, 0]],  
  13.     [[0, 1], [1, 1], [2, 1]],  
  14.     [[0, 2], [1, 2], [2, 2]],  
  15.     [[0, 0], [1, 1], [2, 2]],  
  16.     [[0, 2], [1, 1], [2, 0]]  
  17. ]; // game winning combination index   
  18.    
  19. // STEP 4 ::=> When any request comes it will trigger and bind all the susequence events that will triggered as per app logic  
  20. io.on('connection', client => {  
  21.     console.log("connected : " + client.id);  
  22.     client.emit('connected', { "id": client.id }); // STEP 5 ::=> Notify request cllient that it is not connected with server  
  23.       
  24.     // STEP 6 ::=> It is a event which will handle user registration process  
  25.     client.on('checkUserDetail', data => {  
  26.         var flag = false;  
  27.         for (var id in sockets) {  
  28.             if (sockets[id].mobile_number === data.mobileNumber) {  
  29.                 flag = true;  
  30.                 break;  
  31.             }  
  32.         }  
  33.         if (!flag) {  
  34.             sockets[client.id] = {  
  35.                 mobile_number: data.mobileNumber,  
  36.                 is_playing: false,  
  37.                 game_id: null  
  38.             };  
  39.    
  40.             var flag1 = false;  
  41.             for (var id in players) {  
  42.                 if (id === data.mobileNumber) {  
  43.                     flag1 = true;  
  44.                     break;  
  45.                 }  
  46.             }  
  47.             if (!flag1) {  
  48.                 players[data.mobileNumber] = {  
  49.                     played: 0,  
  50.                     won: 0,  
  51.                     draw: 0  
  52.                 };  
  53.             }  
  54.    
  55.         }  
  56.         client.emit('checkUserDetailResponse', !flag);  
  57.     });  
  58.    
  59.     // STEP 7 ::=> It will send all the players who are online and avalable to play the game  
  60.     client.on('getOpponents', data => {  
  61.         var response = [];  
  62.         for (var id in sockets) {  
  63.             if (id !== client.id && !sockets[id].is_playing) {  
  64.                 response.push({  
  65.                     id: id,  
  66.                     mobile_number: sockets[id].mobile_number,  
  67.                     played: players[sockets[id].mobile_number].played,  
  68.                     won: players[sockets[id].mobile_number].won,  
  69.                     draw: players[sockets[id].mobile_number].draw  
  70.                 });  
  71.             }  
  72.         }  
  73.         client.emit('getOpponentsResponse', response);  
  74.         client.broadcast.emit('newOpponentAdded', {  
  75.             id: client.id,  
  76.             mobile_number: sockets[client.id].mobile_number,  
  77.             played: players[sockets[client.id].mobile_number].played,  
  78.             won: players[sockets[client.id].mobile_number].won,  
  79.             draw: players[sockets[client.id].mobile_number].draw  
  80.         });  
  81.     });  
  82.    
  83.     // STEP 8 ::=> When Client select any opponent to play game then it will generate new game and return playboard to play the game. New game starts here  
  84.     client.on('selectOpponent', data => {  
  85.         var response = { status: false, message: "Opponent is playing with someone else." };  
  86.         if (!sockets[data.id].is_playing) {  
  87.             var gameId = uuidv4();  
  88.             sockets[data.id].is_playing = true;  
  89.             sockets[client.id].is_playing = true;  
  90.             sockets[data.id].game_id = gameId;  
  91.             sockets[client.id].game_id = gameId;  
  92.             players[sockets[data.id].mobile_number].played = players[sockets[data.id].mobile_number].played + 1;  
  93.             players[sockets[client.id].mobile_number].played = players[sockets[client.id].mobile_number].played + 1;  
  94.    
  95.             games[gameId] = {  
  96.                 player1: client.id,  
  97.                 player2: data.id,  
  98.                 whose_turn: client.id,  
  99.                 playboard: [[""""""], [""""""], [""""""]],  
  100.                 game_status: "ongoing"// "ongoing","won","draw"  
  101.                 game_winner: null, // winner_id if status won  
  102.                 winning_combination: []  
  103.             };  
  104.             games[gameId][client.id] = {  
  105.                 mobile_number: sockets[client.id].mobile_number,  
  106.                 sign: "x",  
  107.                 played: players[sockets[client.id].mobile_number].played,  
  108.                 won: players[sockets[client.id].mobile_number].won,  
  109.                 draw: players[sockets[client.id].mobile_number].draw  
  110.             };  
  111.             games[gameId][data.id] = {  
  112.                 mobile_number: sockets[data.id].mobile_number,  
  113.                 sign: "o",  
  114.                 played: players[sockets[data.id].mobile_number].played,  
  115.                 won: players[sockets[data.id].mobile_number].won,  
  116.                 draw: players[sockets[data.id].mobile_number].draw  
  117.             };  
  118.             io.sockets.connected[client.id].join(gameId);  
  119.             io.sockets.connected[data.id].join(gameId);  
  120.             io.emit('excludePlayers', [client.id, data.id]);  
  121.             io.to(gameId).emit('gameStarted', { status: true, game_id: gameId, game_data: games[gameId] });  
  122.    
  123.         }  
  124.     });  
  125.    
  126.     var gameBetweenSeconds = 10; // Time between next game  
  127.     var gameBetweenInterval = null;  
  128.    
  129.     // STEP 9 ::=> When Player select any cell then it will check all the necessory logic of Tic Tac Toe Game  
  130.     client.on('selectCell', data => {  
  131.         games[data.gameId].playboard[data.i][data.j] = games[data.gameId][games[data.gameId].whose_turn].sign;  
  132.    
  133.         var isDraw = true;  
  134.         for (let i = 0; i < 3; i++) {  
  135.             for (let j = 0; j < 3; j++) {  
  136.                 if (games[data.gameId].playboard[i][j] == "") {  
  137.                     isDraw = false;  
  138.                     break;  
  139.                 }  
  140.             }  
  141.         }  
  142.         if (isDraw)  
  143.             games[data.gameId].game_status = "draw";  
  144.    
  145.    
  146.         for (let i = 0; i < winCombinations.length; i++) {  
  147.             var tempComb = games[data.gameId].playboard[winCombinations[i][0][0]][winCombinations[i][0][1]] + games[data.gameId].playboard[winCombinations[i][1][0]][winCombinations[i][1][1]] + games[data.gameId].playboard[winCombinations[i][2][0]][winCombinations[i][2][1]];  
  148.             if (tempComb === "xxx" || tempComb === "ooo") {  
  149.                 games[data.gameId].game_winner = games[data.gameId].whose_turn;  
  150.                 games[data.gameId].game_status = "won";  
  151.                 games[data.gameId].winning_combination = [[winCombinations[i][0][0], winCombinations[i][0][1]], [winCombinations[i][1][0], winCombinations[i][1][1]], [winCombinations[i][2][0], winCombinations[i][2][1]]];  
  152.                 players[games[data.gameId][games[data.gameId].game_winner].mobile_number].won++;  
  153.             }  
  154.         }  
  155.         if (games[data.gameId].game_status == "draw") {  
  156.             players[games[data.gameId][games[data.gameId].player1].mobile_number].draw++;  
  157.             players[games[data.gameId][games[data.gameId].player2].mobile_number].draw++;  
  158.         }  
  159.         games[data.gameId].whose_turn = games[data.gameId].whose_turn == games[data.gameId].player1 ? games[data.gameId].player2 : games[data.gameId].player1;  
  160.         io.to(data.gameId).emit('selectCellResponse', games[data.gameId]);  
  161.    
  162.         if (games[data.gameId].game_status == "draw" || games[data.gameId].game_status == "won") {  
  163.             gameBetweenSeconds = 10;  
  164.             gameBetweenInterval = setInterval(() => {  
  165.                 gameBetweenSeconds--;  
  166.                 io.to(data.gameId).emit('gameInterval', gameBetweenSeconds);  
  167.                 if (gameBetweenSeconds == 0) {  
  168.                     clearInterval(gameBetweenInterval);  
  169.    
  170.                     var gameId = uuidv4();  
  171.                     sockets[games[data.gameId].player1].game_id = gameId;  
  172.                     sockets[games[data.gameId].player2].game_id = gameId;  
  173.                     players[sockets[games[data.gameId].player1].mobile_number].played = players[sockets[games[data.gameId].player1].mobile_number].played + 1;  
  174.                     players[sockets[games[data.gameId].player2].mobile_number].played = players[sockets[games[data.gameId].player2].mobile_number].played + 1;  
  175.    
  176.                     games[gameId] = {  
  177.                         player1: games[data.gameId].player1,  
  178.                         player2: games[data.gameId].player2,  
  179.                         whose_turn: games[data.gameId].game_status == "won" ? games[data.gameId].game_winner : games[data.gameId].whose_turn,  
  180.                         playboard: [[""""""], [""""""], [""""""]],  
  181.                         game_status: "ongoing"// "ongoing","won","draw"  
  182.                         game_winner: null, // winner_id if status won  
  183.                         winning_combination: []  
  184.                     };  
  185.                     games[gameId][games[data.gameId].player1] = {  
  186.                         mobile_number: sockets[games[data.gameId].player1].mobile_number,  
  187.                         sign: "x",  
  188.                         played: players[sockets[games[data.gameId].player1].mobile_number].played,  
  189.                         won: players[sockets[games[data.gameId].player1].mobile_number].won,  
  190.                         draw: players[sockets[games[data.gameId].player1].mobile_number].draw  
  191.                     };  
  192.                     games[gameId][games[data.gameId].player2] = {  
  193.                         mobile_number: sockets[games[data.gameId].player2].mobile_number,  
  194.                         sign: "o",  
  195.                         played: players[sockets[games[data.gameId].player2].mobile_number].played,  
  196.                         won: players[sockets[games[data.gameId].player2].mobile_number].won,  
  197.                         draw: players[sockets[games[data.gameId].player2].mobile_number].draw  
  198.                     };  
  199.                     io.sockets.connected[games[data.gameId].player1].join(gameId);  
  200.                     io.sockets.connected[games[data.gameId].player2].join(gameId);  
  201.               
  202.                     io.to(gameId).emit('nextGameData', { status: true, game_id: gameId, game_data: games[gameId] });  
  203.    
  204.                     io.sockets.connected[games[data.gameId].player1].leave(data.gameId);  
  205.                     io.sockets.connected[games[data.gameId].player2].leave(data.gameId);  
  206.                     delete games[data.gameId];  
  207.                 }  
  208.             }, 1000);  
  209.         }  
  210.    
  211.     });  
  212.    
  213.     // STEP 10 ::=> When any player disconnect then it will handle the disconnect process  
  214.     client.on('disconnect', () => {  
  215.         console.log("disconnect : " + client.id);  
  216.         if (typeof sockets[client.id] != "undefined") {  
  217.             if (sockets[client.id].is_playing) {  
  218.               
  219.                 io.to(sockets[client.id].game_id).emit('opponentLeft', {});  
  220.                 players[sockets[games[sockets[client.id].game_id].player1].mobile_number].played--;  
  221.                 players[sockets[games[sockets[client.id].game_id].player2].mobile_number].played--;  
  222.                 io.sockets.connected[client.id == games[sockets[client.id].game_id].player1 ? games[sockets[client.id].game_id].player2 : games[sockets[client.id].game_id].player1].leave(sockets[client.id].game_id);  
  223.                 delete games[sockets[client.id].game_id];  
  224.             }  
  225.         }  
  226.         delete sockets[client.id];  
  227.         client.broadcast.emit('opponentDisconnected', {  
  228.             id: client.id  
  229.         });  
  230.     });  
  231. });  
  232.    
  233.    
  234. server.listen(PORT, HOST); // 3 ::=> Staring HTTP server which will be consumed by clients  
  235. console.log("listening to : " + HOST + ":" + PORT);  
  236.    
  237.    
  238. // Generate Game ID  
  239. function uuidv4() {  
  240.     return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {  
  241.         var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);  
  242.         return v.toString(16);  
  243.     });  
  244. }  
  • To run the server first go to the project directory
    • npm install
    • npm start
  • Now your server is ready, so now take a look at how front end with react js works

Front end with React JS

 
To run the project:
  • I will explain important topics here and you can clone repo for full source code.
  • So first I have created react js project named as “react-js-tic-tac-toe” using the below commands:
    • create-react-app react-js-tic-tac-toe
    • cd react-js-tic-tac-toe
  • Then installed bootstrap dependency
    • npm install react-bootstrap bootstrap
  • Then installed socket.io client dependency
    • npm install socket.io-client
  • Then started project using
    • npm start
  • If you are using git repo then you do not need to follow above steps just but go to project root and run the below command:
    • npm install
    • npm start
In App.js, I have made a connection with the server using the below code:
  1. this.state = {  
  2.   endpoint: "http://127.0.0.1:4444",  
  3.   socket: null,  
  4.   isGameStarted: false,  
  5.   gameId:null,  
  6.   gameData: null,  
  7. };  
  1. componentDidMount() {  
  2.     const { endpoint } = this.state;  
  3.     const socket = socketIOClient(endpoint);  
  4.     socket.on("connected", data => {  
  5.       console.log(data);  
  6.       this.setState({ socket: socket })  
  7.     });  
  8.   }   
Repos

Conclusion

 
In this article, we learned how to create a tic tac toe game using React js and Node js socket.io