Ludum Dare 44 post-mortem
Apr 30, 2019
20 minute read

So, I tried doing Ludum Dare 44, and I gave up on it, and I feel like absolute crap so I’m going to attempt to write these feelings away in a celebratory post-mortem “I failed!” post.

In typical me fashion, this post is going to be a jolly mix of a lot of things - hopefully explaining what I was going for is going to make me feel better about the end result.

The timeline

Ludum Dare always has awful timing for europeans. This time, it1 started Saturday at 4:30AM, and ended Tuesday at 4:30AM.

On Saturday, I was expecting guests, so I started working on my entry at about.. 7PM. I had been thinking about the theme for a while, and had no ideas up until that point - also typical.

I worked until about midnight, when I sorta collapsed under the accumulated fatigue of the past few months, and the intoxicating power of a single 25cl beer bottle2.

I resumed work Sunday around noon (catching up on sleep) until about 7PM, and I worked on it again on Monday from 11AM to about 7PM as well. So, all in all, that’s about… fifteen hours of work, if you take out lunch and other duties (like dog walking).

I didn’t expect to get a great entry out of it, but I was hoping to get my hands on something that was fun for a few minutes.

Past entry review

I usually try to achieve two goals when doing Ludum Dare: try out a new piece of technology, and tackle a game genre I like but have never done before. (This is a flawless recipe for success, trust me!).

Let’s review a few of my past entries to get an idea of the variety:

Thorny Weather (LD38)

For Ludum Dare 38, I made Thorny Weather, a 2D top-down puzzle game using Kenney’s Game Assets:

I had fun making it, it plays somewhat nice, I was happy with the result even though very few people played it. It was my first time using Excalibur, but the rest of the stack was familiar: npm, webpack, TypeScript, electron.

Overall, the game is a solid 510. Nothing unique, not much content, Kenney’s assets makes it look better than it actually is. It didn’t get enough ratings to be ranked.

Zealot (LD40)

For Ludum Dare 40, I made Zealot, a 1v1 card game. This is one of my favourite entries. It uses React/Redux and a ton of homemade CSS to make the cards move the right way.

It even comes with a computer AI, because I sure as heck wasn’t going to implement matchmaking for a Ludum Dare entry.

I’d rate this an 810 - it came 34th overall! It was really, really polished, and Corinne’s artwork looks fantastic. The soundtrack is just a bunch of my old tracks cobbled together.

TASball (LD41)

For Ludum Dare 41, I made Tasball, which was 20% clicker game, 80% “write a program for a pinball-playing CPU”. It uses React/Redux and Pixi.js, it was my first time using Planck for the physics and tone for sound synthesis (there were CPU instructions to play notes).

I went through a bunch of iterations for the code editor interface, and the level selector even has neat little previews. The levels are actually SVG! A few friends made levels, we used Inkscape and specially-named attributes to decide what was solid, what was a goal, how much it was worth, etc.

The game came 581st overall, but 14th in innovation! It looks like ass, but I can’t remember playing a game like that before, so, yay, go us! (Sebastian Standke provided the initial idea). After the jam I improved the game somewhat and turned it into an automatically-deployed open source project (using ansible-pull).

If you’re curious, you can play it here until I stop paying for that server.

I rate TASball a 610 for effort.

Capitalism Simulator (LD42)

For Ludum Dare 42, I gave the LÖVE/moonscript combo a shot - and failed miserably. It tries to be a transport simulator, sort-of inspired by Simutrans (itself inspired by many other games, but here we are), and it’s just awful.

The game isn’t balanced or fun, the art is programmer-made in GIMP (despite all my rage, I cannot pixel art for the life of me), everything is either under-powered or over-powered, I did think it was a shame that the end result was that shitty and I just wanted to rewrite everything in TypeScript.

At the end of the day, Capitalism Simulator is a 210. I tried, I failed. Oh well.

Bamb (LD43)

For Ludum Dare 43, I tried to learn my lesson and went with something simple (not a simulator of some kind), and that worked out in the past (Zealot was great): a card game. As it turns out, it doesn’t really look like a card game, since the items are square, but oh well.

