Skip to content

rlpt

Github

Serving game state using S3

Two-Up is a coin flipping game played by australians on ANZAC day. An aussie mate of mine Dave had the idea of building an online version for 2020 called 2up2point0.

Keep on flippin'

Only being up for a day (00:01 to 23:59 on 2020/04/25) and timezone differences (me: GMT, users: GMT+10ish) made reliability a key goal. I would be asleep during the first 8ish hours of usage (06:00 onwards their time) and it would be a real shame if there was a problem during this period that broke the experience.

Part of the solution was easy: use elm to build the frontend, as elm guaranties no runtime exceptions. I used create-elm-app and served the frontend as static files using netlify.

Backend was not as obvious. Something like a normal app server with a RDBMS, pushing state to clients using websockets, would work great. But that was too many moving parts for my liking.

After drinking several cups of tea and staring out the window for a while I had my solution: I would power the game using static files, writing out game state as JSON to S3.

Backend tech stack was golang, serverless, S3 and DynamoDB.

The 2up2point0 experience

A spinner flips a pair of coins up to 5 times. If the coins land HH or TT then the round is over, the outcome being called Heads or Tails respectively. If 5 spins happen without either HH or TT, that's called Odds. Players place bets on the outcome. If the result is Tails then the spinner is changed.

Starting with those core rules our experience ended up looking like this:

  • A round is played every minute.

  • All players see the same coin flips.

  • Players bet virtual bucks, getting a $100 balance on sign up, and can bet either Heads or Tails. Doubling their stake if they win. Winnings are added to their balance.

  • Balance == score, with a global leader board.

  • Spinner names are picked randomly from a list of famous Australians

    All players seeing the same coin flips was key, that way people could play together and have a bit of banter using zoom/skype/whatever.

    No real money involved, but Rocks Brewing donated some booze based prizes we could hand out to top scorers. There was also a DONATE button which took players off to woundedheroes.org.au so they could optionally make a donation to charity.

Deriving the game number from unix time

This was the breakthrough that made all this work: If we play every minute we can derive a game number using unix time. Just take the integer value (which should be seconds since epoch), divide by 60 (so it's now minutes since epoch) and round it back to an int.

In go:

In js:

This means the frontend and backend can always agree what the current game number is, and what the next one is (assuming their clocks are correct!) without needing to talk to each other.

For example the game at 07:13 on the 25th of April would be 26463313

You may of noticed that the frontend code snippet is js, not elm. That is because I did some of the timing/game-number logic in js (and sent it into the elm app using ports) so I could use cron-scheduler to trigger stuff every minute, on the minute

Main game loop

  1. A small golang app (called spinner) running on a cheap VPS plays a game every minute, on the minute (e.g 07:13:00). It writes out the result of this game to a S3 bucket using the game number as the filename, e.g 26463313.json. This bucket allows public reads.
  2. At 10 seconds past the minute (e.g 07:13:10) the frontend works out the current game number, then fetches the result file over HTTP (no auth needed, just the url e.g https://somebucketurl.com/26463313.json). The 10 second delay is to ensure step 1 has had the time to fully complete.
  3. Frontend shows the game being played. This is all smoke and mirrors, the result is already known before the animation even starts.
  4. Continue forever.

Example of a game state JSON file:

So apart from spinner, this entire game loop is powered by static files. Frontend is static, game results live in static files, and no extra synchronisation needed between client & server.

spinner was a single point of failure. If it died no new games would be played. But as it received no inbound traffic/requests, the surface area of things that could go wrong was nice and small: either a bug in its own code (there was not much of it), or the underlying/network server dying.

Lets have a flutter

Along side this main game loop was a small API to do sign ups and "betting". Lamdba to provide the endpoints and DynamoDB to store a user table and bet table.

The frontend only allowed betting on the next game. So bets could be processed immediately after a game's result had been decided. Then a full scan of the user table would be done (where balance was stored), and the leader board scores written out to the same bucket as the game results. One file per game, filename being game number with scores appended e.g 26464320-scores.json. Frontend could then fetch these files every game to update the leader board view.

No alarms and no surprises please

I promised Dave I would sleep with my phone off silent just in case there was an issue. I awoke at my normal time with no missed calls or panicked emails. After the all important kettle interaction I checked out the site and all was running smoothly. By the end of the day we had over 600 players, which seems quite respectable considering all this was a last minute for a bit of fun.