Asteroids: A Christmas Gift
This past Christmas, a friend gave my partner and I tickets to a show. I figured that this friend, who we'll call James[1], would enjoy the show as well, so I planned on getting him a retaliatory ticket to come join us.
But I don't like just giving people gifts, I want them to work for it know I care. Usually I do this by making some silly personalized site to go along with the actual gift. So instead of handing James a ticket to the show, I built him a shoddy knock-off of the classic game Asteroids, and hid the digital ticket in that.
The End Result
To save you the suspense, the game is live and playable (not on mobile!) at james.igotyouapresent.com, here's a screenshot:

In short, you rotate your ship with the left and right arrows, thrust (gently!) with the up arrow, and fire with the space bar. Hit rocks with your projectiles to break them into smaller rocks and eventually remove them from the game. Blasting a rock is worth 1 point, and 50 points wins you the game.
Partially as a result of technology choices (see below), partially as a result of my own unmitigated laziness, the game itself is pretty janky. But, in my opinion, it's not broken in ways that make it fundamentally frustrating to play, which is what I care about.
How It's Made
Abusing <div>s
I thought it'd be fun to forgo sane web technologies like Canvas and WebGL, which all the reasonable game libraries (PixiJS, Phaser, etc) use, and instead do the whole thing with HTML, CSS, and JavaScript. The game is a bunch of document.createElement
[2] and el.style.* = ...
calls, and I would 100% not use this approach for a project any larger than this.
The end result is probably exceptionally unperformant, with all the string-munging and attribute-changing and lack of meaningful hardware acceleration, but it plays fine on my overpowered development machine so surely it'll work for everyone.
Collisions "Quirks"
If you've played the game, you might have noticed that collision detection is considerably not-good. I'm doing the dumbest possible collision detection: pairwise bounding box comparisons. The N^2
runtime would be a problem if there were a lot of elements, but in this case N
is never more than fifty or so. The use of bounding boxes (i.e. with Element.getBoundingClientRect()
) is already a problem because none of the objects (asteroids, ship, projectiles) are particularly box-shaped, so those "sharp corners" on the boxes can trigger collision detection even when things don't visually overlap.
Compounding the problem, both the ship and asteroids are rotating, which causes the bounding box to change as well. Here's an MDN playground example, remove the transform: rotate(45deg);
from the CSS and watch how the bounding box parameters change. This manifests as some asteroids just being completely unhittable, which I'm choosing to call a "feature" because it encourages you to move around instead of just sitting in the center and spinning.
Managing Secrets
My favorite piece of building this was figuring out how to do "secret management". You generally don't want to put Things Worth Money (gift card codes, credit card numbers, etc) on the public internet, because even if a bot reading the Certificate Transparency logs[3] doesn't immediately pick it up with some entropy-detecting shenanigans or plain ole regex, a bored human somewhere on the planet eventually might.
In the past, I've handled this by giving a password to the recipient alongside the link to the game. Then, after winning the game, they enter the password, which gets POST
ed to a server and returns the actually valuable gift card code.
This works because the gift card code isn't directly in the website payload (and so can't be grabbed by bots), and I can do basic rate limiting on the server to prevent manual and/or automated brute-force attacks.
But I didn't feel like building + deploying a server this time around. Instead, to make it all work client-side, I still gave James a password, but now I used libsodium.js to encrypt the secret.
Using the password directly for encryption/decryption would be a bad idea, as it could be brute-forced without too much trouble. This is because encryption/decryption is generally designed to be as fast as it possibly can while preserving security, and the password itself doesn't contain a lot of entropy. I could give people long, randomly generated strings, but that doesn't have quite the same emotional appeal as an artisanal, hand-crafted password about something topical to the two of you (and maybe a few random numbers).
The solution for that problem is to use a key derivation function (KDF) from libsodium
to turn the password into a much stronger encryption key. The benefit of using KDFs (in this case, Argon2id) is that you can usually configure how compute- and memory-intensive they are, to make it truly impractical to brute force. Using libsodium's crypto_pwhash_OPSLIMIT_MODERATE
setting, it takes about ~one second[4] to derive the encryption key and decrypt the secret given the correct password.
The downside of this approach is that the crypto_pwhash_*
APIs are only packaged in the -sumo
version of libsodium.js
, so the JavaScript payload is an eye-watering 1 MB, which is anathema to my minimalist web sensibilities. It doesn't seem possible to tree-shake the library because it's generated directly from the C library (using Emscripten). I could have forked it and cut it down to size myself, but I wasn't willing to invest that kind of time into a one-off gift website.
Deployment
Since the finished game artifacts are just three static files (index.html
, styles.css
, main.js
), I could host the site on any static file serving platform, like GitHub Pages, Firebase Hosting, etc, etc. But like most of my projects (this blog included), I've opted to serve it out of a secondhand mini-PC running in my basement, with a sprinkling of Cloudflare for caching.
Assorted Questions
Why Asteroids specifically?
Because James is an astronomer, and we nerd out about space stuff all the time. Asteroids seemed on brand.
Have you considered giving people gifts that don't require effort from them?
Not really, no.
How many website gifts have you made?
Maybe ten or so. Some of them aren't worthy of write-ups, but I do plan to dedicate a blog post to each of the ones that are. Stay tuned!