#1GAM February 2015: ZooKicker [game, releases, postmortem, ocaml, lisp]

One who makes no mistakes never makes anything.

It's Nuit blanche à Montréal, three in the morning, but I'm not out in the city, surrounded by revellers; I'm at home, hunched over an aging Thinkpad, asking myself, "Is this a game? Can I release this?". I tweak another detail, and blaze through the game's three stolen levels again, prolonging the inevitable.

It's #1GAM time again. How did I end up with another last-minute crunch after supposedly learning my lesson last month?

(I will be updating this post with links to binaries shortly; for now, you can build ZooKicker from source using my modified tsdl and extra libraries.)

How to play ZooKicker: move with the cursor keys; press space to kick a square in the direction you are facing. You can kick a square through another square, as long as it's unobstructed. The goal is to kick pairs of squares of the same color together. There are three levels.

1. Demon of the Fall

[Demon of the Fall]

I spent most of the month working on reviving a game I started writing in 2004, called Demon of the Fall.

Demon of the Fall was a way for me to pay tribute to my favorite game, Solstice (and its sequel, Equinox, and related isometric puzzle-platformers like Head over Heels). It started as a straight-forward clone, but as Retsyn and I worked together on it, we came up with some uniquely appropriate gameplay elements. I won't say too much about that here, though, because I will probably be giving Demon of the Fall another shot later this year.

All I'll say is that music is central to Demon of the Fall. Because February is also the month of the RPM Challenge, I decided I could get rid of two albatrosses at once by recording the soundtrack as my album for RPM1, and completing Demon of the Fall.

I started with the best of intentions, as we always do, and resolved to do a little work on it every day. However, the code was in an unusable state in the darcs repo I found, a casualty of a "refactoring" gone wrong.

Over the course of the year, we will examine more of these corpses, and in each case, the cause of death will be the same: refactoring without tests.2 I'm sure 2004-me could have given you countless justifications for making these changes without unit tests, but they were all wrong, as 2015-me gets to discover, again and again.

I'll talk about this more throughout the year in these #1GAM posts, but let me just relate this to what I got out of February:

Christer Kaitila talks about the wall as a reason games don't get finished. That's the point where it stops being the drug-like rush of implementing interesting stuff and becomes all about patience, discipline, and other dirty words you spend your early adult years trying to avoid.

I think that a lot of my projects had a cycle like this:

  • inspiration strikes: I hack out something in a frenzied night or weekend;
  • there's enough kindling that the fire burns while there are interesting problems to solve and clever algorithms to implement;
  • but the logs don't catch, and what remains to do is boring (which we dismiss as "too simple" to preserve our ego, though the truth is it's actually "hard but not fun");
  • time passes, and I wonder whatever happened to project X;
  • I jump in, but I realize the code is a complete hack, or I've learned a much better way to do some major structural thing, or my knowledge of whatever novel programming language I used has completely changed;
  • instead of proceeding cautiously (or better yet, just doing the hard-but-not-fun bits), I start cutting huge swathes through the code, breaking everything – "We had to destroy the code in order to save it".

Thankfully, at some point I turned around and saw the trail of dead projects stretching back for miles. Awareness was the first step; I also tried to improve not only my testing and refactoring habits, but also my version control habits (an area where DVCSes have helped a lot). Now I recognize when it's happening, and avoid the sunk cost fallacy that can accompany breaking changes ("I can't revert these commits, they were so much work!").

Anyway, it took a long time to not only undo some of that damage, but also to modernize the code and port it to Windows. Although I worked diligently, an hour or two a day was not enough. My kanban board was like a frozen river.

