Creating a Tic-tac-toe app with Angular and WebSockets using Socket.IO

Chester Supelana

January 26, 2021

What are WebSockets

WebSockets are a way to create bidirectional communication between a client and a server. This means data can flow in both ways, from the client to the server and vice versa. Another feature of using WebSockets is that it allows for real-time data flow on multiple different clients. Consider this example:

<div> <img class="img-fluid" src="https://strapi.saperium.com/uploads/websockets1_5b66ef1186.png"> </div>

<br/>

When each of those clients first connects to the server, they are opening up a web socket between the client and the server. This becomes their own communication channel. Now, let us say client Blue sends some data to the server. Now the server is able to send the received data to the other clients. By doing so, changes made by client Blue will reflect on all of the other clients immediately without the other clients needing to request for any data. This creates the “real-time” effect that we see in many real-world applications like chat apps, multiplayer games, real-time collaborative document editing (Google docs, sheets), etc.

In this article, we will create a simple tic-tac-toe app that uses WebSockets for data transfer. We will also be using Socket.IO to implement WebSockets.

<br/>

What is Socket.io

Socket.IO enables real-time, bidirectional and event-based communication. It works on every platform, browser or device, focusing equally on reliability and speed.

Socket.io is a library that abstracts the implementation of WebSockets and it makes it very easy to create apps with real-time updates.

The main features of Socket.io are:

1. Reliability

  • Connections are established even in the presence of:
    • proxies and load balancers
    • personal firewall and antivirus software

2. Auto-reconnection support

  • A disconnected client will try to reconnect forever, until the server is available again

3. Disconnection detection

  • Both the server and the client can know when the other one is not responding anymore

4. Binary support

  • Any serializable data structures can be emitted, including:
    • ArrayBuffer and Blob in the browser
    • ArrayBuffer and Buffer in Node.js

5. Simple and convenient API

6. Multiplexing support

  • Create separation of concerns within your application (for example per module, or based on permissions)

7. Room support

  • Can define arbitrary channels, called Rooms, that sockets can join and leave

Socket.io has two parts so we will need to set up Socket.io on both ends (on the server and the client):

1. a Node.js server

2. a Javascript client library for the browser

<br/>

Installing Socket.io

First, create an express app to serve as our backend. To create an express app, we can use express-generator. Install npx and run the application generator command:

mkdir &lt;server-name&gt;
cd &lt;server-name&gt;
npx express-generator

To install Socket.io, run the following command in your application directory (let us also install shortid as well since we will also use that):

npm install socket.io @types/socket.io --save
npm install shortid

Then, we can start the express server.

npm start

Next, let us create a frontend app. For this exercise, we will use Angular. While outside the server app directory, install angular-cli and generate an angular app:

npm install -g @angular/cli
ng new &lt;client-name&gt;
cd &lt;client-name&gt;

Now, we can install Socket.io for Angular, as well as generate all necessary components and services we will need. We will use ngx-socket-io which is a Socket.io module built for Angular.

npm i ngx-socket-io --save
ng g c game-list
ng g c game
ng g s game

<br/>

Overview

For this exercise, we want our tic-tac-toe app to do the following:

1. Create a tic-tac-toe game room

2. List all tic-tac-toe games

3. View a tic-tac-toe game room

4. Join a tic-tac-toe game as a player

5. Make a move/play a tic-tac-toe game

With that said, we will need a button to create new game rooms. We will also need to make a view for the list of games. Then, we will need to show an input box where users can type their name and a join button to join a game. And finally we will need to show the actual tic-tac-toe game board.

<div> <img class="img-fluid" src="https://strapi.saperium.com/uploads/websockets2_05ae29e938.png"> </div>

<br/>

To do this, the server will need a data store for saving our tic-tac-toe games. And when the user clicks on a game from the list, it will show the players and the current board. On the game board, when the Join button is clicked, the server will need to add that player into the game. And lastly, if a user clicks on a cell on the tic-tac-toe board, the server needs to update the game accordingly.

<br/>

Set up Socket.io for your express app

Now that Socket.io is installed in your express app, we can now set up web sockets for our server. In bin/www, you will see that a server object is created:

var app = require('../app');
var http = require('http');

var server = http.createServer(app);
server.listen(3000); // or any port you wish to use

We will use this object to attach a socket server into our express app.

// Set up sockets
const socket = require('socket.io');
const io = socket(server);

After that, we can now tell the socket server what to do. Let us start by printing something when a connection is made to the socket server.

io.on('connection', socket =&gt; {
 console.log('connected');
});

