Online Card Game with Node.js and Socket.io - Episode 3

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.

Time for another update for this project, after Episode 1 and Episode 2 I have made some progress, but due to some time constraints, I wasn't able to do as much development as I wanted. However I still have something to share, I have added further files, thought about the overall architecture a bit, so all-in-all I did achieve something. If you're following the series, you have to bear in mind that I may not do the right thing first and in each post I try to justify what I did and why, but everything can significantly change in my next post. I'm not JavaScript expert, and I probably won't become one by the time I finish this project either, but I will gain invaluable understanding of JavaScript.

In the previous post I managed to put together an application that allowed people to connect to a websocket server, the players were dealt 5 cards each. I have mentioned that this approach, even though it works conceptually, has multiple flows. The most obvious one is that 100 players can join the game but I only have 104 cards, which means I can't deal to every connected user (5 * 100 > 104), therefore a set of players should have their own deck. Taking this (and many other smaller issues) into consideration I introduced a few new features: Rooms and Tables. The analogy is the following:

  • Each Room has multiple Tables (with a maximum number that I have not yet figured out)
  • Each Table has multiple Players (maximum number will probably be set to 4, at the moment it's set at 2 so I can run quicker tests)
  • Each Table will have it's own pack of cards, that will be used by every player who 'sits down' to the table

This solves a lot of my problems, however this has also meant that I had to rewrite nearly all of my code, as it had to be 'Object-Orientified'. Why? Because that allows me to create a room object, allows me to create a table object and also a player object that all have their own methods and properties. I have also created a game object that uses pretty much the same codebase that we saw in Episode 1 of the article series.

Object Oriented JavaScript is achievable via prototyping (Please don't mix up the JavaScript Prototype Framework, with JavaScript prototypes. They are different). I'm not going to explain this concept now as it's outside the scope of my post but I found this website very useful.

Let's have a look at how does the new game.js look like by having a look at a few functions from the previous article. First of all we need to instantiate the game object which can be achieved by calling the following function:

function Game() {
this.pack = this._shufflePack(this._createPack());
}

I'm also creating a pack of cards every time the Game object is created - the var game = new Game(); call is made whenever a table is being created. this - as with every object oriented language - refers back to internal variables or internal methods. Here's the updated shufflePack() and createPack() methods with object orientation applied:

Game.prototype._createPack = function() {
var suits = new Array("H", "C", "S", "D");
var pack1, pack2, finalPack;
pack1 = pack2 = finalPack = new Array();
var n = 52;
var index = n / suits.length;
var pack1Count, pack2Count;
pack1Count = pack2Count = 0;
for(i = 0; i <= 3; i++) {
for(j = 1; j <= index; j++) {
pack[packCount++] = j + suits[i];
}
}
finalPack = pack.concat(pack);
return finalPack;

Essentially every method inside the file had to be rewritten to the format of Game.prototype.methodName = function(parameters).

It's time to talk about the Room, Table and Player objects for a moment as well and let's dive straight in. The room object is probably the most simple one to explain. It contains multiple tables and multiple players - who are currently not playing but are connected to the service.

Examine some of the key functions:

function Room(name) {
this.players = [];
this.tables = [];
this.name = name;
}

Room.prototype.addPlayer = function (player) {
this.players.push(player);
};

Room.prototype.addTable = function (table) {
this.tables.push(table);
};

Creating a new room is rather simple and it can be very easily achieved by the following lines:

var room = new Room('Test Room');
console.log(room);
//{ players: [], tables: [], name: 'Test Room' }

Let's visit the table object, as we'll need to push that into the room.tables array. A table will have a lot more functionality then a room and therefore it requires a lot information in the constructor:

function Table(tableID) {
this.id = tableID;
this.status = 'available';
this.players = [];
this.pack = [];
this.cardOnTable = [];
this.playerLimit = 2;
this.gameObj = null;
}

Each and every time when a table is created, it will have a unique identifier (tableID), a list of players, a status - which will dynamically change, if the playerLimit value equals 2, for example, it will change to 'unavailable'. This is very useful as it can be used as a filtering criteria as well later on. Furthermore, each table will have their own pack of cards, an array to keep track of the cards played by the players and a game object.

Let's add a table to the previously created room and see how the room object changes:

var room = new Room('Test Room');
var table = new Table(1);
table.setName('Test Room');
room.tables = table;
console.log(room);
/*{ players: [],
tables:
{ id: 1,
name: 'Test Room',
status: 'available',
players: [],
pack: [],
cardOnTable: [],
playerLimit: 2,
gameObj: null },
name: 'Test Room' }
*/

This looks much better now doesn't it the tables array is no longer empty and we can also see the elements belonging to the table object. Let's also add a pack and put the first card on the table:

var room = new Room('Test Room');
var table = new Table(1);
table.setName('Test Room');
room.tables = table;
var game = new Game();
table.gameObj = game;
table.pack = game.pack;
table.cardOnTable = table.gameObj.playFirstCardToTable(table.pack);
console.log(room);
/*{ players: [],
tables:
{ id: 1,
name: 'Test Room',
status: 'available',
players: [],
pack:
[ '4H',
'6H',
'8D',
'6D',
'8H',
'6S',
!output trimmed!
'9S' ],
cardOnTable: [ '1C' ],
playerLimit: 2,
gameObj: { pack: [Object] } },
name: 'Test Room' }
*/

And there you have it. A test room with a test table, on the table you have a pack of cards and the first card is played on the table. Note that on the highlighted line, we are calling the game object that is in the constructor of the Table object. This also enables us to call any function from the game object, so if we wanted to reshuffle our deck we could call table.gameObj._shufflePack(pack).

The final thing that is missing would be the list of players, notice the empty players array above. Let's create a player object as well. Each and every player will have a name, a unique identifier, a table ID - to indicate which table they're currently playing at, and of course a hand - which will contain the cards that they currently own.

function Player(playerID) {
this.id = playerID;
this.name = '';
this.tableID = '';
this.hand = [];
}

The playerID will be a unique identifier produced by socket.io itself:

io.sockets.on('connection', function (socket) {
socket.on('connectToServer',function(data) {
var player = new Player(socket.id);

Let's extend our code again and manually add a player:

var room = new Room('Test Room');
var table = new Table(1);
table.setName('Test Room');
room.tables = table;
var game = new Game();
table.gameObj = game;
table.pack = game.pack;
table.cardOnTable = table.gameObj.playFirstCardToTable(table.pack);
var player = new Player("I'm-a-unique-ID");
player.setName('Tamas');
room.addPlayer(player);
console.log(room);
/*{ players:
[ { id: 'I\'m-a-unique-ID',
name: 'Tamas',
tableID: '',
hand: []],
tables:
{ id: 1,
name: 'Test Room',
status: 'available',
players: [],
pack:
[ '4H',
'6H',
'8D',
'6D',
'8H',
'6S',
!output trimmed!
'9S' ],
cardOnTable: [ '1C' ],
playerLimit: 2,
gameObj: { pack: [Object] } },
name: 'Test Room' }
*/

Our room object looks better and better but the user is only in the hall, i.e. he's not sitting at a table -- yet. Let's sit our user down for a game of cards and give him 5 cards in his hand, shall we?

var room = new Room('Test Room');
var table = new Table(1);
table.setName('Test Room');
room.tables = table;
var game = new Game();
table.gameObj = game;
table.pack = game.pack;
table.cardOnTable = table.gameObj.playFirstCardToTable(table.pack);
var player = new Player("I'm-a-unique-ID");
player.setName('Tamas');
room.addPlayer(player);
table.addPlayer(player);
player.hand = table.gameObj.drawCard(table.pack, 5, '', 1);
console.log(room);
/*
{ players:
[ { id: 'I\'m-a-unique-ID',
name: 'Tamas',
tableID: '',
hand: [Object]],
tables:
{ id: 1,
name: 'Test Room',
status: 'available',
players: [ [Object] ],
pack:
[ '4H',
'6H',
'8D',
'6D',
'8H',
'6S',
!output trimmed!
'9S' ],
cardOnTable: [ '1C' ],
playerLimit: 2,
gameObj: { pack: [Object] } },
name: 'Test Room' }
*/

Notice how the table players value changed from an empty array to 'Object'? The same has happened with the Players object's hand value. Let's have a look at these values:

console.log(table.players);
/*[ { id: 'I\'m-a-unique-ID',
name: 'Tamas',
tableID: '',
hand: [ '8S', '3H', '2H', '12D', '4D' ]]
*/

There are a lot of further things to do, such as, assign the tableID to the player, but this can be achieved relatively easily. Also as you may have noticed, I have used the .addPlayer() method, they are rather simple JavaScript array operations, and they are part of their respective JavaScript classes (see the beginning of the article for a prototype type of function declaration).

At the moment I'm at a stage where 2 people can join a table and start playing cards. One final thing before I finish this post. The game has a few rules but one of the most important ones is that you can only play colour to colour or number to number. I have written a control function that checks whether the card that you've selected from your hand is playable or not, let me share this now:

Game.prototype.isCardPlayable = function (card, lastCardOnTable) {
cardArray = card.split(/([1-9]|1[0-3])([H|S|C|D])/);
cardArray = cardArray.filter(function (n) {
return n;
});
lastCardArray = lastCardOnTable.split(/([1-9]|1[0-3])([H|S|C|D])/);
lastCardArray = lastCardArray.filter(function (n) {
return n;
});

cardNumber = cardArray[0];
cardSuite = cardArray[1];
lastCardNumber = lastCardArray[0];
lastCardSuite = lastCardArray[1];
if (cardNumber == lastCardNumber || cardSuite == lastCardSuite) return true;
else return false;
};

//getting the last card from the table:
if (!Array.prototype._last) {
Array.prototype._last = function () {
return this[this.length - 1];
};
}
Game.prototype.lastCardOnTable = function (table) {
return table._last();
};

First I need to parse both the last card on the table and the card that the user wants to play and I have to create an array such that the card 3H (Three of Hearts) will become [[3],[H]]. This then allows me to do a comparison as displayed above.

I hope you've enjoyed this article - I'm looking forward to posting again in the not so distant future about further progress. There's still a lot more to do and I will hopefully be investigating how to implement the game flow - that is, when is a particular player allowed to draw / play a card.