On February 22nd, I realized that, even if I could ignore all my other work (which I couldn't, since money pays for electricity and guitar strings), there was no way to get Demon of the Fall done by the end of the month.

2. Tricky Kick

After January, though, I had come up with multiple backup plans, in case my primary game for any month didn't pan out. These were mostly plans for clones of simple but fun games that I like. After a bit of deliberation, I decided that I would write a clone of Tricky Kick, a PC-Engine game in the fine tradition of puzzle games about kicking or shoving things, such as Kickle Cubicle and Mendel Palace.

[Tricky Kick: Oberon in the Bestiary]

Tricky Kick's puzzles have an excellent property of inducing Einstellung through suggestive placement of the pieces.

One of the reasons I had Tricky Kick in mind was that I had worked on a solver for the levels before, as well as Rush Hour and other Sokoban-like games.3 I knew it had simple mechanics I could implement quickly, and minimal art requirements.

Because of the simple, grid-oriented game state, I considered writing the game with a roguelike interface:

****************
****************
*.*****..*****.*
*.1***....***2.*
*...*....@.*...*
*...3..12..3...*
*...3..12..3...*
*...*......*...*
*.1***....***2.*
*.*****..*****.*
****************
****************

Indeed, the tests still use this ASCII representation:

let kicked_beast_with_no_obstruction_wraps_til_player () =
  compare_boards "
.....1
.1@...
" "
.....1
..@1.."
    (fun it -> move Left it; kick it)

It seemed like something that would be fun in a vector-oriented language like J, but I realized that resolving collisions would be tricky to do idiomatically in J, since they are much easier to deal with sequentially than in parallel.

I had been doing a lot of work in OCaml lately, and since I had prototyped some of my shape grammar stuff with OCaml and the Tsdl bindings for SDL2, I figured I could use a decent language and still get things done. I had delivered software under Windows and OS X with OCaml before, so I figured the porting friction wouldn't be too bad.

3. ZooKicker

In the first ten levels of Tricky Kick, you are kicking adorable animals into each other so that they explode. This is a little bizarre. ZooKicker seemed a suitable name to play on that idea, and I had figured I would draw a bunch of hyper-cute animals to kick around in keeping with that surreal theme.

[Level 2]

Of course, none of that polish got done, so the game should really be called SquarePusher RectangleSlipper. What happened?

4. What Went Right

4.1. Testing

I forced myself to write a bunch of tests for all the things that could happen on the board, and this was valuable. It would have been cool to do some fancier property-based testing, but I easily get tangled up in making fancy tests where simple, example-based tests would do. I'm glad I avoided that trap.

4.2. Music archives

Late on the final night, I dug through my archives of unfinished recordings, hoping I would have something I could use as a backing loop to then record some guitar and keyboard over to serve as music. Instead I found way more snippets than I actually needed, and I didn't even bother recording extra parts on top.

Although some of the loops I included are short, I'm happy that every level in the game has its own music, and the music is a big step up from the disaster that happened in January. Maybe by the end of the year I won't be desperately scrambling for music to add at the last minute.

5. What Went Wrong

5.1. Not sending builds to friends

Something that went right in January was uploading new ROM images daily and soliciting feedback from friends, even when the game was trivial and bugs made it unplayable.

Meanwhile, I didn't have Demon of the Fall actually running until the 17th, and I never produced standalone builds of it. With ZooKicker, I made a few attempts at producing Windows binaries and statically linked Linux binaries, but it seemed like too much of a hassle at the time.

Being able to get feedback from people early on is a big motivator, and although I am adverse to spending the little time I have each day to work on this on infrastructure tasks, it seems that it would be worth making that one of the first steps in future #1GAM developments.

5.2. Stealing Levels

Maybe this is something that went right, in a sense; it allowed me to release something. But I'm not happy with it.

I knew, when considering ZooKicker as a backup option, that one of the hardest parts would be coming up with good level designs. I persuaded myself to copy levels from Tricky Kick in order to get things working, thinking that I would have time to spend on level design. I even thought I might have enough time to adapt my solver code into some kind of procedural level generator.

No such luck. I'm sorry about that. But it's only my second greatest disappointment with this month's game; the first is the art.

5.3. Underestimating Art

Trying to learn from January's experience and Demon of the Fall, I applied the McFunkyPants method, but perhaps a little too dogmatically; or maybe I just didn't allocate enough time to the project til the end (it was mostly an hour here and there for the last week of February, until the big push on the last day). Either way, I kept my focus on the no-art playable for longer than was healthy, given that I am a slow and inexperienced artist.

Art takes time proportional to your desired quality level divided by your skill level. I had hoped to make some cute vector creatures to populate the game, but nothing reached a consistent quality level I could justify replacing the programmer art with.

Looking back, I think I could have thought further outside the box and gotten something better together: for example, I could have taken photos of small plastic farm animals (which we have around the apartment, somewhere) and used those as the animal sprites.

In the end, I added a facing indicator to the player's rectangle, which was the final admission that art was not happening this time around.

5.4. Never Trust a New Tool

I have used OCaml for many things, on and off, in the last decade, but I haven't done much game development with it.

I am a huge admirer of Daniel Bünzli's OCaml libraries, but it turned out I had made some rash assumptions about the state of Tsdl. There were no bindings for any of the usual SDL helper libraries. These libraries, such as SDL2_ttf, SDL2_image, and SDL2_mixer, are not necessarily the most full-featured or optimized implementations, but they are incredibly handy for quickly throwing together a game, and I had just assumed I would have them on hand.

So, I had to modify tsdl and create bindings for SDL2_image and SDL2_mixer.4 Of course, I end up doing that, and learning what the development workflow is for opam packages5, and learning ctypes, on the final day when I really just needed to be creating content and doing polish.

The other thing that bit me is that there seems to be no way to declare that foreign objects have dynamic extent, which I guess is a Lispism, that means (in this case) that this object should live on the stack.6

Not having dynamic extent is a huge pain in the ass, especially when interfacing with C code which often has an API designed around the idea that small, temporary structures can be cheaply setup without adding any memory pressure.

At first, my trivial no-art playable was GC'ing every few seconds, which is totally unacceptable. It is entirely possible (and desirable), when writing games in garbage-collected languages, to never trigger a GC in the inner game loop. Since I've done this before in other GC'd languages, I had assumed (given OCaml's pragmatism, in general) that this would be no problem.