In the code above, we used the .on('...') function. This function is an event listener. The first parameter is the event name, and the second parameter is the callback that we want to execute when the event is fired and received by the socket server (as well as the payload sent). In this case, the ‘connection’ event is fired when a client connects to the server (connection is a reserved event name in socket.io) and so, the server executes the callback with a socket (from the initiating client) as its parameter and prints ‘connected’ to the console.

<br/>

Set up socket.io for your frontend/client side

To connect to the socket server, we also need to set up socket.io on the client/frontend. First, we need to let our Angular app know that we will be using this module. Add these lines before ngModule to set up socket.io in app.module.ts (use the port number where the express app is listening to):

import { SocketIoModule, SocketIoConfig } from 'ngx-socket-io';
// use the port number you used to set up the express app
const config: SocketIoConfig = { url: 'http://localhost:3000', options: {} };

Then, add this to your imports array in ngModule so it would look like:

 imports: [
   BrowserModule,
   AppRoutingModule,
   SocketIoModule.forRoot(config)
 ]

<br/>

Emitting messages

Now, we can establish a connection with the client and the socket server. Let’s create a service that will handle this communication.

In our game.service.ts file, add these lines:

import { Injectable } from '@angular/core';
import { Socket } from 'ngx-socket-io';

@Injectable({
 providedIn: 'root'
})
export class GameService {
 constructor(private socket: Socket) { }

 getGame(gameId: string): void {
   this.socket.emit('getGame', gameId);
 }

 newGame(): void {
   this.socket.emit('addGame');
 }

 joinGame(gameId: string, playerName: string): void {
   this.socket.emit('joinGame', {gameId, playerName});
 }

 playGame(gameId: string, playerName: string, x: number, y: number): void {
   this.socket.emit('playGame', {gameId, playerName, x, y});
 }
}

Let us break this down. We injected the socket provided by ngx-socket-io to our service so that we can use its very nice features. We also initialized a socket here. Upon doing that, we have already established a connection to the server (socket is the channel between the client and server). So we should see that our server printed ‘connected’ to the console.

One of the features of Socket.io is something we used in all of our functions. The .emit('...') function allows us to fire/send an event to the socket server in our express app. Like the .on('...') function, it takes the event name as the first parameter, but takes the payload we want to send as the second parameter.

In our getGame function, we emit the ‘getGame’ event and send the gameId as the payload. So when we call this function, the socket server in our express app should know that the ‘getGame’ event was fired and will receive the gameId. But we haven’t told the server what to do when the ‘getGame’ event is emitted. So, let’s do that.

Back in our bin/www file, let us first create an in-memory data store of our games (do not do this in a real application — use a database instead).

const games = {};

The structure of our games will be like the following: each game is a key-value pair with the key being the gameId and the value being an object containing the players and the current state of the game (0 => empty || 1 => player 1 || -1 => player 2) .

games = {
 gH67FBgs: {
   players: [],
   state: [
     [0, 0, -1],
     [1, 1, -1],
     [0, 0, 0]
   ]
 }, ...
}

Now inside the connection event listener, let us add the getGame event listener.

io.on('connection', socket =&gt; {
 console.log('connected')

 socket.on('getGame', gameId =&gt; {
   const game = {};
   // set the key as the id and the value as the players and state
   game[gameId] = games[gameId];
   socket.emit('game', game);
 });
});

We used the socket from the initiating client to listen to the ‘getGame’ event. We do this because we only want to communicate with the socket that fired the event and not bother communicating with the others (remember, web sockets are like channels). At the end of the callback, we called the .emit('...') function again. This lets our server send back data to the client. In this case, we are sending back the corresponding game with the given gameId. Notice that we are emitting the ‘game’ event here so we need to make an event listener in our client.

Back in our game.service.ts, add this line:

export class GameService {
 currentGame = this.socket.fromEvent&lt;any&gt;('game');

 constructor(private socket: Socket) { }

 ...
}

The currentGame property here refers to the game that the server sent to us to display on our UI. So when the express app fires the game event, our client can receive the data as an Observable.

For our tic-tac-toe game to work, we first need to set up our UI. Add the game list component to the app by including it in app.component.html.

&lt;app-game-list&gt;&lt;/app-game-list&gt;

In our game-list.component.html, add these:

&lt;div class='sidenav'&gt;
   &lt;span (click)='newGame()'&gt;New Game&lt;/span&gt;
 &lt;/div&gt;

This will create a sidebar containing a button that lets us create a new game. Next, add some styles in game-list.component.css:

.sidenav {
   position: fixed;
   height: 100%;
   width: 220px;
   top: 0;
   left: 0;
   background-color: #111111;
   overflow-x: hidden;
   padding-top: 20px;
}
.sidenav span {
   padding: 6px  8px  6px  16px;
   text-decoration: none;
   font-size: 25px;
   font-family: 'Roboto', Tahoma, Geneva, Verdana, sans-serif;
   color: #818181;
   display: block;
}
.sidenav span.selected {
   color: #e1e1e1;
}
.sidenav span:hover {
   color: #f1f1f1;
   cursor: pointer;
}

And then, add the following lines in the class definition in game-list.component.ts:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Observable, Subscription } from 'rxjs';
import { GameService } from '../game.service';

@Component({
 selector: 'app-game-list',
 templateUrl: './game-list.component.html',
 styleUrls: ['./game-list.component.css']
})
export class GameListComponent implements OnInit, OnDestroy {
 currentGameId: string;
 _gameSub: Subscription;

 constructor(private gameService: GameService) { }

 ngOnInit(): void {
   this._gameSub = this.gameService.currentGame.subscribe(game =&gt; {
     this.currentGameId = Object.keys(game)[0];
   });
 }

 ngOnDestroy(): void {
   this._gameSub.unsubscribe();
 }

 newGame(): void {
   this.gameService.newGame();
 }
}

In this component, the _gameSub property will be a subscription to the currentGame property in the game service so that we can do things when we receive data from the server. In this case, we are simply setting the current game to display.

We also want to add and list games, so let’s add those. Back in our express app, add the following event listener:

 socket.on('addGame', () =&gt; {
   // generate a random string as id
   const gameId = shortid.generate();
   // start with empty players
   const players = [];
   // start with empty state
   const state = [
     [0, 0, 0],
     [0, 0, 0],
     [0, 0, 0]
   ];

   // save the new game
   games[gameId] = {
     players: players,
     state: state
   }

   // send the newly created game to the connecting client
   const newGame = {};
   newGame[gameId] = games[gameId];
   socket.emit('game', newGame);

   // send the updated games to all clients (broadcast)
   io.emit('games', Object.keys(games));
 });

So now, when we emit the ‘addGame’ event, the server will add a game to the in-memory object that we have. The server should also let all the clients know that a new game was created and we can do that by using io.emit. Notice that we are sending the ‘games’ (with an s) event which is different from the event we used previously. We will need to catch this on our client. Afterwards, we want to send back the newly created game to the initiating client, so we simply send a game event back to the client.

In our service, let us receive the games event:

export class GameService {
 currentGame = this.socket.fromEvent&lt;any&gt;('game');
 games = this.socket.fromEvent&lt;string[]&gt;('games');

 constructor(private socket: Socket) { }
 ...
}

We also need to send the list of games when a client connects, so under the ‘connection’ event listener, let’s emit the games event:

io.on('connection', socket =&gt; {
 console.log('connected')

 socket.emit('games', Object.keys(games));
 ...
});

Now, we can show the list of games in our UI. Our html file should now look something like this:

&lt;div class='sidenav'&gt;
   &lt;span (click)='newGame()'&gt;New Game&lt;/span&gt;
   &lt;span [class.selected]='gameId === currentGameId' (click)='loadGame(gameId)' *ngFor='let gameId of games | async'&gt;{{ gameId }}&lt;/span&gt;
&lt;/div&gt;

And we need to select a game when we click on a gameId on the UI:

export class GameListComponent implements OnInit, OnDestroy {
 games: Observable&lt;string[]&gt;;
 ...

 constructor(private gameService: GameService) { }

 loadGame(gameId: string): void {
   this.gameService.getGame(gameId);
 }
 ...
}

If you have done everything correctly, your app should look something like this:

<div> <img class="img-fluid" src="https://strapi.saperium.com/uploads/websockets3_787a7bf07f.png"> </div>

It is quite empty right now. So, let us show the actual game. In the game.component.html add these:

&lt;div *ngIf="game" class="game-area"&gt;
   &lt;label for="playerName"&gt;Enter your name: &lt;/label&gt;
   &lt;input id="playerName" type="text" (keyup)="setPlayerName($event)" placeholder="Enter your name..." /&gt;
   &lt;button (click)="joinGame()"&gt;Join game&lt;/button&gt;

   &lt;div class="players"&gt;
       &lt;label&gt;Players:&lt;/label&gt;
       &lt;div *ngFor="let playerName of game[gameId].players"&gt;
           {{playerName}}
       &lt;/div&gt;
   &lt;/div&gt;

   &lt;div id="winner" *ngIf="winner"&gt;
       {{ winner }} wins!
   &lt;/div&gt;

   &lt;div id="game-board"&gt;
       &lt;div *ngFor="let row of game[gameId].state; let x = index" class="game-row"&gt;
           &lt;div *ngFor="let col of row; let y = index" class="tile" (click)="playGame(x, y)"&gt;
               {{ col === 0 ? '' : (col === 1 ? 'O' : 'X') }}
           &lt;/div&gt;

           &lt;br/&gt;
       &lt;/div&gt;
   &lt;/div&gt;
