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.
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:
Modeldefines 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
updatefunction updates the state. For this, the first parameter of
Msgis 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
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
Msgs 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
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
Msgs 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
- The problematic
ifstatement 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
Maybewhereas the else branch does not.
- Consequently, changing line 50 to be
Just newBikersolves 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
Elm also takes over the runtime, the developer does not have to care about it. The developer only provides the
view function is done by the Elm runtime whenever it is required.
So in general the main loop is:
- An update happens.
Msgis sent to
updatetogether with the current model.
- The result is a new model.
- The new model is put into the
viewand 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.
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:
lifetimeof a notification describes how long it is already active. Note that a notification with negative lifetime is removed by the
fontSizecomputes the font size based on the
lifetimewith an outExpo ease function.
paddingTopcomputes the position based on the index of a notification (older notifications are shown above newer ones) and an inExpo ease function with the
- In the
inpart of the
let-in, functions like
rightJustifiedetc. from a graphics library are used to create the viewable
Downsides of using Elm
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
Msgs 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).
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
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
On the top is an overlay layer which is drawn and used by Elm to show the sidebar and the player notifications.
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.
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.