Memory pressure isn't a problem for ZooKicker right now, but it did give me a scare. Anyway, efficiency isn't something I should be talking about with a game thrown together quickly like this, where List.find accounts for around 7% of the total execution time.

6. A digression: code dumps and maintained software

So, as a result of this project, I have now added two more code dumps to the FOSS landscape, despite resolving to avoid this. I'm going to write more about this soon, but I've realized that I have been a poor free software citizen over the past two decades: I would just leave a tarball somewhere to gather dust (or open a public repo, in the github era), rather than tending my open source code like a garden, and I am changing that.

I still believe code dumps are better than not releasing code at all. Sometimes it's much better to be able to build on someone's unmaintained implementation of something than to start from scratch. That said, there's something unconscientious about it.

There is, of course, an irony about pointing this out in a post about a game like this, which is often the purest form of code dump, since games are rarely maintained.

7. Segue to March

What have we learned?

  • learning lessons is hard;
  • content has to come into the picture early;
  • never do more than one new thing at once.

Is this a game? Yes. Can I release this? Yes. Am I disappointed? Sure, but March is another month, and all I can do is try again.


Footnotes:

1

I have participated in RPM every year of its existence, and I have never finished what I intended to finish (although I did finish something one year, but the less said about that, the better.)

2

Beyond that, any time you're tempted to call something a "big bang" refactoring, it's not refactoring.

3

I beat all 60 levels of Tricky Kick without using the solver, though. Click here to see the password to unlock all the levels.

4

Only enough to get ZooKicker running, for now.

5

You probably want path pins, not git pins, no matter what the opam tool tells you.

6

Add dynamic extent to the long list of features in CL that many modern languages omit, only to be rediscovered as if novel in the next wave of "system" programming languages.

JS