We (it was a collab with Veld) had time to make it look minimal but sort of aesthetic (we even bikeshed over the color scheme), design and implement a tutorial, I had time to compose one original music track3 for it.

Overall, for its simplicity and effectiveness (it came 28th overall, and 14th for innovation again), I also give Bamb an 810.4

About Ludum Dare 44

With old entries in mind, it’s easier to understand why I tried what I tried for Ludum Dare 44, and why I failed.

The theme is always a bad starting point when brainstorming for ideas so I figured I’d find a link later5. Nobody liked the clicker part of Tasball, but I didn’t find that surprising, because it wasn’t really fully explored.

So I thought about doing a clicker game. I very much enjoy clicker/idle games in general. If you ranked my number of hours in video games, it would probably go:

  • 12 idle games
  • League of Legends (shh)
  • The Binding of Isaac (all versions)
  • Like, six AAA franchises I actually enjoy

(But don’t tell anyone, the games I choose to spend my time on to decompress is a well-kept secret in the industry.)

I started thinking about what makes games like Cookie Clicker, Clicking Bad, A Dark Room, Swarmsim, Clickpocalypse II, Candybox 2 enjoyable.6

I decided upon a few core things:

  • Everybody loves watching numbers goes up
  • Everybody loves clicking on things as soon as they unlock

(Especially if it makes said numbers go up faster)

Of course, nice visuals and nice juice when interacting with the numbers also go a long way towards making clickers/idle games enjoyable, but I knew I didn’t have time for that.

Most importantly:

  • The most interesting part of clickers isn’t actually the “numbers going up part”

Side-quests are the meat of idle games, whether it’s just getting achievements or something more elaborate like a full gardening mini-game7. The story is also interesting, and adventure/exploration is always nice to give more depth.

“Amos”, you’re starting to worry, “that sounds like an AWFUL lot of work”. Yep, yep. I know. I know now. long sigh.

Getting started

So, the basic recipe for a clicker is pretty simple: click to make numbers go up, spend numbers on facilities that make numbers go up by themselves, repeat until you can’t afford to buy facilities, then upgrades unlock, buy those, rinse repeat.

If you want to get real fancy, you can add a “Prestige” system, where you can restart the game from scratch, but with some permanent upgrades that you’ve obtained in your previous playthroughs.

Prestiging was always out of scope because I wasn’t sure if I’d have the time to finish playing the game.8

Since ideas about making a fun unique little idle game weren’t magically popping in my head (I swear, they used to! I don’t know what happened), I decided to get something up and running.

I’ve been doing mostly Rust for the past month (I’m rewriting a project of mine), and I’ve always been curious about WebAssembly.

I had already tried compiling Go9 to WASM, but the result was unusable: a large blob that immediately tried reserving something like 2GB of RAM? (Which, in the WASM world, is “a large Typed Array”). Anyway, Go’s large binary size apparently translates to the web ecosystem, which, no thanks.

But I had read somewhere (and then immediately forgotten) that Rust had a native WASM target for its compiler. Which makes sense, because it’s based on LLVM, which, if you spend ten years figuring out its API, and another two building it once, will let you target pretty much anything.

I search for “Rust frontend library” and immediately found THE gem10, the yew project. At 7.2K stars and easily a few dozen medium articles, you’d think it would be good enough for a jam entry right?11

The README links to https://github.com/raphamorim/wasm-and-rust, which instructs you to download a set of build scripts from Mozilla games (?) for emscripten (??) so that you can build emscripten (???) yourself. Fair enough, I got a brand new i7, sure. Half an hour later, I was done reading various docs and it still wasn’t finished, so I discovered, hey, there are pre-built releases of emscripten, cool!

A few steps later in the docs, nothing worked correctly (if you think JavaScript stack traces are bad, boy do I have bad news for you), and I discovered “cargo web” by accident.

(In the meantime, I had gotten pissed that http-serve (the Python thing) didn’t set the correct mime type for .wasm files, so I was about to write my own Go static http server once and for all - in, like, 20 lines, and then I discovered that http-server (the JavaScript thing) actually does set the correct mime type)

Reading up cargo-web reminded me that rustc does have a native WASM target: wasm32-unknown-unknown12 - although, it’s experimental, yada yada13

