In this post I will be exploring some utility functions that I wrote as well as some of the logic behind checking the data sent from the client to the websocket server and various other controls. The idea here is that the server should always validate what the client is sending - i.e. we should never trust information from the client as it's easily manipulatable. Imagine a scenario where a player would play a card that is not in their hands, play a card with a different index, or, simply draw 10 cards at the same time as opposed to only 1. These are all factors that had to be taken into consideration and I believe I have come up with solutions against it.
Let's have a look at the most obvious possibility to cheat. Once the cards are assigned to each player, it's fairly easy to change the values of the cards. (Please note that at the moment I load the cards into a multi-select box.) At the beginning of the game there will be one card placed onto the table, and the player who starts the game will have to play a card that either matches a colour or a number.
Note
I had my code reviewed by a friend of mine who is a great JS developer, and after that I have applied a code modification to the isCardPlayable()
function discussed in the previous post. I removed the heavy regular expression ("no need to use a canon against flies") and replaced it with this:
Game.prototype.isCardPlayable = function (card, lastCardOnTable) {
if (card) {
cardNumber = parseInt(card);
cardSuite = card[card.length - 1];
lastCardNumber = parseInt(lastCardOnTable);
lastCardSuite = lastCardOnTable[lastCardOnTable.length - 1];
if (cardNumber === lastCardNumber || cardSuite === lastCardSuite) {
return true;
} else {
return false;
}
} else {
return false;
}
};
I have also replaced the way I create arrays. Before I used var array = new Array();
which is now replaced with var array = [];
.
This solution is much faster. You don't believe me? I wrote a small script to test this:
for (var i = 0; i < 2000000; i++) {
var arr = [];
}
console.timeEnd('squarebrackets');
console.time('newarray');
for (var i = 0; i < 2000000; i++) {
var arr = new Array();
}
console.timeEnd('newarray');
Run this with node test.js
and be amazed by the result:
# node test.js
squarebrackets: 207ms
newarray: 669ms
The reason behind is that by using the square brackets we tell the JavaScript compiler that we want an array and that's it, however with specifying "New Array" there's more lexical analysis needed for the compiler and this will add the extra overhead.
Back to the original problem - we need to make sure that the players can't send falsified/modified information back to the server to gain advantage by doing so. That means, that everytime we call the playCard()
method, we need to verify two things:
Here's the code snippet that achieves this (note, I have removed some irrelevant lines):
On the client-side I have:
socket.emit('playCard', { tableID: 1, playedCard: playedCard, index: index });
And on the server-side:
socket.on('playCard', function (data) {
var playedCard = data.playedCard; //extracting card from data object sent by client
var index = data.index; //extracting card from data object sent by client
var serverIndex = utils.indexOf(player.hand, data.playedCard); //checking whether the index on the server-side matches the client-side
if (index === serverIndex) {
errorFlag = false;
}
if (utils.indexOf(player.hand, data.playedCard) > -1) {
errorFlag = false;
}
});
To achieve the check of indexes and cards I have written - yet another - prototyped object, Utils, which has utility functions that I use throughout my code, one of them is indexOf:
function Utils() {}
Utils.prototype.indexOf = function (array, needle) {
for (var i = 0; i < array.length; i++) {
if (array[i] === needle) {
return i;
}
}
return -1;
};
module.exports = Utils;
And finally, I have extended the Player class with a turnFinished
variable. This is set to a value of true once the player has either played a card or drawn a card - the rules of the game dictate that a player can do one action only. I had to extend the Table class as well with a method:
Table.prototype.progressRound = function(player) {
for(var i = 0; i < this.players.length; i++) {
this.players[i].turnFinished = false;
if(this.players[i].id == player.id) { //when player is the same that plays, end their turn
player.turnFinished = true;
}
}
I iterate through all the players at the table (at the moment, two) and set the turnFinished value to 'true' when the player who plays has his current round. And during either the draw or play functions, I call this function and send in the player as a parameter. Simples.
At this moment the basics of the backend is pretty much complete, I have all the necessary functions and logic in place to be able to play this game with two people. There are a few other things that need implementing, such as adding multiple tables/rooms, allow users to enter a username and last but not least, add some extra functionality for some cards. After that, we'll deal with the frontend.