&lt;/div&gt;

And add styles in game.component.css:

.game-area {
   position: relative;
   left: 300px;
}

#winner, #game-board {
   margin-top: 1em;
   margin-bottom: 1em;
   width: 600px;
}

.game-row {
   display: flex;
}

.tile {
   width: 30px;
   height: 30px;
   border: 1px solid #000;
}

And finally, add these in the game.component.ts:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
import { GameService } from '../game.service';

@Component({
 selector: 'app-game',
 templateUrl: './game.component.html',
 styleUrls: ['./game.component.css']
})
export class GameComponent implements OnInit, OnDestroy {
 game: any;
 gameId: string;
 _gameSub: Subscription;
 currentPlayerName: string;
 winner: string;
 constructor(private gameService: GameService) { }

 ngOnInit(): void {
   this._gameSub = this.gameService.currentGame.subscribe(game =&gt; {
     this.game = game;
     this.gameId = Object.keys(game)[0];

     const winIndex = this.checkStateForWin(this.game[this.gameId].state);

     if (winIndex) {
       if (this.game[this.gameId].state[winIndex[0]][winIndex[1]] === 1) {
         this.winner = this.game[this.gameId].players[0];
       } else if (this.game[this.gameId].state[winIndex[0]][winIndex[1]] === -1) {
         this.winner = this.game[this.gameId].players[1];
       }
     } else {
       this.winner = null;
     }
   });
 }

 ngOnDestroy(): void {
   this._gameSub.unsubscribe();
 }

 setPlayerName(event): void {
   this.currentPlayerName = event.target.value;
 }

 joinGame(): void {
   if (this.currentPlayerName) {
     this.gameService.joinGame(this.gameId, this.currentPlayerName);
   }
 }

 playGame(x: number, y: number): void {
   this.gameService.playGame(this.gameId, this.currentPlayerName, x, y);
 }

 checkStateForWin(state): any {
   // send pos where win was found
   if (state[0][0] !== 0 &amp;&amp; state[0][0] === state[0][1] &amp;&amp; state[0][1] === state[0][2]) {
     return [0, 0];
   }
   if (state[1][0] !== 0 &amp;&amp; state[1][0] === state[1][1] &amp;&amp; state[1][1] === state[1][2]){
     return [1, 0];
   }
   if (state[2][0] !== 0 &amp;&amp; state[2][0] === state[2][1] &amp;&amp; state[2][1] === state[2][2]){
     return [2, 0];
   }
   if (state[0][0] !== 0 &amp;&amp; state[0][0] === state[1][0] &amp;&amp; state[1][0] === state[2][0]){
     return [0, 0];
   }
   if (state[0][1] !== 0 &amp;&amp; state[0][1] === state[1][1] &amp;&amp; state[1][1] === state[2][1]){
     return [0, 1];
   }
   if (state[0][2] !== 0 &amp;&amp; state[0][2] === state[1][2] &amp;&amp; state[1][2] === state[2][2]){
     return [0, 2];
   }
   if (state[0][0] !== 0 &amp;&amp; state[0][0] === state[1][1] &amp;&amp; state[1][1] === state[2][2]){
     return [0, 0];
   }
   if (state[0][2] !== 0 &amp;&amp; state[0][2] === state[1][1] &amp;&amp; state[1][1] === state[2][0]){
     return [0, 2];
   }
   return null;
 }
}

You do not need to know exactly how each function/property works but the role of each would be the following:

<u>game</u> - the current game to display

<u>gameId</u> - the id of the current game

<u>_gameSub </u>- the subscription that listens to the ‘game’ event

<u>currentPlayerName </u>- the name of the player on the current client

<u>winner </u>- the player that wins the game

<u>setPlayerName()</u> - sets the player name based on the value in the input box

<u>joinGame()</u> - emits the joinGame event using the game service

<u>playGame()</u> - emits the playGame event using the game service

<u>checkStateForWin()</u> - checks if the current state has a winner

As you might have guessed by now, the _gameSub will trigger whenever the express app sends the game event. This will update the current game to display and replace whatever gameId was sent. It also checks for the state of the game whether there is a winner already or not (which it shows to the UI). We haven’t added the joinGame and playGame event listeners yet to the express app but you should have learned everything you need to make them.

