Realtime Infrastructure Made Simple

Live event updates for your web app with our Simple Notification Service

--

Recently we introduced the Simple Notification Service, a quick and easy realtime platform that lets you focus on actually building features rather than worrying about infrastructure.

Today we want to show you an example of how you might build an online, multiplayer game using the Simple Notification Service.

The game we're going to create is a classic, Noughts and Crosses. You may know it as Tic-tac-toe.

You can see a working example of this game here.

Ingredients

So what goes into building something like this? Well, as always there are a number of ways to do this kind of thing. For this particular example, you need the following:

Bootstrap helps us quickly put together a front-end for our game without having to write too much CSS. VueJS provides a nice gateway into the many JavaScript frameworks and includes a number of nice features like data bindings, components, and more.

If you are unfamiliar with any of these technologies, I recommend giving them a quick look over before continuing. They all come with great documentation, and you should be able to get started in no time!

All of the code covered in this article is available here.

Get started

First, we build a simple Node.JS app, running Express, to help us serve some web pages and host a couple of API calls. You can start a new project by creating a new directory, and running npm init:

Answer the setup questions, which create your package.json file.

Next, in that same directory, you create a file called app.js, which is the Node.JS app that we are going to build. Let’s take a look at some of the code we need here.

At the very top of app.js we define all of our modules and so forth. You can add these modules by running the associated npm install command and they get installed and included in your package.json.

One thing to point out is that we are creating a variable to store our “games” in, games. This is in lieu of using a database to store this kind of information, just to simplify things!

At the very bottom of app.js, we then do:

Here we define a few different routes that serve out some HTML from the public directory. You need to create this directory along with a few other directories for all of the individual JavaScript, CSS, and image resources. You also need to create two files: index.html and game.html in the public directory.

In app.js, between these two chunks of code, we’ll add a few API endpoints, which I’ll cover in just a moment.

So, at this point we have a very simple Node.JS app that does almost nothing except serve out some HTML on the / and /game/:id endpoints.

Creating the API

Before we build anything on the front-end, we should finish off our back-end and create those API endpoints I mentioned.

Take the code covered in the next few sections and place it in app.js between the two pieces of code just discussed. As a model, you can refer to my app.js file on GitHub.

Creating a new game

This endpoint creates a new game object within our existing games object, with its own ID. We use this object to record the players that are currently connected to this particular game.

Adding a player to a game

This endpoint adds a new player ID to the game specified, and then returns the current details of this particular game. This helps us do the initial sychronisation between players at the start of a game.

Removing a player from a game

Similarly, this endpoint removes a player from a specified game and returns the current details of the game. Again it helps synchronise player details on new connections.

Homepage

The first thing a user sees when they arrive at our app is the homepage. From here we want to be able to either create a new game, or join an existing game.

This file is/public/index.html, and you can see all of the code for this page here.

I would like to point out a few notable things, however.

We need to include a number of CSS and JavaScript libraries.

Here we have our Bootstrap CSS and JavaScript, and a custom.css file with some of our own styles in it. We also include jQuery, and VueJS, along with Vue Resource which is a plugin for Vue that lets us make HTTP requests.

In index.html, we also introduce the Vue framework itself.

In the app variable, we define a new Vue instance. Within this instance, we define the HTML element that our Vue instance relates to: el: '#app'. We can also define a number of methods that we want to make available to our Vue instance: in this case, startGame and joinGame.

We can now refer to these methods from within our HTML. Here’s an example showing how you might invoke the startGame method:

Game page

One of our routes (GET /game/:id) is what we use to access individual games. This page renders the game components and manages all of the game mechanics, including the communication between players via the Simple Notification Service.

This file is /public/game.html, and you can see all of the code for this page here.

We include the same JavaScript libraries as on the homepage, but we also include two more files at the bottom of the page.

game.js

This file contains all of the Vue-related JavaScript, and defines all of the game mechanics.

We introduce the new data object when defining our Vue app. Let’s take a look at the different data points we have:

  • gameID - a reference to the ID of this game, derived from the URL
  • turn - whose turn it is (o, or x)
  • status - the status of this game (x win, o win, draw, or nothing)
  • players - an object that defines the SNS ID of each player
  • me - is this player o or x
  • check - reference to an audio clip, for sound effects
  • squares - an object with nine sub-objects, that show whether a square has a o or an x in it
  • logs - an array of log data
  • hideLogs - do we hide the logs on the front-end, or not?

We also define a number of methods that perform different actions within the game.

  • takeTurn - called when a square is clicked, try to take a turn
  • isSquareOccupied - is this square currently occupied?
  • checkWinners - do we have any winning combinations?
  • checkDraw - are there no more turns to take?
  • getCombinations - helper function, gets all of the different combinations of played squares
  • reset - reset the game, ready to play again
  • cancel - go back to the homepage
  • getAvailablePlayer - talk to the API and attempt to assign this user as a player. Also returns current players.
  • whoAmI - determine which player this user is, if any
  • quit - call the API and remove this player from the game, also returns current players
  • toggleLogs - show/hide the logs on the front-end

