Part V: Put the update logic server-side

Overview

This is the 5th and last 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
  5. Put the update logic server-side (this post)

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

Goal

In this section, we’ll see how to prevent cheats by putting the update logic server-side. For this, we’ll use Plynd abilities, you do not need to set up servers.

At the end of this post, you’ll have the same behavior as at the end of the last section. However, your code will be secure. We’ll start from /parts/part-4-update-game-state obtained at the end of the previous section.

The problem

In the previous section, we’ve seen how to use

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

in order to update the state of the game.

Specifically we’ve seen that if we call

1
Plynd.updateGame({winnerID:456},{})

The player 456 will be declared as the winner of this game, no matter what the actual state of the game is.

Therefore if somebody opens the developers console and makes this call when it is his turn, he’ll be declared the winner of the game without even playing it.

Plynd solution

In a situation like this, the flow we really want to have is

1
2
3
-> client sends an event (e.g. current player clicks on cell 9) to the server
-> server receives event and validates that this action is allowed (e.g. player has turn, cell is available)
-> server updates the state of the game by calling updateGame and returns validated event to client

In such a flow, the state of the game is never updated directly from the browser, which prevents easy cheats.

To easily allow developers to do so, Plynd has the concept of server functions, which can be configured in your application console

The Server Page is a javascript file containing special functions that can be run from Plynd servers. The setting Block updates from client-side controls whether the function Plynd.updateGame can be called directly from the browser. Thus, the 2 combined can prevent cheat from the browser side.

Basic example - Hello World

Let’s see how this works with a simple example.

You can find the sources for this example alone in /parts/part-5-base-server-functions.
Update your application page in the console to point to http://localhost:3000/parts/part-5-base-server-functions/.

A server function has to be registered into Plynd.ServerFunctions and have the signature

1
function(args, successCallback, errorCallback)

So this is how to declare a simple function “hello” in server.js (within the /js folder):

1
2
3
4
// Very simple function that automatically invokes the success callback with a "hello" message
Plynd.ServerFunctions.hello = function(args, success, error) {
success({message:"Hello " + args.name});
};

All server functions have to be called via Plynd.call with the following arguments

1
Plynd.call(functionName, args, success, error);

In our case, we just want to print out in body the returned message from “hello”:

1
2
3
4
// Call Plynd.ServerFunctions.hello with the args {name:"World"} and print the output
Plynd.call("hello", {name:"World"}, function(output) {
document.write("\"hello\" sent me back \"" + output.message + "\"");
});

The following index.html allows to see it in action:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8">
<title>ServerFunctions</title>
</head>

<body>Loading</body>

<script src='//sandbox.plynd.com/plynd-sdk-0.2-unstable.min.js'></script>
<script src='js/server.js'></script>
<script>
// Call Plynd.ServerFunctions.hello with the args {name:"World"} and print the output
Plynd.call("hello", {name:"World"}, function(output) {
document.body.innerHTML = "\"hello\" sent me back \"" + output.message + "\"";

});
</script>

</html>

Open your application in the Playground and observe the message being printed.

However, in this case, we have included the server.js script within our html page, so the code has been run on the client-side. This ability to run the server functions on the browser during development is extremely to debug there.

Now, upload server.js to your static server. If you use Dropbox (see the previous section), the public link would look something like https://dl.dropboxusercontent.com/u/{your dropbox ID}/TicTacToe/js/server.js.
Whatever the link is, report it into your application console:

Now you can remove the include from your index.html page:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8">
<title>ServerFunctions</title>
</head>

<body>Loading</body>

<script src='//sandbox.plynd.com/plynd-sdk-0.2-unstable.min.js'></script>
<script>
// Call Plynd.ServerFunctions.hello with the args {name:"World"} and print the output
Plynd.call("hello", {name:"World"}, function(output) {
document.body.innerHTML = "\"hello\" sent me back \"" + output.message + "\"";

});
</script>

</html>

Refresh the page and observe that the function “hello” is still returning the proper result. But this time, it was run on Plynd servers!

Set up our server function for TicTacToe

Let’s go back to the code we got at the end of the previous section. If you want to start fresh, you can copy /parts/part-4-update-game-state into /work-directory. Don’t forget to have your application point back to http://localhost:3000/work-directory.

We are going to isolate the update logic from our previous section into ServerFunctions. Specifically we want to validate the events server-side, to prevent cheats like described above.
So in the click handler we are not going to directly call Plynd.updateGame but we will instead invoke a server function.

Let’s add a server.js file in /js/ and include it in index.html. Remember, for development purposes, it is really convenient to load the server functions in the browser at first because that allows you to debug them in the developers console.

Your index.html file should read:

1
2
3
4
5
6
7
8
<html>
...

<script src='//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js'></script>
<script src='//sandbox.plynd.com/plynd-sdk-0.2-unstable.min.js'></script>
<script src='js/tictactoe.js'></script>
<script src='js/server.js'></script>
</html>

In server.js let’s register the function submitEvent:

1
2
3
Plynd.ServerFunctions.submitEvent = function(event, success, error) {
// ...
}

In this function, we want to receive an event (just the description of a cell clicked) and validate that it is allowed before updating the game accordingly.

Something really important to note, is that all serverFunctions must be stateless, which means that they cannot be using any global state. They cannot call any DOM object either. That’s because the state we have on the frontend will not be reproduced on Plynd servers.

