Thursday, December 22, 2011

Tic-Tac-Toe with Redis and Backbone.js

This post is a technical overview of my Tic-Tac-Toe implementation. It is a zero server-side logic, pub-sub based, realtime, multiplayer game which uses Redis and Backbone as the key enablers of real-time and moving logic to the client side, respectively. Now that all the buzz-words are out of the way, the source code is on Github. There are rough edges with reliable communication, security holes do exist, but for the most part it works well.
I would specially like to thank Nicolas Favre-Félix for Webdis without which this would’ve been impossible.

Basic functioning

On visiting the home page the user has an option to either join a game, or play with a friend. The join game option pairs the player with another player who also wants to play (if you are the only one online, you’ll have to wait). If you play with a friend you get a link you can send him/her so you can play together.
The players are asymmetric in the sense that there is a ‘hoster’ and a ‘joiner’, whose roles I’ll get into in a bit.

Technology Stack

Tic-Tac-Toe game me the opportunity to experiment with a ton of technologies/products I hadn’t handled before. The stack goes like this.
  • A Linode VM hosts the service
  • Redis powers base Pub/Sub
  • Webdis provides a REST API to Redis so that the clients can directly talk to it
  • nginx serves static files
  • haproxy routes requests to nginx or Webdis depending on the path
  • Backbone is used for MVC
  • Raphaël is used to draw the grid and pieces using SVG
  • jQuery is used for AJAX and the impromptu is used for modal dialogs
  • underscore is a utility belt
  • UUID.js for generating UUIDs
Yep, thats a lot of stuff for something so simple, so I’ve tried to rationalize it below :P

Clients

All clients are identified by a UUID. The ‘host’ player’s UUID is used as the name of the Redis Pub/Sub channel. The ‘joiner’ will also subscribe to this channel and publish to it. This is why the ‘host’, ‘join’ bifurcation, so that a common channel can be used for communication. All messages are JSON objects.
All communication is initiated by the ‘joiner’. The host player keeps waiting. The joiner starts the game, after which both players keep sending each other the moves made by the humans playing. After each move, both sides check for a win/lose/draw. Again, the joiner sends a gameover message when it detects somebody has won (or it’s a draw). The host then verifies that this is actually true, and responds back with it’s own gameover message. Then both parties change their UI accordingly. This causes the slight delay between the actual win and the notification.
WSD for Tic-Tac-Toe

Server side

The first question is ‘why both nginx and haproxy?’ I could’ve run Webdis and nginx on two different ports since Webdis supports CORS, but Opera does not support it. Running just nginx web-facing and then proxying to Webdis based on path is also not possible since nginx does not currently support HTTP chunked replies, while haproxy does. Chunked replies are used by Webdis to relay Pub/Sub messages.

Implementing ‘Join Game’

To allow two people to be paired, a Redis list is maintained. When a client clicks Join Game it attempts to LPOP a UUID off the list. If none are found (no other players), it RPUSHes itself onto the list.

Client side

The client side has all the logic and so is more interesting. I won’t go deep (you can read the source), but will discuss the key areas.
Once a game has been initiated, both clients open a XMLHttpRequest to the subscriber channel. They watch for readystatechange events and try to parse the messages. This is the Subscriber which fires events when it receives valid messages.
The Publisher component is used to send messages, while Webdis is used for other Redis commands (currently only LPOP and RPUSH).
The GameRouter is the controller, sets up models and views, watches for gameover and beginning a host or join. HTML5 pushState or hashes are used to offer permalinks for games. /host/UUID is for hosting and /play/UUID is the joiner. Backbone’s Router and History is used to cleanly handle this. One feature I wish there was, is a way to query what the current route is via Backbone instead of mucking with window.location.
The GridModel drives the game once it begins, triggering gameover events and checking the grid after every move, also handling the turns. GridView handles rendering the SVG grid and pieces based on GridModel and processes clicks while HUDView notifies the user of his turn, piece and game state. Events are an integral part of this entire setup and Backbone makes it very simple and modularized.
One thing that stands out in my use of Backbone is the absence of Sync. I’ve not used it, although it integrates well with Backbone. The game model didn’t seem suited to it, being more event-based rather than the model reflecting the game state. Collection is also not used since there is only one grid.

Issues

The lack of any server side code does lead to certain issues. None of them are serious when it comes to Tic-Tac-Toe but some affect the user experience and others are security issues.

Usability

On the usability front, the current code is very fickle. The joiner only tries to initiate the connection the first time it starts. If the host fails to respond for some reason (e.g. network connectivity) then both parties will keep waiting. If a joiner refreshes his page mid-way through a game, the game will restart for both parties (a good idea if you are a joiner and you are losing \:P). If the host refreshes the page, the joiner has to refresh after him.
The slight lag to decide win/lose/draw was probably unnecessary. Rather than verifying both sides are on the same page, the notification could’ve been shown directly since this is just a game. The UX would’ve benefited.

Security

Webdis sports a HTTP interface to Redis, but there is no real authentication support, which means the Redis instance hosting Tic-Tac-Toe is also effectively a public domain database (although the command set is restricted). Ideally this should run only in a trusted intranet. Ideally Webdis itself would be capable of serving files and deliver something like a nonce which it would then use to ensure that only its own connections are allowed to relay messages. Similarly nothing is stopping someone from grabbing the UUID and then playing future moves using a bot of some kind to always play optimally. In the case of Tic-Tac-Toe this is just a minor prick, but for actual client-side MVC applications this requires fixing.

Conclusion

Tic-Tac-Toe demonstrates that with a data structure server and HTTP interface to it, web applications where all logic is client-side are possible. Using the server as a Pub/Sub relay also allows near real-time performance, with WebSockets or SPDY possibly leading to better performance. Security policies still mean that true peer-to-peer isn’t possible. Authenticity and authorization also remain to be solved. On trusted intranets, such applications could be used for non-critical tasks. Meanwhile, games like this at least can be safely implemented and played as long as you have no sore losers :).

2 comments: