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

This post is 4 years old. (Or older!) Code samples may not work, screenshots may be missing and links could be broken. Although some of the content may be relevant please take it with a pinch of salt.

I have been really busy in the past week(s) making progress with this project and making it better by rewriting some of the backend functions as well as adding further rules to the game. What I had before was a working version of a somewhat simplified version of the game, ignoring various types of action cards. Now, I have finally added one action card and I'm working on the rest. Time, the lack of it at least, forbids me to work on this project as much as I want to and I'd like to thank you the people who keep on reading the article series for their patience.

The game itself will allow a number of action cards to be played, which are: "2", "Ace" and "King". The game has multiple variations and permutations so in each country there are different rules for different cards, therefore I do need to explain the actions behind the cards for my version:

  • 2: The first player plays "2", then the player who is next in turn will have to take two cards from the pack, unless he/she has another "2". In which case, he/she has the option to play that card and then the next player will have to take four cards. Adding +two cards will continue unless someone decides to draw cards or has no other option (has no 2 card)
  • Ace: a player that plays an "Ace" has the opportunity to make a request for a suite (Hearts, Spades, Clubs or Diamonds). The next player has to play a card that has the requested suite unless he/she decides to play an "Ace" with which they can make a new request and hence eliminating the previous one. If a player has no "Ace" nor the requested suite, they need to draw a card from the pack.
  • King: exactly the same as above but instead of making a request for a suite, the request can be made for a number (2 to 10 and "Ace", "Jack", "Queen" and "King"). Overriding this request is possible with another "King"

I had to 'code' these requests which could have been a challenge itself but this game actually also encourages cheating. Let's assume you have an "Ace" in your hand and you could override someone's request but you can decide to draw a card instead - to keep the "Ace" for later. Therefore I can't force people to play a particular card. Also, imagine the situation where you play a "King" and you have a "5" in your hand. You can make a request for "8" because maybe the next player also has a "King" and they'd override your request. So there's a slight chance that they'll override it and maybe request a "5". Again, I can't force the player to request the card that he/she has in his/her hand.

Let's have a look at the only rule that I have create so far - that is for "2". There are two different scenarios that I had to take into consideration: At the start of the game

The first card placed automatically onto the table is completely random and of course, it can be the card number "2", therefore:

  • Check the first card on the table, and determine whether if 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 am only going to talk about the penalising action card in this post
  • If the player who starts the game (again, this is randomly selected) has no "2" card in his/her hand, I force the draw for that user and finish his/her turn. It's also very important that I remove the action card "flag", which means that the next player can freely play any card (bearing the number-to-number, suite-to-suite rule in mind as explained here)
  • If the player has a "2" card, then he/she has two options, one is taking two cards from the pack, the other is to play the "2" card in hand. If the penalising two cards are taken, the next player can play, again, the action card "flag" is removed.
  • If the player plays the "2" card in hand, the next player's hand is checked. If they have a "2" they have the option to play that or they can draw 4 cards and so on

During gameplay

This scenario is exactly the same as mentioned above.

Okay, that was enough gibberish for the time being, let's do some coding shall we? First things first, I have extended the variables for my table.js file and I have added the following fields that are related to the action cards: 'actionCard', 'requestActionCard', 'penalisingAction', 'forcedDraw', 'suiteRequest' and 'numberRequest', so the file looks like this (Of course it still has the methods explained in 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 have also added a function to the main 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 most important thing to take away from the code above is the way that I'm actually setting/reading the actionCard variable as well as the forcedDraw variable. Every time a person plays a "2", table.forcedDraw =+ 2; is called.

As you can see, I have a new method 'isActionCard' as well as 'isInHand', both are part of game.js, let's have a look at them:

/* 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 also added a function to check force 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 is a "2" on the top of the pile (and if 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 did introduce this function before in a previous post, but I need to talk about it again. The force draw essentially calls the regular 'drawCard' function from game.js but sends in 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 full example below in the 'penalisingTaken' function), note that I am actually passing in the table.forcedDraw variable (that holds the count for the amount of "2" card that were played):

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

At the moment, on the front-end I have a "Draw a card" button, which, allows you to draw one card from the pack. When a player plays "2" and the next player has "2" in his/her hand as well, I place a new button that simply says "Penalising cards". This means that you have two options: play the "2" in your hand or take two cards from the pack. This functionality is enabled by the socket.emit("playOption", { value: true }); bit from the code above and 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 of course, as mentioned before, a button:

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

Clicking this button 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 applied to the 'drawCard' method is exactly the same as shown in the 'preliminaryRoundCheck' function.

That's it for this time. As mentioned before I'm still working on the two other request cards - they are slightly more complicated than the one explained in the post as players can make various requests and they can also override each other's requests so I have to make sure that all combination of requests are programmed properly.

Stay tuned ...