In our express app, add these event listeners:

 socket.on('joinGame', (event) =&gt; {
   // only add up to 2 players
   if (games[event.gameId].players.length &lt; 2) {
     games[event.gameId].players.push(event.playerName);
   }

   // send new game info
   const newGame = {};
   newGame[event.gameId] = games[event.gameId];
   socket.emit('game', newGame);
 });

 socket.on('playGame', (event) =&gt; {
   const gameId = event.gameId;
   const playerName = event.playerName;
   const positionX = event.x;
   const positionY = event.y;

   // position must be empty and players must be 2
   if (games[gameId].state[positionX][positionY] === 0 &amp;&amp; games[gameId].players.length === 2) {
     // player 1 made move
     if (games[gameId].players[0] === playerName) {
       games[gameId].state[positionX][positionY] = 1;
     }
     // player 2 made move
     else if (games[gameId].players[1] === playerName){
       games[gameId].state[positionX][positionY] = -1;
     }

     // send new game info
     const newGame = {};
     newGame[event.gameId] = games[event.gameId];
     socket.emit('game', newGame);
   }
 });

<br/>

Creating “Rooms”

Now, there is only 1 thing missing. We don’t only want to send the new game info to the initiating client. We want to send the new game info to everyone viewing the game that was updated so the changes are reflected in real-time for everybody. Socket.io lets us do that by creating “rooms”. You can think of it as like entering a real-life room when you view a game. And everyone in that room can see what everyone else is doing. And to view another game, you need to leave the current room and enter another.

Socket.io provides us with 2 functions for joining/leaving rooms which are aptly named join and leave respectively. Both take 1 parameter, the name of the room that you wish to join/leave. For the purposes of this exercise, we will simply name our rooms as the gameId.

So in our express app, we can add some lines to our getGame event listener:

io.on('connection', socket =&gt; {
 console.log('connected')
 let previousId;

 const safeJoin = currentId =&gt; {
   // when user joins a new room, server makes him/her leave the previous room
   socket.leave(previousId);
   socket.join(currentId);
   previousId = currentId;
 };

 socket.on('getGame', gameId =&gt; {
   // join room
   safeJoin(gameId);
   const newGame = {};
   newGame[gameId] = games[gameId];
   // console.log('sending game', newGame);
   socket.emit('game', newGame);
 });
 ...
});

We added a safeJoin function to emulate the effect that a client is leaving a room before entering a new one. Once we have this, viewing a game will also let the client join a room that corresponds to that game. This allows us to send data only to certain clients.

To send data only to users in a specific room, we can use the io.in('...') function. It takes the room name as the parameter and returns an object where you can emit events. In our joinGame and playGame event listeners, add this line:

 socket.on('joinGame', (event) =&gt; {
   if (games[event.gameId].players.length &lt; 2) {
     games[event.gameId].players.push(event.playerName);
   }

   // send new game info to everyone in that room
   const newGame = {};
   newGame[event.gameId] = games[event.gameId];

   io.in(event.gameId).emit('game', newGame);
 });

 socket.on('playGame', (event) =&gt; {
   const gameId = event.gameId;
   const playerName = event.playerName;
   const positionX = event.x;
   const positionY = event.y;

   // position must be empty and players must be 2
   if (games[gameId].state[positionX][positionY] === 0 &amp;&amp; games[gameId].players.length === 2) {
     // player 1 made move
     if (games[gameId].players[0] === playerName) {
       games[gameId].state[positionX][positionY] = 1;
     }
     // player 2 made move
     else if (games[gameId].players[1] === playerName){
       games[gameId].state[positionX][positionY] = -1;
     }

     // send new game info to everyone in that room
     const newGame = {};
     newGame[gameId] = games[gameId];
     io.in(event.gameId).emit('game', newGame);
   }
 });

Now when the joinGame and playGame events fire, the server will also send data to the clients viewing the game that was updated.

Finally, let’s not forget to join a room when we make a new game.

socket.on('addGame', () =&gt; {
   // generate a random string as id
   const gameId = shortid.generate();
   // start with empty players
   const players = [];
   // start with empty state
   const state = [
     [0, 0, 0],
     [0, 0, 0],
     [0, 0, 0]
   ];

   games[gameId] = {
     players: players,
     state: state
   }

   // automatically join room
   safeJoin(gameId);
   const newGame = {};
   newGame[gameId] = games[gameId];
   socket.emit('game', newGame);
   io.emit('games', Object.keys(games));
 });

After everything, we should have a functioning tic-tac-toe app that also uses web sockets to update all clients in real-time.

<div> <img class="img-fluid" src="https://strapi.saperium.com/uploads/websockets4_5a68525bb5.png"> </div>