Skip to main content

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

8 min read

Older Article

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

I’ve been busy these past weeks rewriting backend functions and adding more rules to the game. What I had before was a working but simplified version that ignored various types of action cards. Now I’ve finally added one action card and I’m working on the rest. Time (or the lack of it) keeps me from moving faster, so thanks to everyone still following along for your patience.

The game allows a number of action cards: “2”, “Ace” and “King”. Every country plays with slightly different rules, so let me explain how my version works:

  • 2: The first player plays “2”, then the next player must take two cards from the pack, unless they’ve got another “2”. If they do, they can play it, and the next player must take four cards. The penalty keeps stacking by two until someone either draws or has no “2” card left to play.
  • Ace: Playing an “Ace” lets you request a suit (Hearts, Spades, Clubs or Diamonds). The next player has to play a card of that suit unless they drop another “Ace” to override the request. If a player has neither an “Ace” nor the requested suit, they draw from the pack.
  • King: Same idea as the Ace, but instead of requesting a suit, you request a number (2 to 10 and “Ace”, “Jack”, “Queen” and “King”). Another “King” can override the request.

Coding these requests was tricky enough on its own, but the game also encourages cheating. Say you’ve got an “Ace” in your hand and could override someone’s request. You might choose to draw a card instead, keeping the “Ace” for later. I can’t force people to play a particular card. Same goes for the “King”: you might request “8” because you suspect the next player also has a “King” and would override your request, possibly landing on a number you actually hold. The player’s choice has to stay open.

Let’s look at the only rule I’ve implemented so far: the “2”. Two scenarios needed handling.

At the start of the game

The first card placed automatically onto the table is completely random and could be a “2”, so:

  • Check the first card on the table and determine whether it’s an action card
  • If it’s an action card, determine whether it’s a penalising action card (“2”) or a request action card (“Ace” or “King”). I’m only covering the penalising card here.
  • If the starting player (randomly selected) has no “2” in their hand, I force the draw and end their turn. The action card “flag” gets removed, meaning the next player can play freely (following the number-to-number, suit-to-suit rule explained here).
  • If the player has a “2”, they can either take two cards from the pack or play their “2”. Taking the penalty cards removes the action card flag.
  • If the player plays their “2”, the next player’s hand is checked. If they’ve got a “2” they can play it or draw 4 cards, and so on.

During gameplay

Same logic as above.

Right, enough talking. Let’s look at code. First, I extended the variables in table.js with fields for action cards: ‘actionCard’, ‘requestActionCard’, ‘penalisingAction’, ‘forcedDraw’, ‘suiteRequest’ and ‘numberRequest’. The file now looks like this (it still has all the methods from the previous 5 articles):

function Table(tableID) {
  this.id = tableID;
  this.name = '';
  this.status = 'available';
  this.players = [];
  this.playersID = [];
  this.readyToPlayCounter = 0;
  this.playerLimit = 2;
  this.pack = [];
  this.cardsOnTable = [];

  this.actionCard = false;
  this.requestActionCard = false;
  this.penalisingActionCard = false;
  this.forcedDraw = 0;

  this.suiteRequest = '';
  this.numberRequest = '';

  this.gameObj = null;
}

I’ve also added a function to server.js called ‘preliminaryRoundCheck’. Here’s the full code:

socket.on("preliminaryRoundCheck", function(data) {
console.log("preliminary round check called.");
var player = room.getPlayer(socket.id);
var table = room.getTable(data.tableID);
var last = table.gameObj.lastCardOnTable(table.cardsOnTable); //last card on Table
console.log('Last card on table ==>' + last);

  if (table.gameObj.isActionCard(last) && table.actionCard) { //Is the card on the table an action card?
    if (table.gameObj.isActionCard(last, true)) { //Is the first card on the table a penalising card? (2*) (checked by the true flag)
      table.forcedDraw += 2; //add 2 cards to the forcedDraw function
      table.penalisingActionCard = true;
      console.log("FORCED DRAW ==>" + table.forcedDraw);
      console.log("it's a penalising card");
      if (table.gameObj.isInHand(last, player.hand)) { //Does the starting player have a response in hand?
        console.log("I have a 2, optionally i can play it"); //GIVE OPTIONS
        socket.emit("playOption", { message: "You have a 2 card in your hand, you can either play it or take " + table.forcedDraw + " cards.", value: true}); //OPTION - TRUE
      } else {
        console.log("no 2 in hand, force me to draw"); //No penalising action card in hand, force draw
        console.log("HAND ==> " + player.hand);
        socket.emit("playOption", { value: false }); //OPTION - TRUE
        table.gameObj.drawCard(table.pack, table.forcedDraw, player.hand, 0);
        socket.emit("play", { hand: player.hand }); //send the card in hands to player
        io.sockets.emit('updatePackCount', {packCount: table.pack.length});
        table.forcedDraw = 0; //reset forced draw variable
        table.actionCard = false; //set the action card to false
        table.penalisingActionCard = false; //reset the penalising action card variable
        /*PROGRESS ROUND*/
        table.progressRound(player); //end of turn
        socket.emit("turn", {myturn: false}); //end of my turn
        messaging.sendEventToAllPlayersButPlayer("turn", {myturn: true}, io, table.players, player); //add turn to next player
        messaging.sendEventToAllPlayersButPlayer("cardInHandCount", {cardsInHand: player.hand.length}, io, table.players, player); //update cards count for other players
      }
    } else { //Is the first card on the table a request card (1*, 13*)
      console.log("it is a request card, player to make a request"); //SHOW REQUEST WINDOW
        //TO BE IMPLEMENTED LATER
    }
  } else { //The first card on the table is not an action card at all
    console.log(last + " is not an action card or we don't care about it anymore");
  }
});

The key takeaway from that code: watch how the actionCard variable and the forcedDraw variable get set and read. Every time someone plays a “2”, table.forcedDraw += 2; fires.

You’ll notice two new methods, ‘isActionCard’ and ‘isInHand’, both in game.js:

/* checking if card is an action card */
Game.prototype.isActionCard = function (card, penalising) {
  penalising = typeof penalising === 'undefined' ? false : penalising;
  if (card && !penalising) {
    var cardNumber = parseInt(card);
    console.log(cardNumber);
    if (cardNumber in utils.has(['1', '2', '13'])) {
      return true;
    } else {
      return false;
    }
  }
  if (card && penalising) {
    var cardNumber = parseInt(card);
    if (cardNumber === 2) {
      return true;
    } else {
      return false;
    }
  }
};

Game.prototype.isInHand = function (card, hand) {
  //checks whether there's a card in our hand
  if (card) {
    cardNumber = parseInt(card);
    //parse numbers in hand
    var numbersInHand = [];
    for (var i = 0; i < hand.length; i++) {
      numbersInHand.push(parseInt(hand[i]));
    }
    if (utils.indexOf(numbersInHand, cardNumber) > -1) {
      return true; //I can play a card if I want to
    } else {
      return false; //I can't play, force me to draw.
    }
  }
};

I’ve also added a function that forces a player to play a penalising action card on top of another penalising action card. In other words, a player has to play “2” if there’s a “2” on top of the pile (and the actionCard variable is set to true):

Game.prototype.isPenalisingActionCardPlayable = function(card, lastCardOnTable) {
  if (card) {
    var cardNumber = parseInt(card);
    var lastCardNumber = parseInt(lastCardOnTable);
    if (cardNumber === 2 && lastCardNumber === 2) {
        return true;
    } else {
      return false;
    }
  }
}

I mentioned this function in an earlier post, but it’s worth revisiting. The force draw calls the regular ‘drawCard’ function from game.js but passes a different parameter for the ‘amount’:

Game.prototype.drawCard = function (pack, amount, hand, initial) {
  var cards = [];
  cards = pack.slice(0, amount);
  pack.splice(0, amount);
  if (!initial) {
    hand.push.apply(hand, cards);
  }
  return cards;
};

In action (see the ‘penalisingTaken’ function below), notice that I’m passing the table.forcedDraw variable (which holds the count of how many “2” cards were played):

table.gameObj.drawCard(table.pack, table.forcedDraw, player.hand, 0);

On the front-end there’s a “Draw a card” button that pulls one card from the pack. When a player plays “2” and the next player also holds a “2”, a new button appears labelled “Penalising cards”. That gives you two options: play your “2” or take two cards. This is triggered by socket.emit("playOption", { value: true }); from the code above. The front-end code looks like this:

socket.on('playOption', function (data) {
  $('#playOption').html(data.message);
  if (data.value) {
    $('#penalising').show();
  } else {
    $('#penalising').hide();
    $('#playOption').hide();
  }
});

#penalising is a button:

<button id="penalising" class="btn btn-warning">Penalising cards</button>

Clicking it calls the ‘penalisingTaken’ function on the server:

$('#penalising').click(function () {
  socket.emit('penalisingTaken', { tableID: 1 });
  $('#penalising').hide();
});

socket.on('penalisingTaken', function (data) {
  var player = room.getPlayer(socket.id);
  var table = room.getTable(data.tableID);
  if (table.actionCard) {
    table.gameObj.drawCard(table.pack, table.forcedDraw, player.hand, 0);
    socket.emit('play', { hand: player.hand }); //send the card in hands to player
    io.sockets.emit('updatePackCount', { packCount: table.pack.length });
    table.forcedDraw = 0; //reset forced Draw variable
    table.actionCard = false; //set the action card to false
    table.penalisingActionCard = false; //set the penalising action card to false;
    /*PROGRESS ROUND*/
    table.progressRound(player); //end of turn
    socket.emit('turn', { myturn: false });
    messaging.sendEventToAllPlayersButPlayer(
      'turn',
      { myturn: true },
      io,
      table.players,
      player
    );
    messaging.sendEventToAllPlayersButPlayer(
      'cardInHandCount',
      { cardsInHand: player.hand.length },
      io,
      table.players,
      player
    );
  }
});

The logic mirrors what’s in ‘preliminaryRoundCheck’.

That’s it for this episode. I’m still working on the two other request cards. They’re trickier than the penalising card because players can make various requests and override each other’s, so every combination needs proper handling.

Stay tuned …