So cargo web start does everything I wanted, which is:

  • To build my Rust code to a .wasm bundle
  • To generate JavaScript machinery to load it (because it has to be loaded asynchronously, and it’s a binary blob so we need to fetch() it)
  • To go the extra mile and generate an index.html file that references said JavaScript machinery.

Sprinkle on top: the --auto-reload flag will reload the page on changes. I expected it to use WebSockets like webpack does, but it turns out it just repeatedly hits the server to grab the “build ID”, and if it doesn’t match, it’s a location.reload() for you. Nice and simple (although noisy in the devtools).

It has been a nice enough workflow. There’s also cargo web deploy, which generates the ./target/deploy directory for you, ready to push it somewhere.14

However, I’m going to review a few aspects of the stack, for the curious in the bunch. Keep in mind that anything working at all is a miracle at this stage, and that some stuff will get better over time.15

Compilation times

I was right to be worried about compilation times.

A no-op build is 0.14s, which is honorable, but changing even one constant in the code takes 1.3s. It might not seem like much, but it adds up. To give some perspective, though, a complete build of the project as I left it, takes a whopping 1 minute and twelve seconds.16

Auto reload

Auto reload is not hot module reload. For those you who aren’t familiar with React HMR: in JavaScript, you can have an app with some state, change the code to one component, and have that component be seamlessly remounted with the new code, using the same state.

So iteration is really fast - you can get the app in some state by clicking around, and then mess in your editor, save, and a few ms later, you see the changes in your browser. I’ve gotten used to that17, so I’ve really missed that fast iteration speed in that project.

Bundle size

With a modern packer (that does dead code elimination, and minifying - hopefully with a working minifier18), you can easily get a complete app in a few hundred KBs, including whatever parts you use of your dependencies. Especially if it’s just a fun jam game.

For comparison, Super Tasball uses fuse-box, and its dev bundle is 4.1MiB. Its production bundle is 1.7MiB (452KiB gzipped).

For LD44, the current .wasm bundle size is 1.7MiB (478KiB gzipped) - so, about the same, but Super Tasball is a vastly more complicated project. It ships with a sound synthesis library, an SVG parser, a physics engine, etc. The LD44 is a bunch of constants and macros.

Error messages

Good error messages are life. They are love. I don’t want to ever go out into the wild without them. rustc has great error messages for example.

But getting an exception thrown in a Rust program compiled to WASM is not something I relish.

This particular screenshot is of a sample panic!("whoops") invocation I made. Notice the panic message anywhere? Nope? Me neither!

This was a lot of fun, especially when it panicked deep, deep inside a yew macro. I lost a bunch of times to “comment out code / compile / reload” or “just reload a bunch of times hoping the panic message makes it through” (sometimes it does!).

Again, I want to point out that it’s a miracle any of this works at all - and for what it’s worth, I do think this is one of the areas that will get better over time.

Macros

Macros are like salt, when it gets in your wounds it really fucking hurts.

In my month-long exploration of Rust I went from positively loving macros, to calling for the death of all of them, to resigning myself to use them as little as possible.

(The following few paragraphs will be obscure if you’re not well-versed into Rust or dumb programming language stuff in general, sorry)

The worst macros, of course, are procedural macros - because they let you mess with the AST in any way you see fit (even generate new identifiers in scope, yeah!), so I’ve tried hard to stay away from them.

But yew uses macros for markup - it gives you a bastardized version of JSX. Let’s review:

impl Renderable<Model> for Model {
    fn view(&self) -> Html<Self> {
        html! {
            <p>{ "Hello there" }</p>
        }
    }
}

So far, so good! All functions that render something must return Html<Model> - I don’t know why markup is parameterized by the model, don’t ask me. All text must be some rust strings? (or at least something that implements fmt::Display), and rust expressions must be in brackets. That makes sense.

Things get hairy when you want to use attributes:

fn foo(&self) -> Html<Self> {
    html! {
        <div class="message-body",>
            {"Hello there"}
        </div>
    }
}

Uh oh, looks like someone macroed their way to a dark place. You need to have a comma after every attribute. It’s super duper easy to forget.

There’s good news and bad news. The good news is: missing a comma is a compile-time error. The bad news is: the diagnostic shows anything but:

