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.
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.
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:
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.
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'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.* *...*[email protected]*...* *...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 [email protected] " " .....1 [email protected]" (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.
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.
Of course, none of that polish got done, so the game should really be
SquarePusher RectangleSlipper. What happened?
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.
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.
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.
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.
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.
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_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_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.
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.
What have we learned?
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.
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.)
I beat all 60 levels of Tricky Kick without using the solver, though. Click here to see the password to unlock all the levels.
Only enough to get ZooKicker running, for now.
You probably want path pins, not git pins, no matter what the opam tool tells you.
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.