Building an HTML5 game with React and socket.io

12 Mar 2015

Some time ago I started a little side project in order to fiddle with a few different technologies and get to know them better. One of the things I wanted to explore was how to build UIs with ReactJS and see how it will perform at a little bit larger scale. The goal was to build something usable and to have fun while doing it. And so, I thought that creating an online game would be a good idea.

Do you remember Battleships (aka Sea Battle)? This simple, yet absorbing game you could play in a school desk as a kid? Let me introduce the new remake of it!

You can play the actual game at Battleships.mobi

The GitHub repo for Battleships is available here.

A brief explanation of game rules: before actual play begins each player secretly arranges their ships on their own board. When both players are ready the game proceeds in a series of rounds where each player tries to hit and sink ships of the opponent, having one shot each round. The player who will first destroy all enemy ships - wins.

Game features

  • Real time gameplay for two players;
  • User Lobby where players can see who is currently online and invite each other to play;
  • Support for mobile and desktop browsers by means of responsive web design;
  • Single-player mode (play against computer);

Playing on Facebook canvas

Later on I came up with an idea to integrate the game with Facebook Javascript SDK (basic features: friends list, invitations) and bring the game to Facebook canvas.

Here’s the beta version: apps.facebook.com/battleshipsboardgame

Used technologies and tools

React and (Re-)Flux

Battleships is technically a Single Page Application with UI built with React. For those who are new to React I recommend, besides official React docs, a post by James Longster "Removing User Interface Complexity, or Why React is Awesome", which is a great analysis of React’s concepts.

As the whole client-side code is written as CommonJS modules and bundled with Browserify, it allows for using Reactify transform. One benefit of this is that modules which are using JSX syntax are converted to pure Javascript on the fly. What’s more, Reactify enables to compile a subset of EcmaScript 6 syntax to ES5. Supported ES6 features are: arrow functions, rest params, templates, object short notation and classes. That is a great thing, because many of long-awaited features of ES6 bring a great relief in writing JavaScript code, making it more concise, readable and saving some keystrokes.

To be able structure the app better I decided to use Flux, a recommended architecture for React. Same as React, Flux adheres to unidirectional data flow and enables to decouple application logic from views (React components). Flux introduces a few new building blocks into the architecture: stores and actions. In a way it has many similarities to the pub-sub pattern – stores and views are listening to events they have subscribed. Actions are events triggered off from views and stores also dispatch their respective update events, which are listened to by views. The main purpose of stores is to manage the application state and logic.

Over the last months a few different flavors of Flux architecture have popped up. For Battleships I have chosen Reflux, which seems to simplify a few concepts of the originally proposed architecture but still retain the architectural features. Differences to the original Flux are best described in official docs. For the purposes of building Battleships I really needed a small subset of features that Reflux supports.

An example of a view-store interaction for the ships arrangement step. SetupView triggers a "placeShips" action once the user is ready with the ships placement:

var SetupView = React.createClass({
  mixins: [Reflux.listenTo(SetupStore, 'onSetupChange')],
  onSetupChange(setup) {
    this.setState({setup});
  },
  placeShips() {
    Actions.setup.placeShips();
  },
//...
  render() {
    var {state} = this;
    return (
      <div className="setup">
        {state.setup.config ?
          <div>
            <div className="command">
              Place ships on the gameboard!
            </div>
            <div className="side">
              <div className="confirm">
                <button type='button' className="btn btn-default"
                    disabled={!state.setup.allPlaced} onClick={this.placeShips}>
                  <span className="fa fa-check"></span>
                  Ready!
                </button>
              </div>
              <ConfigPanel setup={state.setup} />
            </div>
            <SetupBoard setup={state.setup} />
          </div> : null}
      </div>);
  }
});

The SetupStore manages the application logic for the whole setup process and handles communication with the web socket server):

var SetupStore = Reflux.createStore({
  init() {
    this.listenTo(Actions.setup.placeShips, this.emitShips);
    //...
  },
  emitShips() {
    var {state} = this;
    var allPlaced = () => {
      return (!_.any(state.config.ships, (item) => {
        return (item.count > 0);
      }))
    };
    if (allPlaced()) {
      var toSend = state.ships.map((ship) => {
        return ship;
      });
      socket.emit(gameEvents.client.placeShips, toSend); //send arranged ships to the server
    }
  },
//...
});

And this how the setup view looks like in horizontal mode:

Game notifications with react-toastr

To implement game notifications I forked the react-toastr project. My version ditches jQuery for CSS3 animations to display toasts.

SVG components

SVG with its built-in scalability fits in well with responsive design. Part of Battleships React components was built using inline SVG, especially those responsible for the game grid and the deployment of ships. Good news is that support of SVG standards is quite advanced across all modern browsers, including the mobile ones. The capability to define a viewbox with its own coordinates system allows for full control over relative proportions and position of particular SVG elements. Coordinate system makes drawing multiple layers a straightforward task with SVG. Like in the example below where first a regular grid cell is drawn. Then an optional cross-out is rendered, indicating that the cell was already shot at:

var Cell = React.createClass({

  render() {
    var {props} = this;
    var rectProps = {
      className: 'cell',
      width: 10,
      height: 10,
      x: props.x * 10,
      y: props.y * 10
    };

    if (props.shot) {
      var shot = props.shot;
      var classes = React.addons.classSet({
        'cell': true,
        'shot': !!shot,
        'update': props.update,
        'adjacent': (shot && shot.isAdjacentToShip)
      });

      return (
        <g className={classes}>
          <rect onClick={props.onCellClick} {...rectProps} /> //regular cell
          <g> //crossout
            <line x1={rectProps.x} y1={rectProps.y} x2={rectProps.x + rectProps.width} y2={rectProps.y + rectProps.height} />
            <line x1={rectProps.x} y1={rectProps.y + rectProps.height} x2={rectProps.x + rectProps.width} y2={rectProps.y} />
          </g>
        </g>);
    }

    return (
      <rect onClick={props.onCellClick} {...rectProps} />
    )
  }
});

During the development I encountered two major IE11 glitches concerning SVG. One is that IE11 seems to not support CSS3 animations on SVG elements. The second issue relates to how browser determines SVG dimensions based on the container element width.

Let’s look at the example:

Assuming that the SVG element has a defined width (percentage or absolute) and the viewBox attribute set, the height of the element will be calculated proportionally (at least in Chrome, FF, Opera and their mobile counterparts). In the example above both viewBox height and width are equal, therefore the rendered area will always be a square with size dependent on containers width. However, this reasonable behaviour seems not to be the case for IE11 – there the height of SVG element remains always at fixed 150px regardless of the actual container width. A partial workaround for that might be to additionally set a height on the SVG or its parent, expressed in with the CSS viewport width (vw) unit, like this:

.main {
   display: inline-block;
   width: 50%;
   border: solid 1px gray;
   height: 50vw;
}

Socket.io server

Client-server communication throughout the whole gameplay is implemented using socket.io. Each individual connection between client and server is represented by a socket instance on both sides. Socket.io provides us also with a concept of socket rooms, which are simply logical channels that individual sockets can join or leave. This comes in handy when we need to support multiple groups of users wherein messages are broadcast. I have used the room feature to handle the communication within user lobby and for each active game session.

One thing I noticed is that the official documentation of socket.io seems not to be very comprehensive. Part of the API was better described in GitHub issues to the project.

Unit testing the game logic

Since the server is managing most of the gameplay, it contains quite a lot of logic regarding validation and game flow. To unit test this code I had to get rid of initial dependencies on socket.io and split my code into game.js and service.js modules. The former handles the actual game logic and the latter is only a sort of proxy, which passes on requests and results using the socket instance. Both parties communicate through Node EventEmitter Bearing in mind the similarities between EventEmitter and how socket.io emits its own events, we can setup a proxy with a few lines of code:

function BattleshipsService(emitter, sockets) {
  //…
  var clientEvents = [
    gameEvents.client.placeShips,
    gameEvents.client.shoot];

  sockets.forEach(function (socket) {
    emitter.on(socket.username, function (event, data) {
      if(event != gameEvents.server.gameOver) {
        socket.emit(event, data);
      }
    });

    clientEvents.forEach(function (event) {
      socket.on(event, function (data) {
        emitter.emit(event, socket.username, data);
      });
    });
  });
  //…
  // game logic
  var game = new Game(emitter, sockets[0].username, sockets[1].username);
}

With such decoupled code it is much easier to write Jasmine unit tests for the game logic:

GameSpec.js

var BattleshipsGame = require('../../game/battleships/game')
  , EventEmitter = require('events').EventEmitter
  , gameEvents = require('../../game/gameEvents');

var game, emitter;

describe('Battleship game', function () {

  beforeEach(function () {
    emitter = new EventEmitter();
    game = new BattleshipsGame(emitter, 'player1', 'player2');
  });

  it('doesn\'t allow to shoot if ships are not placed', function(done) {

    //setup async test with event emitter
    emitter.on('player1', function (event, result) {
      expect(event).toEqual(gameEvents.server.shotUpdate);
      expect(result.error).toBeTruthy();
      expect(result.error).toEqual('Place ships first!');
      done();
    });

    //run test
    game.shoot('player1', {x:0, y:5});
  });
});

Conclusions

Building Battleships was definitely an informative experience and helped me to get a solid grasp of a few different tools, which I hope to work with in the future.

My main conclusion is that the mix of React and Flux proved to be a good choice for building a robust and maintainable JavaScript application. In terms of responsive design I had an opportunity to check out several CSS3 features and capabilities of media queries in modern browsers (I loved the aspect-ratio media query – it is in many cases much more suitable than min / max-width queries). Two other interesting lessons were: how to build a basic real time application with socket.io and how to integrate a JavaScript app with the Facebook canvas API. I’ll try to expand on it in a future post.

comments powered by Disqus