That’s just one of the many, many errors you can see if you miss a comma. This is what happens when you take that “one cool hack” and build an entire framework on top of it I guess. Anyway I’ve enjoyed having large swathes of my code squiggly-underlined in red, trying to figure out which comma I missed.

But wait! There’s more! There’s a lot of other mistakes you can do when writing yewSX code. For example, you can have mismatched opening/closing tags. (Oh, I forgot to mention: you know how rustfmt is great and formats everything? Yeah, that does not apply to macros. It’ll format bits of rust code inside brackets, but not the actual markup - fair enough).

There’s bad news and worse news there. The bad news is: it’s not a compile-time error:

The worse news is: good luck tracking it down.

The only two bits of location information above are: 1) the JS machinery that loads the wasm (but apparently also does other stuff because it’s 32KiB?), and 2) the place where the macro is defined.

Again-again, it’s impressive anything works at all, but, yeah, while iterating on a codebase getting worryingly close to over 1KLOC, this was a huge pain.

Finally, related to errors, something happened that I didn’t see coming: one of my i64 overflowed, and it just… stopped the whole application (because panic). That sort of makes sense, it was just… surprising.

Ok, so, how did it go

Well, dealing with jank is pretty much my job description so I was able to navigate my way around the limitations of the stack and start building stuff out pretty quickly.

I did get stuck for a few hours trying to define a custom type that behaves like i64, but is formatted differently (different fmt::Display impl), and can’t be coerced/promoted to i64 automatically. I’ve learned a bunch more about Rust, but I don’t know that I’m happy about that.

So, I’m there building a clicker, and I figure, hey I got an idea to make it unique. You’re playing DEATH. The Grim Reaper. Sickle and everything - ooh, ominous. You’re harvesting souls, which are our cookiescurrency, hey, life is currency, cool, theme taken care of.

Next logical step: look up what the birth rate and death rate are on earth nowadays.

Okay, so, births and deaths are expressed “per 1000 population” - that makes sense. The problem I kept running into is that, well, that goes up pretty fucking quick.

Originally I thought well okay, there can totally be an aspect of the game where.. the earth can only sustain so many humans, and growth slows down after a while (see right graph - I didn’t have the graphs when I brainstormed, but that’s pretty much what I imagined). And maybe then the humans could.. develop technology? And then the sustainable amount of population would go up? And you could optimize your soul harvest that way? Idk.

So I thought okay numbers going up is fine but what can you actually do - what if.. the souls aren’t actually yours. You’re death - you’re a facilitator. You take souls, and you give them to either Heaven or Hell (lack of time = clichés, whatever).

As a result, if you spend souls on something else (like facilities, or upgrades..), then.. you’re in debt! And maybe, like19, when you’re in debt to Heaven, you can.. do some missions for them? And they can forgive part of the debt. Or maybe you have to keep your “credit rating” above a certain level (for example, you can’t owe more than half the total population), otherwise you lose..

..and then it’s fun! Because you can lose. So it’s not just numbers going up. It’s a balancing act. You have to keep track of a bunch of things and uhh make sure they line up properly.

And maybe Hell is less forgiving than Heaven with your debt, so you’d prioritize paying them first. And also maybe Hell has better rewards (dark rewards!), so maybe at some point you just stop dealing with Heaven altogether because you built defences using your newfound dark powers and…

Yeah there’s a lot of things to pick from here, but I pretty quickly went back to “okay, okay, you’re right, it’s way too much, I need to start simple”. I needed to find a handful of systems that I could implement and balance in the remaining timeframe.

So I implemented the basics. Harvest button you can click to get 1 soul: check. Item you can buy so that you get more souls per click (SpC) - check. Item you can buy so that you start getting souls per month (SpM) - check. Buy 1/10/100 buttons because, numbers are gonna get large, better plan ahead. Even with this minimal amount of UI, it was already painful to write with yew.20

Plus it wasn’t fun. I was out of my element and running out of time so I didn’t feel like spending time messing with CSS or temporary component states to add juice to the game. It was just boringly going up. A 1s tick rate was less overwhelming for the player, but really boring. A 100ms tick rate was slightly more lively (hiding the fact that numbers weren’t interpolated), but threw the balancing completely off.

