Part IV: Update the game state with events

Overview

This is the 4th post of a 5 parts series describing how to create a fully multiplayer TicTacToe game on Plynd:

  1. Setup and publish the skeleton of your application
  2. Setup the development environment
  3. Read the game state and display information about the players
  4. Update the game state with events (this post)
  5. Put the update logic server-side

Don’t hesitate to ask all your questions via the comments section or directly to info@plynd.com

Goal

In this section, we’ll explain how to update the game state with an event-based approach.

At the end of it, we’ll have a playable version of our TicTacToe game, synchronized in realtime with Plynd servers.

We’ll start from /parts/part-3-read-game-state obtained at the end of the previous section.

The event-based approach

In the previous section we’ve seen how the information about a game is split between the state and the metadata.

In order to update the game state, the SDK offers the function

1
Plynd.updateGame(event, state, successCallback, errorCallback)

As we see, the signature enforces to pass in an event, and the resulting state of the game:

  • the state will simply overwrite the previous saved state and be returned by any next call to Plynd.getGame.
  • the event will stack onto all the previous events that occurred in the game. This allows to keep track of the sequence of changes in order.
    All events are numbered by Plynd backend, and the game is always signed with the eventNumber n of the last event that occurred.
    An event should contain enough information so that if a client knows the game state at the stage n and receives the event n + 1, it is able to reconstruct the game state at n + 1.

The successCallback has to have the signature

1
2
3
successCallback = function(validatedEvent, updatedMetadata) {
// ...
}

State and event representation for TicTacToe

TicTacToe is a simple game, so we’ll use a simple model:

  • All cells in the game are indexed from 1 to 9, with a specific ordering. You can see the ordering in index.html - the attribute data-magicnumber of the td elements - and the reason behind this ordering is available in this article.
  • state is a map {cell -> playerID}. At first all cells are empty, so the keys will not be defined and the state be {}.
  • In the events, we will just describe in which cell the player having turn has played.

For instance, at the eventNumber 2, a valid game state between the player 123 and 456 could be:

1
2
3
4
{
4:123,
6:456
}

Then if the player 456 decides to play on the cell 8, the event would be

1
2
3
4
{
cell:8,
playerID:456
}

Which would allow to reconstruct the state at stage n + 1:

1
2
3
4
5
{
4:123,
6:456,
8:456
}

Show the state on the board

In our previous example, at the stage 2, we’d want to have one “X” on the cell 6, and one “0” on the cell 4.
Let’s implement the function showBoard that does just that: it loops over all the cells, and prints the symbol of the player that occupies each:

1
2
3
4
5
6
7
8
9
10
// Show the game state
function showBoard() {
for (var cell = 1; cell <= 9; cell++) {
var playerOnCell = state[cell];
if (playerOnCell) {
var cellDiv = $("td[data-magicnumber=" + cell + "]");
cellDiv.html(getPlayerSymbol(playerOnCell));
}
}
}

Of course, at this point, the state is simply {} because there have been no events yet. Let’s fake some data right after the fetch of the game in order to test our function:

1
2
3
4
5
6
7
8
9
10
11
Plynd.getGame(function(_state, _metadata) {
state = _state;
metadata = _metadata;

// Fake some state (as in our example, the first player occupies the cell 4 ; the second player occupies the cell 6)
state[4] = metadata.orderOfPlay[0];
state[6] = metadata.orderOfPlay[1];

showPlayers();
showBoard();
});

Open the game, you should now see this

Send events to the backend

Now that we are able to show the state, let’s update it when a player clicks on a cell. We want to do 2 checks beforehand:

  • only the player having its turn should be able to play
  • the cell must be empty for the click to be valid

Let’s add a click handler on td within getGame

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$("#board td").on("click", function() {
// Read the ID of the cell clicked
var cellID = $(this).data("magicnumber");
var ownPlayer = metadata.ownPlayer;

// Validate that it's this player turn
if (ownPlayer.status != "has_turn") {
return alert("It's not your turn");
}

// Validate that the cell is free
if (state[cellID]) {
return alert("This cell is already taken");
}

return alert("Would save click on cell " + cellID);
});

Now if you go back and forth between the 2 players tabs in the Playground, you should be able to test that the rules are correctly enforced.

Note: when you want to test your changes, remember to refresh the page first.

This time, let’s complete the click handler with an actual update of the game and see what the SDK returns to us. Replace:

1
return alert("Would save click on cell " + cellID);

with

1
2
3
4
5
6
7
state[cellID] = ownPlayer.playerID;
Plynd.updateGame({cell:cellID}, state, function(event, metadata) {
// Show the validated event
$("body").append($("<div/>", {
text:JSON.stringify(event)
}));
});

Let’s also remove the 3 lines:

1
2
3
// Fake some state (as in our example, the first player occupies the cell 4 ; the second player occupies the cell 6)
state[4] = metadata.orderOfPlay[0];
state[6] = metadata.orderOfPlay[1];

Test this new function by executing a valid click on a cell. As you see, the event comes back enriched from the SDK. It gets signed with eventNumber and the playerID of the current player is automatically added to it.

This currently does not update the display of the game though. But if you refresh the page, you should see the event that was just saved is now taken into account.

Let’s create a better successCallback onEvent that actually updates the display:

1
2
3
4
function onEvent(event, _metadata) {
state[event.cell] = event.playerID;
showBoard();
}

To call it, simply replace:

1
2
3
4
5
6
Plynd.updateGame({cell:cellID}, state, function(event, metadata) {
// Show the validated event
$("body").append($("<div/>", {
text:JSON.stringify(event)
}));
});

with

1
Plynd.updateGame({cell:cellID}, state, onEvent);

This gives the following for /js/tictactoe.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// Keep the game state and the metadata globally so
// that they can be used in all the different functions
var state, metadata;

///////////////////////////////////////////////////////////////////////////////////////////
// The functions to update the UI according to the game state and metadata
///////////////////////////////////////////////////////////////////////////////////////////

// The first player has the symbol "X"
// The second has the symbol "O"
// We use metadata.orderOfPlay to know who is the first player
function getPlayerSymbol(playerID) {
if (playerID == metadata.orderOfPlay[0]) return "X";
return "O";
}

// Show the players, based on the metadata object
function showPlayers() {
// metadata contains a lot of information, among which
// * metadata.players contains the info about all the players in the game, given by their ID
// * metadata.ownPlayer contains the info about the player with the point of view on the game
// * metadata.orderOfPlay is the list of the player IDs in the game
var players = metadata.players;
var ownPlayer = metadata.ownPlayer;
var orderOfPlay = metadata.orderOfPlay;

for (var i = 0; i < orderOfPlay.length ; i++) {
var playerID = orderOfPlay[i];
var player = players[playerID];

// Select the left div for "ownPlayer", the right one for the other
var playerDiv;
if (playerID == ownPlayer.playerID) {
playerDiv = $('#player-left');
}
else {
playerDiv = $('#player-right');
}

// Show the name, the status and the player image
playerDiv.find('img').prop('src', player.user.picture);
playerDiv.find('.player-name').text(player.playerName);
playerDiv.find('.player-status').text(player.status);
playerDiv.find('.player-symbol').text("(" + getPlayerSymbol(player.playerID) + ")");
}
}

// Show the game state
function showBoard() {
for (var cell = 1; cell <= 9; cell++) {
var playerOnCell = state[cell];
if (playerOnCell) {
var cellDiv = $("td[data-magicnumber=" + cell + "]");
cellDiv.html(getPlayerSymbol(playerOnCell));
}
}
}

///////////////////////////////////////////////////////////////////////////////////////////
// The hook to react to events happening on the game
///////////////////////////////////////////////////////////////////////////////////////////

function onEvent(event, _metadata) {
state[event.cell] = event.playerID;
showBoard();
}

// getGame is the entry point.
// There we initialize state and metadata
Plynd.getGame(function(_state, _metadata) {
state = _state;
metadata = _metadata;

showPlayers();
showBoard();

// Register the click handler
$("#board td").on("click", function() {
// Read the ID of the cell clicked
var cellID = $(this).data("magicnumber");
var ownPlayer = metadata.ownPlayer;

// Validate that it's this player turn
if (ownPlayer.status != "has_turn") {
return alert("It's not your turn");
}

// Validate that the cell is free
if (state[cellID]) {
return alert("This cell is already taken");
}

state[cellID] = ownPlayer.playerID;
Plynd.updateGame({cell:cellID}, state, onEvent);
});
});

Special keywords for events

If you try the above code, it is going to give you a weird game play. Only the player who has the turn when the game starts is able to play, and he can play as many times as he wants.
That’s because we are not declaring at any point that an action (e.g. occupying a cell) should also end a player’s turn.

Luckily this is made really easy in Plynd, with the use of special keywords in events. The different keywords are:

Keywords Accepted values Meaning
endTurn true the event marks the end of the current player’s turn
nextPlayerID comma-separated list of playerIDs sets the players passed in as having their turn
winnerID comma-separated list of playerIDs sets the players passed in as winner and the others as defeated
eliminatedID comma-separated list of playerIDs sets the players passed in as eliminated

Note: See the previous section for the description of the different player statuses.

In our case, we want to specify that the event marks the end of the turn for the player. Replace the call to updateGame with:

1
Plynd.updateGame({cell:cellID, endTurn:true}, state, onEvent);

This will have the effect of changing the metadata for the game. We can read the updated metadata from the successCallback.
Within onEvent we simply have to update our local copy of metadata and refresh the display of the players as well:

1
2
3
4
5
6
7
8
9
function onEvent(event, _metadata) {
// Copy the updated metadata in our local object and refresh the display of players
metadata = _metadata;
showPlayers();

// Update the state from the validated event and refresh the display of the board
state[event.cell] = event.playerID;
showBoard();
}

Now we have a playable version of our TicTacToe game: Each player has to play one after the other.
There is still an issue though: the game does not end and no-one ever wins.

Evaluating the end of the game

We need to check 2 more things before we send the event in updateGame:

  • whether there is a winning position for the player that just played. In that case we’ll specify the current player in winnerID
  • otherwise, whether all the cells are occupied, in which case we’ll specify all the players in winnerID

The evaluation of a winning position is outside the scope of this tutorial, but you can find some explanations for it in this article. We’re just going to add the 2 following functions in tictactoe.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function checkVictory(playerID, state) {
var cellsOccupiedByPlayer = [];
for (var cell = 1; cell <= 9; cell++) {
if (state[cell] == playerID) {
cellsOccupiedByPlayer.push(cell);
}
}

return hasMagicSquare(cellsOccupiedByPlayer);
}

// Logic taken from http://fr.mathworks.com/moler/exm/chapters/tictactoe.pdf
function hasMagicSquare(cells) {
for (var i = 0; i < cells.length; i++) {
for (var j = 0; j < cells.length; j++) {
for (var k = 0; k < cells.length; k++) {
if (j == k || i == k || i == j) continue;
if (cells[i] + cells[j] + cells[k] == 15) return true;
}
}
}
return false;
}

Now we can use the following logic just before we send the event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Prepare the event to send
var event = {cell:cellID};

// Count the number of cells occupied
var numCellsOccupied = Object.keys(state).length;

// This is a winning move
if (checkVictory(ownPlayer.playerID, state)) {
event.winnerID = ownPlayer.playerID;
}
// No more cells available - declare all the players as winners
else if (numCellsOccupied == 9) {
event.winnerID = metadata.orderOfPlay.join(",");
}
// There are still turns to play, just end this player's turn
else {
event.endTurn = true;
}

Plynd.updateGame(event, state, onEvent);

We are also going to display a popup when the game is over.
To do so, we simply need to consume metadata.status which returns the status of the game. It can have only 2 values

Status Meaning
game_is_active the game is active, some of its players can play
game_is_over the game is over, all of its players should either have a defeated or winner status

Let’s consume this status in onEvent:

1
2
3
4
// Check if the game is over
if (metadata.status == "game_is_over") {
alert("The game is over!");
}

Voila! this time we have a fully working TicTacToe game! The state is saved in Plynd servers, so you can resume any game at any time.
Note: In order to test the full logic in a complete game, you might want to create a new playground (to start from a fresh state).

Make it realtime!

For the moment, we are only receiving back the events that are sent (in the callback of the function updateGame). So if events are triggered in another window, the clients will not be aware of it in any of the other windows open.
You can do the experience for yourself by clicking “Open full tab” for each of the 2 players. (see the video at the top of this post at 0:20 if you don’t see what we are talking about). The board does not auto-update.

For this purpose, Plynd SDK provides a realtime utility. You only have to register a callback to the realtime channel, and every window will be aware of all the events occurring in the game.
You can register to the realtime channel by using:

1
Plynd.Realtime.onEvent(callback);

The callback has to have the signature

1
2
3
eventCallback = function(event, metadata) {
// ...
}

… which is exactly the same signature as for the successCallback in updateGame.

Therefore, getting the update for all the events in the game is extremely simple. Just register to the realtime channel when the game is loaded:

1
Plynd.Realtime.onEvent(onEvent);

This gives our final, working version for our TicTacToe game: /parts/part-4-update-game-state/js/tictactoe.js.

Publish your application

So far, the sources that you have modified are only available from your localhost.

Now to make it available worldwide, all you need to do is host your static assets somewhere:

1
2
3
4
5
6
/TicTacToe
index.html
/css
style.css
/js
tictactoe.js

There are many ways to host static files online. The simplest way we’ve found is probably using Dropbox.
If you save the repository TicTacToe into your Public folder in Dropbox, you just need to replace your Application page with https://dl.dropboxusercontent.com/u/{your dropbox ID}/TicTacToe/ and you’ll be all set!

Summary

In this post, we’ve seen:

  • how to use updateGame to save the actions happening in the game
  • how state can be of any kind and any form (just any JSON blob), and contains the info we need for the representation of a game
  • how the event-based approach allows to communicate simply with Plynd backend, update the metadata of the game, and make it realtime
  • how to make your application available online by using any static file hosting

Now we’ll see how to prevent cheats by putting some of the logic server-side. Don’t worry, it’s all going to be on Plynd servers, you will not need to deploy any server yourself!