Using Elixir, Phoenix and Elm to Visualise Conway’s Game of Life

In a previous blog post I discussed an implementation of Conway’s Game of Life in Elixir that myself and some colleagues recently built. Here I describe the visualisation of this using Phoenix and Elm.

Screenshot

The requirement here was to provide a nice web UI which would display the game simulation in real time showing the changing cells on the screen and allowing the user to stop, start and change the speed of the simulation and also to view which portion (or board) of the overall grid of cells they wish to view. Although changes that control the simulation flow from the browser to the web server, and could match a typical HTTP request/response cycle, updates to the state of the cells flow only from the server to the browser and do not fit this pattern. Therefore utilising Phoenix channels (which sits on top of web-sockets) ideally match this model. Furthermore, developing a single-page web to handle this communication suits Elm, which also has libraries that communicate easily with Phoenix channels.

Simple Phoenix App

We took the approach outlined in the conference talk Phoenix is not your application and created a separate Phoenix application for the visualisation and then added the Game of Life project (containing the calculation and board logic) as a mix dependency. This clearly separates the responsibilities of the two applications with the ‘web’ project only interacting with the ‘game’ project via APIs.

Enabling Elm

Using the default Phoenix app generated by mix phoenix.new we just changed /web/templates/page/index.html.eex to remove the default content and replace it with a <div> where our Elm app will run:

<div id="elm-container"></div>

No changes to the controller, router or any other files were necessary to render the container for Elm.

After installing Elm via npm install -g elm Elm is configured to compile and build by adding the NPM package elm-brunch to package.json.

Then brunch-config.js needs to be configured to build elm using this:

plugins: {
  ...
  elmBrunch: {
        elmFolder: "web/elm",
        mainModules: ["App.elm"],
        outputFolder: "../static/vendor"
      },
}

Phoenix/Elm Communication

In order for the Elm app to communicate with Phoenix we use multiple Phoenix channels:

  • grid - this is used for communicating the list of boards available and controlling the ticker. There is one global channel.
  • board:x,y - there are multiple channels for each board where x,y represents the board id (which is the co-ordinates of its bottom left corner). However, the Elm app is only connected to one board channel at a time, which is the board it is currently displaying.

In Phoenix the channels are used by implementing callbacks in GridChannel and BoardChannel. On the Elm side we used the elm-phoenix library . Unfortunately this cannot be bundled as a Elm package so we just have to include the source code in an elm-vendor folder. However, this is simpler to use and requires less boilerplate code than other libraries such as elm-phoenix-socket which we initially used. Using this we wire up ‘join’, ‘leave’ and ‘disconnect’ notifications as well as messages pushed to the channel to messages which will have a JSON payload.

gridChannel : Channel.Channel Msg
gridChannel =
  Channel.init "grid"
  |> Channel.on "ticker:update" ReceiveTickerUpdate
  |> Channel.onJoin ReceiveGridChannelJoin
  |> Channel.onLeave ReceiveGridChannelLeave
  |> Channel.onDisconnect ReceiveGridChannelDisconnect

This can then be wired up to the socket in the subscriptions function.

When the Elm app starts it tries to connect to both the grid channel and board channel board:0,0 (We assume this board will always exist).

Grid Channel

On joining the grid channel we reply with the current state of the ticker and the list of available boards. We do this by implementing the

def join("grid", message, socket)

callback in GameOfLifeWeb.GridChannel

When the reply is received in Elm we decode the JSON result, update the list of boards and the state of the ticker.

Board Channel

In the same way that the logic for synchronising boards listens to the :board_update event that is published via the EventManager, this web app also listen to the events in the Phoenix code and then re-broadcasts them on the board channel. Again, these are received as messages in the Elm code, decoded and the Elm’s model updated with the board state.

Elm view

The view is relatively simple and consists of a status bar which shows the generation number and controls for updating the ticker. Below the board is displayed by drawing all the alive cells using relative positioning.

Going full screen

We provide a button to show the browser in full-screen mode. There is a Javascript API to perform this so a call to native JS code is necessary using an Elm port. In our Update function we invoke the command when the button is clicked

ToFullScreenClicked ->
  (model, requestFullScreen "on")

…and then the corresponding definition of the JS function

port requestFullScreen : String -> Cmd msg

…and the JS function itself

elmApp.ports.requestFullScreen.subscribe(function(status) {
  // Only works for Webkit currently
  document.querySelector('.board').webkitRequestFullscreen();
});

Source code

Follow the setup instructions in the Visualisation project to run it yourself.

What next?

There are several places the code could be refactored and more tests could be added. Additionally it would be nice to implement a version of this with the calculations for the Game of Life happening in the browser in Elm and each browser representing a different part of the grid. The Phoenix app could then just be used to communicate the border regions between the browsers.