All of these data points and methods can be accessed via the app variable.

sns.js

This file contains all of the specific Simple Notification Service-related JavaScript and refers to the Vue app to perform game related actions.

We need to connect to the Simple Notification Service, handle player connections and disconnections, and update all users when a player makes a move. In short, any interactions between players are all funneled through the Simple Notification Service, and through the sns.js file.

Connecting

To begin with, we simply need to connect to the Simple Notification Service. We define our userData, passing through the gameID from our Vue app. The userData describes this particular user to the Simple Notification Service, and allows other users to interact with this user.

Our userQuery is going to be the same. The userQuery describes other users that this user wants to receive connection and disconnection notifications about. For our game, we want to hear about users that are playing the same game.

The code to connect to the Simple Notification Service is shown below:

Once we are connected, we want to talk to the API to assign this user as a player within the game. This is done by using the connected event and the getAvailablePlayer method.

Here, we are saying to the Noughts & Crosses API (POST /game/:id/register/:playerid) that we want to register a particular player (identified by the sns.id) as a player in this game. Thissns.id is assigned to any available playing space (either as o or x), and the current players are returned.

The response from this API call will be something like:

The Simple Notification Service then sends this data out to all users associated with this game, by using the sns.send() method. This method lets a user send a notification to any other Simple Notification Service user, along with some data.

When players receive this event, they use the data to update their local Vue instance with the correct player details.

In this instance, we are wanting to update any user who is currently viewing our game. So we define our userQuery as {gameID: app.gameID}. In our data payload, we define an action (playersync), and some data (which is our response from the API above, detailing which players are assigned to which team). This notification is received by all users of the Simple Notification Service who match this userQuery, including the originating user.

Receiving Notifications

To react to receiving this notification, we must implement the notification event within Simple Notification Service.

The first thing we do here is put this data into the app.logs array to render on the front-end. This is just something that was added to help visualise the data flow within the application.

Next, we use a switch statement on the n.action property, and define what we want to happen for the playersync action.

You can see here that first we update the app.players data point with the response we received from the POST /game/:id/register/:playerid API call we made on first connection. Then we call app.whoAmI, passing in our local sns.id. This lets us update the app.me data point.

This means that, at this point:

  • The Noughts & Crosses app has a record of our game, and the user assigned to each player
  • All connected users now have an up-to-date record of which user is assigned to which player
  • Each user also has a local record of which player they are (if applicable)

Playing the game

So we have our players — now we want to play the game!

Each of our game squares makes a reference to the app.takeTurn() method defined on our Vue instance.

When a square is clicked, we call the app.takeTurn() method, passing in the square ID (3, in the example above). This method makes sure that:

  • we have 2 players
  • it is the correct player’s turn
  • this square is not occupied

If any of these conditions are not met, we simply return and do nothing. Otherwise, we mark this square as occupied by whichever player has clicked, and send out a notification to all users, with an action value of turn. We also send the square ID and player details.

We can add a new “case” to our switch statement in sns.js to handle this action:

Each player will receive the turn data, and then we can synchronise all of our players.

First up we update the Vue instance to mark this square as occupied by the player that made the move. We also play a sound effect — just for fun!

Next, we need to check to see if we have a winner, using app.checkWinners(), passing in the player whos turn it is (app.turn, either o or x). If we have a winner, we update the app.status, alternate app.turn, and display the modal that lets us reset the game.

If there’s no winner, we then check for a draw — which is essentially checking to see if all of the squares have been played. The result is very similar — update the app.status, alternate app.turn, show the modal.

If there’s no need to end the game, we simply alternate app.turn and we're ready to play the next move!

Ending the game

There are three different ways to end the game, either a win for one player, all of the spaces being played and therefore a draw, or a player leaves — ending the game early.

In each instance, we display a modal — one of the features of Bootstrap. From here, we can either quit the game or reset, ready to play again.

If we hit the reset button, we call the app.reset() method, which sends a notification to all players:

Again, we handle this in our switch statement, synchronising all players:

We loop through all of our app.squares, setting them to be unoccupied and we update the app.status back to empty. This means we are ready to start again.

If we were to hit the cancel button, we send a slightly different notification to our players via the app.cancel() method:

Handling this in a similar way, simply redirecting all the players to the homepage.

In conclusion

The Simple Notification Service allows for the transmission of data between users (or, between browsers) and is ideally suited for something like Noughts & Crosses.

The main purpose of the Simple Notification Service in this application is to synchronise the game data between users by sending notifications whenever a user performs an action, but this approach can be applied to almost any realtime application.

The key is to only apply local data changes when they are received via the Simple Notification Service. This means that changes are only applied when all users receive them — and there are no assumptions being made on the part of any user. It is based on the actual data.

You can use the Simple Notification Service to power almost any realtime feature or application using this approach.

--

--