I would specially like to thank Nicolas Favre-Félix for Webdis without which this would’ve been impossible.
Basic functioningOn 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 StackTic-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
ClientsAll 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.
Server sideThe 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 sideThe 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/UUIDis for hosting and
/play/UUIDis 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
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.
IssuesThe 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.
UsabilityOn 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.