In our case, this specifically means that we cannot read either metadata or state defined in tictactoe.js.

Apart from that, server.js is pretty close to what we had in the click handler: we include the 2 functions necessary to evaluate the winning position and simply copy over the logic where we evaluate the state of the game before updating it.

Because we do not want to take anything sent from the client for granted, we check for any incorrect state passed in.
Also we transform the calls to alert we were using in tictactoe.js to calls to the error callback.

Quick note: The code part of error responses is optional and follows HTTP status code conventions.

This is what our submitEvent should look like:

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
Plynd.ServerFunctions.submitEvent = function(event, success, error) {
var cellID = parseInt(event.cell);

// Verify that the cellID is valid
if (cellID < 1 || cellID > 9) {
return error({
code:400,
data:"cell must be an integer between 1 and 9"
});
}

// Need to fetch the most recent and valid state of the game
Plynd.getGame(function(state, metadata) {
var ownPlayer = metadata.ownPlayer;

// Check if the current player is allowed to play
if (ownPlayer.status != "has_turn") {
return error({
code:403,
data:"It's not your turn"
});
}

// Check if this position is already taken
if (state[cellID]) {
return error({
code:403,
data:"This cell is already taken"
});
}

// Update the state with the validated event
state[cellID] = ownPlayer.playerID;

// Create the event to send to everybody
var event = {
cell:cellID
};

// Check if there is a victory or a tie
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 the game a tie
else if (numCellsOccupied == 9) {
event.winnerID = metadata.orderOfPlay.join(",");
}
// Still turns to play, just end this player turn
else {
event.endTurn = true;
}

// Invoke updateGame, and pass it the success callback of submitEvent.
// success will therefore be invoked at the end of the call with [validatedEvent, metadata]
Plynd.updateGame(event, state, success, error);
});
};

Now, in tictactoe.js, we delegate the update logic to the submitEvent function. We still do the checks beforehand though, because we do not want to send events to submitEvent that we already know are invalid.
The logic in the click handler now becomes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Register the click handler
$("#board td").on("click", function() {
var cellID = $(this).data("magicnumber");

// Do some basic validation on the client side
if (metadata.ownPlayer.status != "has_turn") {
return alert("It's not your turn");
}

if (state[cellID]) {
return alert("This cell is already taken");
}

// Delegate the update logic to submitEvent
Plynd.call("submitEvent", {cell:cellID}, onEvent);
});

The functions checkVictory and hasMagicSquare can be removed from tictactoe.js.

If you open the Playground, you should see that everything works as it used to, although now all the logic is isolated into submitEvent.

Block update calls from the browser

We have isolated the logic to update the game in a stateless function that can be run server side.

The same way as we did for our Hello World example above, you can now upload the final server.js file to your static file server:

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
Plynd.ServerFunctions.submitEvent = function(unvalidatedEvent, success, error) {
var cellID = parseInt(unvalidatedEvent.cell);

// Verify that the cellID is valid
if (cellID < 1 || cellID > 9) {
return error({
code:400,
data:"cell must be an integer between 1 and 9"
});
}

// Need to fetch the most recent and valid state of the game
Plynd.getGame(function(state, metadata) {
var ownPlayer = metadata.ownPlayer;

// Check if the current player is allowed to play
if (ownPlayer.status != "has_turn") {
return error({
code:403,
data:"It's not your turn"
});
}

// Check if this position is already taken
if (state[cellID]) {
return error({
code:403,
data:"This cell is already taken"
});
}

// Update the state with the validated event
state[cellID] = ownPlayer.playerID;

// Create the event to send to everybody
var event = {
cell:cellID
};

// Check if there is a victory or a tie
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 the game a tie
else if (numCellsOccupied == 9) {
event.winnerID = metadata.orderOfPlay.join(",");
}
// Still turns to play, just end this player turn
else {
event.endTurn = true;
}

// Invoke updateGame, and pass it the success callback of submitEvent.
// success will therefore be invoked at the end of the call with [validatedEvent, metadata]
Plynd.updateGame(event, state, success, error);
});
};

///////////////////////////////////////////////////////////////////////////////////////////
// Win evaluation
///////////////////////////////////////////////////////////////////////////////////////////

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;
}

and report the link URL into your application console. Also, check the box that reads “Block updates from client-side”:

Now try again your application in the Playground. You’ll note that it does not work: That’s because the server.js is loaded in the browser, and therefore the function is being called from there, which we have just explicitly blocked.

So the last thing we have to do is remove the include of server.js from index.html. This time, because the function is not defined in the scope of the browser, it will run on Plynd servers.

And voila! Your update logic is being run safely on Plynd servers, which prevents basic cheats!

Summary

In this post, we’ve seen:

  • why it is important to block the raw update of the game state from the browser
  • how to register some functions in Plynd.ServerFunctions so they can be run on Plynd servers
  • how to configure your game so that the functions are run on the server and cheap cheats are prevented

Final word

This was the last part of our series covering how to implement TicTacToe.

We really hope you’ve enjoyed it! It covers everything you need to know in order to implement your next asynchronous game on Plynd.
No more needs for servers, no more needs for a long registration flow. It’s all about implementing the game play and design!

Feel free to contact us via the Comments section or info@plynd.com, we’d love to hear from you!