No matter how much I struggled, the population either stayed desperately low and you were out of souls to buy stuff, or it shot up waaaaaay to fast and there was no way to keep up, let alone repay your debt to heaven or hell.

I tried and I tried and I tried simplifying stuff and moving things around. Debt turned out to be hard to get right, because it was either right on the screen and then there was information overload all over, or it was hidden in tabs and then it wasn’t obvious you were in debt.

So I added other stuff: I implemented upgrades for items (that took a while to figure out, what with the borrow checker getting up in my stuff), I implemented a notification system so Heaven/Hell could speak to you, I ran out of time before I actually implemented objectives/quests.

Eventually I ended up like something that could have been a fun game, looked decent-ish, but was just really, really bad:

Conclusion

There’s no good conclusion to this. If you enjoyed this post, you might want to read some of my other stuff, which is less ranty!

You can also follow me on Twitter, I post a lot of development deep dives there whenever I’m working.


  1. The 72h jam category. I never bother doing the 48h category because I never have time to finish anything cool in 48h anyway, and I like being able to work in groups or re-use assets to make the final product better.

    [return]
  2. 28 does feel old y’all. [return]
  3. Feel free to check out the rest of both my soundclouds. Why does everybody on Twitter hate soundcloud? I don’t get it.

    [return]
  4. My rating scale doesn’t stop at 7 - a 1010 would be something like Right Click To Necromance. Also, it’s my scale - you don’t have to agree with it.

    [return]
  5. Psst: that’s what EVERYBODY DOES [return]
  6. The answer, of course, is months of careful design and balancing. Which I would later discover, to my great chagrin.

    [return]
  7. Yep, that’s in Cookie Clicker. If you haven’t played in a while, go check it out again.

    [return]
  8. Come to think of it, the exact moment I realized that would’ve been a pretty fucking good moment to completely bail out of there.

    [return]
  9. I’ve used Go to build preetty much all my contributions to the https://itch.io ecosystem: butler, a lot of backend stuff. The notable exception is the app’s frontend, which is TypeScript+electron.

    [return]
  10. Narrator: it wasn’t the gem. [return]
  11. Amos stop blaming the framework, the framework was not the problem. [return]
  12. No, I don’t know why “32” either. I’m starting to think triplets weren’t the hottest idea.

    [return]
  13. For example, apparently debug builds are broken. So it forces release builds (but still includes debug info).

    [return]
  14. I used butler, of course. Disappointingly enough, everything on that side worked first try, without anything fun to rant about. Even the itch.io embed worked great.

    [return]
  15. Other stuff won’t, because they’re flaws that are inherent to the stack. But that’s life on the edge bby.

    [return]
  16. That’s with a 12-core i7-8750H and a 2018 SSD. It’s pretty impressively costly. [return]
  17. So much so that I’ve spent hundreds of hours trying out and coming up with my own schemes for good HMR in electron. I’ve lost count of the number of projects I’ve tried, forked, patched, then abandoned - in the end, it all goes back to whatever the latest webpack is. It works okay, even though the dev dependencies are a bit heavy. Of course, now that everything is set up correctly, I don’t feel like touching that codebase ever again because fatigue. Good job me!

    [return]
  18. Turns out, minifying ECMAScript 2019 is hard, who knew? I’ve routinely run into issues where the minifier that was blessed by the webpack team generated completely whacked out code, that threw exceptions because of subtle scoping fuckups. It only took a few months of people being puzzled at random breakage for them to bless another minifier (must be 12th generation at this point), but that one also has issues, distinct from the previous gen. TL;DR stuff is hard.

    [return]
  19. These paragraphs are a lot more fun if you read them in the voice of an excited fourteen year-old, which is basically what I am at the start of every jam. Before I promptly turned into a concerned fourty-year-old, and back into a tired late-twenties human.

    [return]
  20. Ok last disclaimer: I’m not actually blaming the yew devs, and I don’t want a response from them. I was trying something and it got hard and I’m the only one to blame here. I just wanted to make that really, really clear. We clear? Ok good.

    [return]

If you liked this article, please support my work on Patreon!

Become a Patron