Making browser games more secure with Elm, Part Two

How we built replay-as-authentication with Elm

Erkal Selman
diesdas.direct
Published in
6 min readSep 15, 2021

--

In the preceding post, we shared how we got into game development explained how Elm allowed us to build a reliable record/replay system to validate scores. In this post, we will go more into detail together with a working example. Let’s first go through the example, so the “what” part of this blog post becomes more clear. Then we will go into “why” and “how”.

Here, you can play the game. Click the ‘Play’ button and play the game to the end without changing the browser tab.

Start screen. The recording did not start yet.

During your gameplay, the framework records all keyboard inputs, mouse movements, and touch positions. When the game is finished, this long sequence of recorded inputs is compressed and sent to our database. The final, ‘claimed’ score is also sent to our database. The database responds with a unique ID that can be used to query the compressed recording and the claimed score.

End Screen. The recording has been compressed and sent to the server.

Once the game’s completed the ‘Validate’ button links the player to the Record Player page, using the unique ID as a query string. Here is the link for an example recording (in case, you don’t want to wait until the game is over).

The record player page. The game is being simulated using the same sequence of inputs

The Record Player page queries the database using the unique ID and runs a simulation of the game — using the same sequence of inputs recorded during play. Once completed it checks whether the score reached matches the initially claimed score. This is how we solved the score validation problem.

What we mean by reliability

There are many aspects to reliability in game development. We believe our record/replay system is reliable in that it:

  • prevents cheating: creating a fake recording is very difficult.
  • is safe for developers: those who build the game can’t break the system.

Protection against cheating is a prerequisite for every score validation system — we won’t delve into that here. (Max talks about it in the first blog post.) In this post, we want to clarify the second aspect of reliability and explain how Elm makes it possible.

Multiple games maintained by multiple developers

Over the last two years, we have built around 20 games with Elm. Some of them have been built by junior developers that didn’t have Elm experience, they learned the language by creating a game.

Having multiple developers with varying levels of experience means it’s really important that the system is unbreakable. In fact, the developer who makes the game should not worry or even know about the record/replay system. It should be hidden completely from game developers and just work out-of-the-box.

These expectations may seem very high, but as we’ll see, they are easily achievable in Elm.

Restrictions you can’t impose in JavaScript/TypeScript

Imagine that we were building our games and our game framework using Javascript instead of Elm. Then, for example, a game developer could easily

  • listen to keyboard or mouse clicks,
  • generate a random number, or
  • query computer time

in a different way than the game framework provides and use them such that they affect the gameplay and the score. In this case, our Javascript framework would not be able to keep track of this input and the record/replay system would break.

The solution in Elm: Controlling the main

To understand how this problem can be solved in Elm, let’s look at Elm Playground. Our game framework, in essence, is very similar to it. In short, Elm Playground does

  • render your shapes to SVG and
  • makes it easy to access things like mouse input or screen size by feeding Computer as an argument to your view and update functions. (See Playground.game)

But what is more important is that Elm Playground imposes many restrictions on its user. It achieves this by controlling the main function. The following are the first two lines from an online example.

import Playground exposing (..)main = game view update (0,0)

Even if this program consists of thousands and thousands of lines, by just checking the above lines, meaning that by only observing that the main is defined using the Playground.game function, we can be sure, for example, that the only way this program accessed the mouse position was by looking into the Computer. Our framework records only the actions that update the Computer. And Elm takes care of the rest: it makes sure that the game developer who uses the framework has access to things like time or user input only via Computer and not in any other way! This degree of guarantee is possible thanks to The Elm Architecture.

Let us explain this a little further for the reader who is not familiar with Elm. Every Elm application must define a main value of type Program. The only way to create a Program is using Platform.worker or one of the functions in the Browser package. These let you define the run-time behavior of your program by means of pure functions. Thanks to the limits of Elm/JS Interop, Elm has a very well-defined boundary between

  • ⛈ the outside world where time exists and things change and
  • 🌈 the Elm program where time does not exist and everything is a constant

We say, Elm has managed effects.

Our recording system relies on this well-defined boundary. Therefore, it is unbreakable by the game developer. Without Elms managed effects we wouldn’t attempt building this type of record/replay system.

Recordings as small as 10kb

Because the game recordings are stored on the server, we wanted them to be as small as possible. A recording is a long list of TickInputs:

type alias TickInput =
{ playerInputs : List PlayerInput
, deltaTimeInMillis : Int
}
type PlayerInput
= KeyDown String
| KeyUp String
--
| MouseDown Point
| MouseMove Point
| MouseUp
--
| TouchStart (List TouchEvent)
| TouchMove (List TouchEvent)
| TouchEnd (List TouchEvent)
| TouchCancel (List TouchEvent)
--
| Wheel WheelEvent

In our games, we don’t use fixed time steps. Therefore, we had to record the delta time for each time step. This means that for every second of gameplay, we record approximately 60 TickInput's. That makes our recordings very long. To compress the recordings, we've used the packages MartinSStewart/elm-serialize, danfishgold/base64-bytes, and folkertdev/elm-flate in combination. In addition, we have bounded the deltaTimeInMillis by 255 from above and encoded it to a single byte using Serialize.byte. In the end, we were able to compress a 2 minutes game recording to around 10 kilobytes, which was small enough for our purposes.

Conclusion

Elm has been criticized for not having a traditional Foreign Function Interface with JavaScript and not allowing ports in packages. What we have shown here is that thanks to those limitations, Elm allowed us to fulfill requirements that other languages would not have been able to.

diesdas.digital is a studio for strategy, design and code in Berlin, featuring a multidisciplinary team of designers, developers and strategists. We create tailor-made digital solutions with an agile mindset and a smile on our faces. Let’s work together!

Curious to learn more? We’re also on Twitter and Instagram and we highly recommend our Tumblr. You could also subscribe to this publication to get notified when new posts are published! That’s all. Over & out! 🛰

--

--