Skip to main content

Online Card Game with Node.js and Socket.io – Episode 4

4 min read

Older Article

This article was published 13 years ago. Some information may be outdated or no longer applicable.

In this post, I’ll cover some utility functions I wrote, plus the logic behind validating data sent from the client to the websocket server (and various other controls). The core principle: the server should always validate what the client sends. Never trust information from the client, because it’s easily manipulated. Imagine a player playing a card that’s not in their hand, playing a card with the wrong index, or drawing 10 cards at once instead of 1. These are all scenarios I had to account for, and I think I’ve found working solutions.

Let’s look at the most obvious way to cheat. Once cards are assigned to each player, it’s fairly easy to tamper with the values. (Right now I’m loading the cards into a multi-select box.) At the start of the game, one card is placed on the table. The first player has to play a card matching either the colour or the number.

Note

A friend of mine (a strong JS developer) reviewed my code. After that, I reworked the isCardPlayable() function from the previous post. I dropped the heavy regular expression (“no need to use a cannon 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’ve also swapped how I create arrays. Before, I used var array = new Array();. Now it’s var array = [];.

This is faster. Don’t believe me? I wrote a small script to test it:

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 it with node test.js and see for yourself:

# node test.js
squarebrackets: 207ms
newarray: 669ms

The reason: square brackets tell the JavaScript compiler “give me an array” and that’s it. With new Array(), the compiler needs more lexical analysis, which adds overhead.

Back to the original problem. We need to make sure players can’t send falsified information to the server for an advantage. Every time we call playCard(), we verify two things:

  1. The player actually has the card in their hand
  2. The index of the card matches the index stored on the server side

Here’s the code that does this (I’ve removed some irrelevant lines):

On the client side:

socket.emit('playCard', { tableID: 1, playedCard: playedCard, index: index });

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 check indexes and cards, I’ve written another prototyped object: Utils. It holds utility functions I use throughout the 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;

I’ve also extended the Player class with a turnFinished variable. This flips to true once a player has either played or drawn a card (the rules say you get one action per turn). I had to extend the Table class as well:

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 players at the table (currently two), and set turnFinished to true for the player whose turn it is. During both the draw and play functions, I call this and pass in the player as a parameter. Simple.

At this point, the backend basics are essentially complete. I’ve got the necessary functions and logic in place for two people to play the game. A few things still need building: multiple tables/rooms, letting users enter a username, and adding special functionality for certain cards. After that, it’s on to the frontend.