I've implemented my favorite board and card games in many different technologies over the years. Several years ago I tried using a cutting-edge technology at the time named Comet, which is supposed to be a pun on AJAX. Since I chose Comet I got bogged down in testing on different browsers and wrote hack upon hack to make everything work. Comet is really just a hack to make bidirectional communication between browsers and servers work. This is tough to do since HTTP is a stateless protocol. When I recently read about WebSockets I knew the time had come to see if I could clean up my projects and actually focus on the game logic instead of worrying about how messages were being passed back and forth.
WebSockets won't be available in browsers until the next generation, but there is a wonderful Flash-based bridge written by Hiroshi Ichikawa called web-socket-js that lets you use WebSockets in almost any browser that supports Flash. It's a great project since everything just works out of the box.
The web server that I chose to implement the games in is Tornado. Tornado is a high performance web server written in Python. I went with Tornado because one of its authors, Bret Taylor, recently added support for WebSockets. WebSockets are a subclass of RequestHandler so they can be mounted at any path and are accessible at the same port as your Tornado server. This should make it easier to run WebSockets in production.
Creating a WebSocket object on the client is as simple as can be:
ws = new Websocket('ws://localhost:9999/ws');
Once you have a WebSocket object, you can add callbacks for when you receive messages and you can send messages:
ws.send('hello server'); // Sends "hello server" to the server
ws.onmessage = function(msg) {
// Alerts any message received on the WebSocket
alert(msg.data);
}
It's probably best to wrap all of this inside of an onopen block in case the WebSocket doesn't open it's connection immediately. This would cause the send command to fail.
ws.onopen = function() {
ws.send('hello server');
ws.onmessage = function(msg) {
alert(msg.data);
};
};
In my projects, I tried to keep the protocol as simple as possible. I think I succeeded in my mission. In the templates, player moves are defined like so:
onclick="ws.send('{{ player.remember(player.play_card, card) }}');"
player.remember returns the id of a callback that is stored on the server. In this example the callback is equivalent to calling player.play_card(card). Anything that follows the callback id on the wire is passed to the callback as the keyword argument 'message'. The callback that allows players to rename themselves is constructed like this:
<input id="name" value="{{ player.name }}" onchange="ws.send('{{ player.remember(player.rename) }} ' + $('#name').val())"/>
This greatly reduces the need to perform complex serialization and deserialization. I could've used JSON on both ends but even then I'd still have to look up items in a similar way to get to the actual live Python objects on the server. Using callbacks means adding a new action doesn't require too many modifications on either end.
Here's how the callback mechanism works:
def remember(self, callback, *args, **kwargs):
def doit(message):
kwargs['message'] = message
callback(*args, **kwargs)
cid = str(id(doit))
self.callbacks[cid] = doit
return cid
When the server receives a message it asks the player to decode the message. The player object does this by looking up the reference in its callback dictionary. Once the player decodes the message it tells the game object that the player wants to perform an action. The game workflow is tremendously simple as well. The current state is stored as a string. For example, in Kaibosh, if we are in the bidding phase, game.state is 'bid'. I chose to use strings instead of references to methods because I found that Python would never garbage collect an object that referred to its own methods. I created a simple decorator which is declared above each state method. The decorator ensures that the player is playing on their turn and that the parameter is of the appropriate type. This is how play_card is defined in the kaibosh module:
@message(Card)
def play_card(self, player, card):
...
I tried to keep the client as stupid as possible so I could keep state in one place: the server. I had originally planed on making it so that there was one supernode client who controlled the games. Then I realized that I'm a Python developer and I really don't want to mess around too much with Javascript. Plus, it would be really hard to deter cheating if all the information was stored in one client. But, I digress. Every time the client receives a message from the server it pulls in the table and player's hand with an XMLHttpRequest call. This helps keep the logic all in one place. jQuery is another technology that I can't live without anymore. Loading chunks of HTML is ridiculously simple with jQuery:
$('#hand').load('http://' + location.hostname + ':' + location.port + location.pathname + '/hand');
It's possible that for some games you'll need to queue up actions and store some state on the client. I did this for Set, since three cards have to be selected for the client to send a message to the server. I tried to keep the state in one place and one place only. When a card is clicked this toggles whether or not it has the "selected" class. When three cards have this class they are sent to the server. When someone else discovers a set while you have cards selected the play area will remove some cards. Since some of the cards that are removed might have been selected, every time a new table is loaded the cards that were saved in the selected_cards array have the "selected" class applied to them. jQuery makes this easier than it should be:
var selected_cards = [];
function check_set() {
selected_cards = [];
$('.selected').each(function() {
selected_cards.push($(this)[0].id);
});
if (selected_cards.length == 3) {
ws.send(selected_cards.join(' '));
}
}
function check_table() {
for (i in selected_cards) {
if (document.getElementById(selected_cards[i])) {
$('#' + selected_cards[i]).addClass('selected');
}
}
}
function update_table() {
$('#table').load(
'http://' + location.hostname + ':' + location.port + location.pathname + '/table',
function (responseText, textStatus, XMLHttpRequest) {
$('.card').bind('click', function() {
if ($('.selected').length < 3 || $(this).hasClass('selected')) {
$(this).toggleClass('selected');
check_set();
}
});
check_table();
}
);
}
WebSockets have made creating games that run in the browser a much easier task than it was before. I'm sure that people will find many new uses for WebSockets. It's great that we can start using it today through web-socket-js and Tornado.
Here is a picture of WebSocket Kaibosh (a Euchre variant which my family plays quite often):
The rules for Kaibosh are here.
Here is a picture of WebSocket Set:
You can play Set here.
The source code for the games I've implemented can be found at GitHub:
- Set - A matching game. Rules can be found here.
- Card Games - Currently Cross Purposes and Kaibosh have been implemented.
- Tic Tac Toe - A much simpler game which can serve as a tutorial for building games using the techniques I described above.