Online Card Game with Node.js and Socket.io - Episode 3
Older Article
This article was published 13 years ago. Some information may be outdated or no longer applicable.
Time for another update. After Episode 1 and Episode 2, I’ve made some progress. Time constraints held me back from doing as much development as I wanted. But I’ve still got something to share: new files, architectural thinking, and tangible results. If you’re following along, keep in mind I don’t always get it right first time. Each post explains what I did and why, but everything could shift in the next instalment. I’m not a JavaScript expert. I probably won’t be one by the time I finish this project either. But I’ll walk away with a much deeper understanding of JavaScript.
In the previous post, I put together an application that let people connect to a websocket server and get dealt 5 cards each. I mentioned that approach had multiple flaws. The most obvious: 100 players could join the game, but I only have 104 cards. You can’t deal to everyone (5 x 100 > 104). So a set of players needs their own deck. Taking this (and many smaller issues) into account, I introduced two new concepts: Rooms and Tables.
The analogy works like this:
- Each Room has multiple Tables (with a maximum number I haven’t settled on yet)
- Each Table has multiple Players (maximum probably set to 4, currently 2 for quicker testing)
- Each Table gets its own pack of cards, used by every player who sits down
This solves a lot of problems. But it also meant rewriting nearly all my code, because it had to become object-oriented. Why? Object orientation lets me create Room, Table, and Player objects, each with their own methods and properties. I’ve also created a Game object that uses roughly the same codebase from Episode 1.
Object-Oriented JavaScript works through prototyping. (Don’t confuse the JavaScript Prototype Framework with JavaScript prototypes. They’re different things.) I won’t explain the concept here since it’s outside the scope of this post, but I found this MDN resource very useful.
Let’s look at the new game.js by examining a few functions from the previous article. First, we instantiate the Game object:
function Game() {
this.pack = this._shufflePack(this._createPack());
}
I’m creating a pack of cards every time the Game object is created. The var game = new Game(); call happens whenever a table is being created. this (as with every object-oriented language) refers back to internal variables or methods. Here are 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;
Every method in the file had to be rewritten to the Game.prototype._methodName_ = function(parameters) format.
Now let’s talk about the Room, Table, and Player objects. The Room object is probably the simplest. It holds multiple tables and multiple players who are connected but not currently playing.
Here are some 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 simple:
var room = new Room('Test Room');
console.log(room);
//{ players: [], tables: [], name: 'Test Room' }
Let’s look at the Table object, since we’ll need to push it into room.tables. A table carries more functionality than a room and needs more information in its constructor:
function Table(tableID) {
this.id = tableID;
this.status = 'available';
this.players = [];
this.pack = [];
this.cardOnTable = [];
this.playerLimit = 2;
this.gameObj = null;
}
Every time a table is created, it gets a unique identifier (tableID), a list of players, and a status that changes dynamically. If playerLimit equals 2 and two players sit down, it flips to “unavailable”. That’s useful as a filtering criterion later on. Each table also gets its own pack of cards, an array tracking cards played, and a game object.
Let’s add a table to the room we created earlier 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' }
*/
Better. The tables array is no longer empty, and we can 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' }
*/
There it is. A test room with a test table. The table holds a pack of cards, and the first card is placed on the table. On the highlighted line, we’re calling the game object from the Table constructor. This also lets us call any function from the game object. If we wanted to reshuffle the deck, we’d call table.gameObj._shufflePack(pack).
The last missing piece is the list of players (notice the empty array above). Let’s create a Player object. Each player gets a name, a unique identifier, a table ID (indicating which table they’re playing at), and a hand of cards.
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 the code 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' }
*/
The room object is looking better, but the user is only in the hall. They’re not sitting at a table yet. Let’s seat them and deal 5 cards into their hand:
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’s players value changed from an empty array to “Object”? Same thing happened with the Player object’s hand value. Let’s inspect:
console.log(table.players);
/*[ { id: 'I\'m-a-unique-ID',
name: 'Tamas',
tableID: '',
hand: [ '8S', '3H', '2H', '12D', '4D' ]]
*/
There’s still plenty to do. Assigning the tableID to the player, for one (though that’s easy enough). You may have noticed I’ve used the .addPlayer() method. These are simple JavaScript array operations, and they live inside their respective JavaScript classes (see the prototype-style function declarations from earlier in the article).
Right now, two people can join a table and start playing cards. One final thing before wrapping up. The game has rules, and one of the most important is: you can only play colour to colour or number to number. I wrote a control function that checks whether the card you’ve selected from your hand is playable:
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 parse both the last card on the table and the card the user wants to play, creating an array so that 3H (Three of Hearts) becomes [[3],[H]]. This lets me run the comparison shown above.
I’m looking forward to posting about further progress soon. There’s still a lot to build, and next up I want to figure out the game flow: when a particular player is allowed to draw or play a card.