The Sandwich Delivery Game is a game where the player, virtually, has to deliver as many sandwiches as possible to customers while sitting, in reality, on a fixed bike. A cellphone attached to the handlebars in front of the players shows their position on a map. Turning the handlebars changes the orientation of the player on the map. Moving forward is realized with a speed sensor: The sensor is attached to the back wheel of the bike which in turn is fixed in a trainer stand and runs against a resistance. The speed of the wheel is sent to the cellphone. In order to attract attention of passersby, the device’s screen is mirrored to the wall in front of the player. Some impressions of the setup from the exhibition can be seen in image 1 and 2.
This post will first describe the way from finding the idea of the game, introducing Elm and its winning and loosing points for the project and closes with a description of the overall architecture. The latter shows the challenges of integrating JavaScript libraries in Elm, especially the central map component as well as connecting the inputs of the hardware components to the game logic.
The source code can be viewed on GitHub.
Finding the idea
The requirement was to build something catchy and interactive for the visitors of our exhibition stand at the “Smart City” exhibition in Basel.
At the exhibition we wanted to address the topic of “Connected Mobility”. Therefore we upgraded an ordinary bike with the COBI hardware, i.e. a smart phone holder and a controller for the right hand along with a speed sensor. The phone itself was mounted on the handlebars and should show the game screen. The bike was mounted on a trainer with resistance to the back wheel. With this setup, the player could move faster or slower by using the pedals and breaks as well as move left and right with the moveable handlebars. Letting the player virtually move around the city of Basel was a must have feature (see image 1 and 2). But just moving around, especially if only virtually, is boring. We had to give a task to the player.
After some brainstorming and discarding ideas like a Tron derivate or a platform game (in German we call it jump ‘n’ run) we finally arrived at a delivery game. Because of a tough deadline, the discussion of whether the player should deliver ice cream or sandwiches was pretty short.
A score was introduced to motivate a competition between players: Within a time frame of two minutes the player has to deliver as many sandwiches as possible. With each delivered sandwich CHF 10 are earned. Driving not on a street as well as ignoring red traffic lights are penalized with the realistic Swiss fines of CHF 30 and CHF 60.
In order to make the scenario even more realistic, green waves along with other virtual bikers were added. A column of bikers approaching a red traffic light is triggering the traffic light to change to green. Therefore following a group of virtual bikers may reduce the waiting time on red traffic lights for the player.
The intro screen (opened on a laptop) and a scene from the final game can be seen in image 3 and 4.
Before diving more into our learnings and the architecture, let’s deliver some sandwiches: Sandwich Delivery Game !
How to play with a keyboard or a mobile phone is described on the intro screen depending on which device the game is opened. For the published version, the score server is deactivated since different input methods (keyboard or mobile phone) are not comparable. Moreover, it would be very easy to fake the results.
The bike integration which was done with the COBI DevKit was removed from the code since the DevKit was not released yet at the time the game was developed. Moreover, the Sandwich Delivery Game was the first externally developed COBI module and therefore surely also a test for the now released DevKit.
As a side note: COBI themselves evolved and completed the idea to the Brezelbike.
Introducing and justifying Elm
The Elm Architecture
Elm is a purely functional programming language which has its roots in Haskell: Elm has a similar and clutter-free syntax, is statically typed and has a powerful type inference.
But Elm is far more than just a language, it also provides an architecture which is the most thrilling feature of Elm. Even though the concept of the architecture is old and rusty since it is just Model-Update-View, Elm embodies the concept in such a way that it feels naturally to develop a programm with this architecture.
In Elm the Model-Update-View architecture is realized as follows:
- The
Model
defines the state of the application. This can simply be expressed by a type alias:
Anything that may change at runtime has to be described in the Model
, otherwise it cannot be updated in the update
function.
- The
update
function updates the state. For this, the first parameter ofupdate
is aMsg
:Msg
is a union type (or algebraic data type, ADT) of everything that happens in the program and may cause an update to the model:
The second parameter of update
is the current Model
which should be updated. The result is the updated Model
.
A little bit confusing is that update
also returns a command Cmd Msg
. A command can be a HTTP request or generating a random number. So, along with the updated model, update
can request asynchronous operations, i.e. operations which are no pure functions. The result of these operations are Msg
s which get fed into update
again by the Elm runtime.
The counterpart of commands, which are not shown in this post, are subscriptions: A subscription Sub Msg
also produces Msg
s for update
. Time or JavaScript callbacks are examples of subscriptions.
For the curious ones: These concepts of commands and subscriptions are described in the “Effects” chapter of the Elm Guide.
- The third part,
View
, maps from model to HTML:
Again there is a Msg
in the return value. So a view can also generate Msg
s for the update
function, for example when a button is clicked.
Elm’s talkative compiler
Another noteworthy feature of Elm is a surprisingly and incomparably friendly compiler. The compiler not only states compile errors, it also tries to provide a solution and gives hints. One such compiler error is:
Usually such a long compiler error means something bad has happened (especially when you are coding in C++). But this is not true for Elm. Looking at the error message, the following information is printed:
- Apparently the compiler complains about the type of an
if
statement. - The problematic
if
statement is printed: It starts in line 43 of the file Biker.elm. - In order to help to fix the bug, the compiler states the inferred and mismatching types of the branches:
- Therefore the only thing to do is to compare and fix the types: The problem is that the if branch returns a
Maybe
whereas the else branch does not. - Consequently, changing line 50 to be
Just newBiker
solves the problem. - Also note the hint at the very end of the compiler error which helps to understand the reported failure.
So the compiler does not only complain about being not satisfied, it also tries to provide information to find a solution and gives hints to improve the understanding of an error that has occurred. Consequently the compiler itself played a not so small role in the on-boarding process by supporting the trainee with a nice error messages.
Upsides of using Elm
Already by means of its architecture, Elm forces the developer to describe the application in a safe way: There is no shortcut for updating the current state but using update
. There is also no way to present some other, non-static, information in the view
function other than providing it via the model
.
Elm also takes over the runtime, the developer does not have to care about it. The developer only provides the update
and view
function and subscribes to effects like mouse clicks, callbacks from JavaScript or time. Calling the implemented update
and view
function is done by the Elm runtime whenever it is required.
So in general the main loop is:
- An update happens.
- A
Msg
is sent toupdate
together with the current model. - The result is a new model.
- The new model is put into the
view
and results in a new view. - The new view is shown.
- Wait for new updates.
On first sight this loop seems to be a restriction for the developer because it is so simple and because it forces the developer to express everything in just these few functions and types. But in the end the loop has all the needed power and additionally prevents the developer from doing (hopefully unknowingly) stupid things like modifying state anywhere or handling concurrency and side effects carelessly.
Important enough to mention: Elm does not render the complete page after each call of the view
function. Elm keeps a copy of the current page and computes the difference of the current and the new page. By knowing the differences, Elm updates only these parts of the page.
More information on this technique called a “virtual DOM” as well as an Elm-provided performance test can be found in an Elm blog post.
Elm’s architecture and the nature of the language lead to the fact that after satisfying the compiler, which becomes like talking to a friend after some passionate discussions (which the compiler always won), the program worked every time as intended. That was pretty astonishing, we faced no runtime errors (except when we hit a bug in the Elm compiler which in turn was rooted in a JavaScript limitation).
Another advantage arises from the declarative description of the view: animations are almost trivial. By subscribing to the time on every 100ms, the update
function gets called every 100ms. An animation then just saves the start time and computes the new position of the object over time.
As a matter of fact, all notifications in the game poping up next to the player arrow are animations. Notifications indicate the remaining time, earned money and penalties. An animation of a notification is a text which moves up and simultaneously gets smaller over time. In order to do this more stylish, i.e. not linear, ease function are used (all available ease functions are on easings.net). With some simplifications, the code is as follows:
- The
lifetime
of a notification describes how long it is already active. Note that a notification with negative lifetime is removed by theupdate
function. -
fontSize
computes the font size based on thelifetime
with an outExpo ease function. -
paddingTop
computes the position based on the index of a notification (older notifications are shown above newer ones) and an inExpo ease function with thelifetime
. - In the
in
part of thelet-in
, functions likeposition
,rightJustified
etc. from a graphics library are used to create the viewableForm
.
Downsides of using Elm
Since Elm is transpiled to JavaScript, i.e. to its own file, it is hard to modularize an Elm application into separate, loosely coupled programs. It is possible to develop independent Elm programs and let them communicate over a JavaScript bridge with subscriptions and commands, but this is certain to become tedious. In general, Elm is intended for the creation of single page application (SPA) and consequently is good at developing exactly this, SPAs.
The Sandwich Delivery Game is a pure SPA and it is only reasonable to program games this way: The game captures the whole page and has to control every single pixel on the screen.
Nevertheless, modularizing within a single application becomes a problem as well: It lets the Model
as well as the update
function get bigger and bigger with every added feature. At some point the update
function is not maintainable and understandable any more and the question on how to split the function will arise.
One solution is to create subtypes in Msg
and a handle
function for each subtype. Then update
acts as a router and delegates the messages to its handle functions.
Then, still, every messages enters the update
function, but this solution is certainly more expressive and understandable than connecting different modules over a non-visible router.
For our application we just delegated all updates of the 14 Msg
s which could not be expressed in one line to specific handle
functions and had already a big and easy win regarding comprehensibility.
Sadly there is currently no support from any IDE for refactorings like extracting code to smaller functions which would be handy for modularizing functions.
Debugging in Elm is different as well and therefore it needs time to get used to it. An advanced approach would be to use the Elm debugger which can be opened in the live view, see images 5 and 6.
This debugger has some neat features: First of all, the debugger has a history of all messages which have been passed to the update
function. Therefore all states of the application can be viewed: Double-clicking on one of the messages shows the state which resulted from the message. This is pretty powerful for analyzing a bug.
Also very handy is that the history can be exported and imported in order to share a program run or reconstruct a run later.
On the contrary to our initial expectations the debugger does not stop the program on selecting a message from the history and since we are subscribed on a 100ms basis to time, the messages with the time as content still flow through the update
function. Moreover, these time messages have side effects since they draw elements on the map (see the chapter on the game architecture for the flow).
Consequently the debugger was, despite analyzing the history of messages, due to the tight interaction with the JavaScript universe, not very useful for us.
Nevertheless, the poor man’s debugger of printing messages to the console was a good enough alternative.
Another downside is that on-boarding is hard. People who are not used to functional programming have to wrap their head around handling things in a functional way. For example generating a random number is a side effect. Means: A command has to be returned in the update
function and the generated random number will asynchronously be received by the system over the update
function via a Msg
.
Handling this correctly is painfully at first sight. But then most often the clarity arises within the developer that the problem has to be expressed in this way in order to be free of side effects. A similar and often one of the first experiences is the correct handling of non-existence with Maybe
. There is no workaround with null
as in Scala.
In the end all this leads to learning and using patterns of functional programming.
The Game Architecture
The game consists of a JavaScript and Elm component, interacting with each other to provide the game experience: The core screen of the game is the city center of Basel, presented as a 2D map via Leaflet (the tiles are served by CARTO). Since leaflet is a JavaScript library, the Elm application has to send commands to JavaScript for updating the map and the elements shown on it. On the other side, the JavaScript component listens to the navigation input events, depending on the device: Keyboard inputs, callbacks from the orientation API and/or callbacks from the COBI library. All these callbacks are forwarded to Elm via the subscription mechanism (see the Elm Guide).
The overall architecture with the responsibilities of the JavaScript and the Elm component are described in image 7.
Initialization and configuration of the Elm application is done in JavaScript.
The Elm application then initializes the basic layout of the page. After this, the JavaScript part is notified that the map can be drawn.
Following the request from Elm, the lowest layer, the map, is initialized and drawn by JavaScript.
On the top is an overlay layer which is drawn and used by Elm to show the sidebar and the player notifications.
For the navigation, the speed sensor of the bike and the orientation of the cell phone are received via callbacks in JavaScript (alternatively keyboard input is supported in non-mobile browsers). These info are forwarded to Elm.
Due to game play or due to updates from the navigation inputs, Elm updates the displayed map elements as well as the shown map sector and the rotation of the map. The elements of the game are other bikers (blue dots), the player arrow, traffic lights, hot spots for sandwich requests (circles with numbers) and streets.
This architecture was more or less given by the choice to use Elm. Nevertheless it proved to be robust and viable despite the integration of JavaScript, Elm and hardware.
Conclusion
Programming with Elm is fun and can be productive at the same time. This is not only because of the language and the simple but powerful architecture, but also because the power of millions of JavaScript libraries still lie at hand. It is as easy as it can be to integrate JavaScript into the holy world of the Elm universe. Developing the game proved the concepts of Elm to be well thought-out and that its way of integrating JavaScript may be an enabler for a lot of programs written in Elm.
On the other hand, Elm is very young. This not only means that breaking changes between releases still happen but also that things like tools for refactoring are still missing. These are rather high-level but nonetheless non-negligible problems of the new “product” Elm.
In conclusion, use Elm in every case where it is meaningful. In every other case, try to mimic the architecture provided by Elm in order to have a stress-free life.