# HG changeset patch # User Steve Losh # Date 1450197409 0 # Node ID 97f882678222938f1a835fd53f8545269bbda699 # Parent 47f9b4b91599ad48adc9a646f6963305ba0eedad LD34 diff -r 47f9b4b91599 -r 97f882678222 content/blog/2015/12/ludum-dare-34.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/blog/2015/12/ludum-dare-34.html Tue Dec 15 16:36:49 2015 +0000 @@ -0,0 +1,483 @@ + {% extends "_post.html" %} + + {% hyde + title: "Ludum Dare 34 Postmortem" + snip: 'I made a "game"!' + created: 2015-12-15 16:20:00 + %} + + {% block article %} + +This past weekend was [Ludum Dare 34][]. Ludum Dare is a thrice-a-year event +where a theme is chosen and people have 48 hours (for the competition) or 72 +hours (for the "casual" jam) to create a game based on a theme. + +I actually managed to have some free time for a change, so I decided to give it +a go. I've got a lot of experience programming and a little bit in "sprinting" +like this (my team [placed second in Django Dash][Django Dash] a few years ago), +but I haven't made very many games. Still, by the end of the jam I managed to +make something that's not quite a game but *is* pretty fun to play around with. +I figured I'd write about the process while it's all still fresh in my mind. + +[Ludum Dare 34]: http://ludumdare.com/compo/ +[Django Dash]: http://djangodash.com/judging/c2/results/team/49/ + +[TOC] + +## My Game + +The theme this year was actually *tied* in voting, so there were two: + +* Growing +* Two Button Controls + +### Language & Code + +I knew going in that I wanted to make something with ASCII graphics with Clojure +and [clojure-lanterna][]. Once I thought about the themes I decided to make +a simulation of a world with plants and animals. I settled on the name "Silt" +(the stuff at the bottom of riverbeds that slowly accumulates and reshapes the +world) and got to work. + +You can get the source for the game [on BitBucket][bitbucket]. There's also +a mirror on GitHub if you prefer git. + +### Download + +There's a prebuilt jar file [on BitBucket][bitbucket] if you want to play the +game. + +### Gameplay + +You are the god of a [toroidal][] world. Initially it's inhabited by a small +population of four hundred and one creatures. Time ticks by at a few ticks per +second. + +[![Silt Initial World](/media/images{{ parent_url }}/silt-initial.gif)](/media/images{{ parent_url }}/silt-initial.gif) + +The creatures need energy to survive. They can get energy by eating fruit from +shrubs or being near water. + +If a creature has enough energy it might reproduce by splitting off a clone of +itself. The clone will be identical to its parent (sibling?), though there is +a slight chance of mutation. + +The ideal body temperature for a creature is 0 degrees. The world starts at +that temperature, creating a paradise. As the god of the world, the only way +you can interact with it is to increase or decrease the temperature. + +If the temperature of the world is different than the temperature of a creature, +they will need to spend more energy to maintain their body heat. This makes it +more difficult to survive. Swinging the temperature in large increments quickly +is a sure way to kill off the population. + +The creatures are not entirely at the mercy of the climate, however. Creatures +have an "insulation" score that represents fur or skin. More insulation +protects the creature from the outside climate, at the expense of a small amount +of energy. + +The mechanics of temperature/insulation and reproduction/mutation interact to +cause evolution. If you slowly increase the temperature of the world over a few +thousand ticks, children with more insulation are more likely to survive, and so +your creatures will tend to evolve more insulation over time. + +Creatures' movement patterns are also affected by mutation. Over time your +creatures will likely evolve to wander instead of staying stationary, because +fruit takes time to grow and so it's more effective to keep moving and +gathering. + +Creatures can also change their colors and glyphs through mutation, but this is +much rarer. After letting the game run for a while you'll probably notice +"gangs" of creatures with similar characteristics, all descended from a single +parent. + +[![Silt Later World](/media/images{{ parent_url }}/silt-later.gif)](/media/images{{ parent_url }}/silt-later.gif) + +Finally, there are eight mysterious objects scattered throughout the landscape. +Each one does something, but discovering exactly *what* will be difficult +(unless you read the source). + +### Controls + +The controls are pretty basic: + +* **`arrow keys`** to move your view of the world. +* **`R`** to reset the world. +* **`escape`** to quit the game. + +Put your cursor over a creature to see their stats: + +* **`hjkl`** or **`wasd`** to move your cursor. + +The world ticks along, but you can freeze time: + +* **`space`** to pause/unpause time. + +Those are the basic controls. To actually *interact* with the world you have +only two options (in accordance with the "Two Button Controls" theme): + +* **`+`** to make the world one degree hotter. +* **`-`** to make the world one degree colder. + +[clojure-lanterna]: http://sjl.bitbucket.org/clojure-lanterna +[bitbucket]: https://bitbucket.org/sjl/silt +[toroidal]: https://en.wikipedia.org/wiki/Torus + +## Good Choices + +I managed to make a working, fairly interesting game within the time limit, so +I'm pretty happy with most of my choices. + +### Familiar Environment + +I decided to make Silt in Clojure. If you follow me on [Twitch][] you might +have noticed I've been getting more into Common Lisp these days. I don't really +like writing in Clojure any more, but I used it for the jam because I wanted an +environment I was familiar with. + +This was a good decision. I wouldn't have finished if I tried to use Common +Lisp -- I'd probably still be struggling with whatever its Curses library is. + +A 72-hour jam is not the time to be trying out a new language or framework +you've never used before if you want to have a finished product at the end. +Pick something you know well so you don't spend 30% of your time trying to +Google things. + +[Twitch]: http://twitch.tv/stevelosh + +### Streaming and Talking + +I streamed a bit of the coding process a couple times over the weekend (look in +past broadcasts and they might still be there). I didn't have a ton of viewers, +but it was good to talk things out. Sometimes the stream was my [rubber duck][] +and it helped a bunch. + +[rubber duck]: https://en.wikipedia.org/wiki/Rubber_duck_debugging + +### Releasing Before the End + +Ludum Dare ends at a certain time, but there's a "release hour" afterword when +you can take an hour to package up the final version of your game. I think it's +better if you don't leave it until then to run through your packaging/release +process at least once, though. Once I had the game in a runnable (if not yet +very fun) state, I made it into a jar to make sure that everything works. + +You don't want to find out your build +process is fucked when you only have 40 minutes left, so do a dry run early and +discover any snags while you have lots of time to fix them. + +### Getting Feedback + +Once I had a playable game I asked my friend [Hafdís][] to see if it would run +on Windows. Luckily it did -- Java and Lanterna are pretty nicely +cross-platform. + +She ran the game in the background for a while and showed me the results later. +It's really helpful to get a second opinion about your game (or website or +whatever you're making) as you're working on it. It's too easy to get too close +to your own game and not realize what's fun or painful about it any more. + +Asking in the IRC channel can work, but generally everyone there is busy working +on their own games so you won't get a ton of takers. You can ask someone +online, but I think in-person is better if at all possible. Seeing someone else +enjoy something you've made is a big morale boost, which can be important when +you're dealing with tricky bugs later. + +[Hafdís]: http://havethis.info/ + +### Profiling and Performance + +During the last day I spent an hour or so using a profiler to identify the +hottest spots in the code and improving performance. I didn't focus too much on +speed, but a little bit of attention can go a long way. + +### Ruthless Simplicity + +One of the main reasons I managed to finish despite my lack of experience making +games was being ruthless about making the game as simple as possible. I have +a ton of ideas that I could potentially do, but cramming them all into the game +would take far too long and would probably create a giant mess. + +Focusing on one core mechanic (temperature) and building the things that +interact with it got me pretty far. The fact that the theme was "Two Button +Controls" really helped me a lot with this, because it forced me to come up with +a mechanic simple enough to encode in two buttons. + +### Sleeping + +I was hanging out in the official IRC channel over the weekend and noticed +a bunch of people talking about how tired they were and how little sleep they +were getting. + +Because the time frame is so short there's a tendency to want to stay awake for +as much time as possible to use as many hours as you can. I think this is +usually a mistake. During this weekend, and also for the Django Dash weekend, +I stuck to my normal sleeping/waking schedule and I think it was the right +choice. You may get *more* hours of work in if you don't sleep, but you get +*better* hours of work if you're rested, fed, and showered. + +This is especially tricky if your timezone is offset with the event. The themes +were announced at 2 AM my time, and while I would have loved to be awake and +dive in right away I chose to go to sleep instead and just get started the next +day. A screwed up sleep schedule is almost as bad as not enough sleep (for me +at least). + +## Bad Choices + +I made a few mistakes along the way too (luckily none of them prevented me from +finishing). + +### Clinging to Failed Mechanics + +One of the first mechanics I added to the game was aging -- creatures would age +over time and eventually die of old age. Immediately I had trouble balancing +this against reproduction to avoid population explosions and extinctions, but +I stuck with it for quite a while trying to make it work. + +Eventually I tore the entire aging mechanic out and replaced it with hunger, +which ended up working far, far better. In hindsight I should have abandoned +aging much sooner, as soon as it was obvious that it was making things painful +and unfun. + +### Last-Minute Additions + +I added a couple of mysterious objects right before I packaged up the final +version of the game and I didn't have time to playtest them very much. I tested +that they didn't crash the game, but I assumed their effects would be minor +curiosities and didn't worry too much. + +Now that I've tested it a bit more, they have a bigger effect than I thought +and drastically affect the world. I should have either tested them more or held +off on them until I had time to test. + +### Working Alone + +This one was unavoidable for me this time, but I'm sure Silt could have been +a lot more interesting if I had worked with another programmer, an artist, or +a sound designer. + +## Clojure + +I'll end with a few notes on the experience of writing Silt in Clojure. Like +I said earlier: I don't particularly like Clojure any more, but it was familiar +so I ran with it. After doing a bunch of Common Lisp lately a few things jumped +out at me about writing in Clojure, so I wanted to get them down on paper before +I forget. + +### Destructuring is Nice + +Clojure's ubiquitous destructuring is really nice. Common Lisp has +`destructuring-bind` but it's not as powerful as it is in Clojure, and it's +certainly not threaded through the language nearly as much. + +Destructuring in function arguments is *really* handy -- I almost want to write +a `defun-destructured` macro that will let me do it in CL. + +### Fewer Superfluous Parentheses + +In Clojure's syntax there seems to be a general theme of using fewer +parentheses. I don't just mean using brackets and curly braces for other +literals either. Take the humble `let` in Clojure: + + :::text + (let [x (foo) + y (bar)] + (+ x y)) + +If you translate the vector literal to a Common Lisp list, it would look like +this: + + :::text + (let (x (foo) + y (bar)) + (+ x y)) + +But Common Lisp actually makes you surround the binding pairs with *another* set +of parentheses: + + :::text + (let ((x (foo)) + (y (bar))) + (+ x y)) + +This is annoying. There are always going to be pairs of binding forms, why do +I need to wrap them in extra parens? Clojure's way is cleaner. + +### The Tooling is Good + +Jvisualvm's statistical profiler is a bit homely, but it gets the job done. +It's nice to have mature tools to introspect what your code is doing at runtime. + +`lein uberjar` just worked and produced a jar that runs fine on Windows and OS +X. This was a great relief. I haven't looked at packaging up a Common Lisp app +but I am not optimistic. + +### But the Compiler Is Not Helpful + +SBCL warns me when I'm doing something stupid: + + :::lisp + CL-USER> (defun square (x) (* x x)) + + SQUARE + CL-USER> (defun bad (x y) + (+ x (square y y))) + ; in: DEFUN BAD + ; (SQUARE Y Y) + ; + ; caught STYLE-WARNING: + ; The function was called with two arguments, but wants exactly one. + ; + ; compilation unit finished + ; caught 1 STYLE-WARNING condition + + BAD + CL-USER> + +Clojure happily lets me be stupid without a peep: + + :::clojure + user=> (defn square [x] (* x x)) + #'user/square + user=> (defn bad [x y] + #_=> (+ x (square y y))) + #'user/bad + user=> + +I'm often stupid, so I prefer a compiler that helps prevent it. + +### Errors are Still Terrible + +Clojure stack traces have historically been awful. When I ran into my first +error this time I noticed that the stack trace was... nonexistent? + + :::clojure + user=> (defn foo [] (/ 1 0)) + #'user/foo + user=> (foo) + + ArithmeticException Divide by zero clojure.lang.Numbers.divide (Numbers.java:158) + user=> + +I guess they got tired of people complaining about how the stack traces look +like shit, so they just... removed them? You can get them back with a bit of +work: + + :::clojure + user=> (use '[clojure.stacktrace :only [print-stack-trace]]) + user=> (print-stack-trace *e) + java.lang.ArithmeticException: Divide by zero + at clojure.lang.Numbers.divide (Numbers.java:158) + clojure.lang.Numbers.divide (Numbers.java:3808) + user$foo.invoke (form-init5869611501548592997.clj:1) + user$eval1219.invoke (form-init5869611501548592997.clj:1) + clojure.lang.Compiler.eval (Compiler.java:6782) + clojure.lang.Compiler.eval (Compiler.java:6745) + clojure.core$eval.invoke (core.clj:3081) + clojure.main$repl$read_eval_print__7099$fn__7102.invoke (main.clj:240) + clojure.main$repl$read_eval_print__7099.invoke (main.clj:240) + clojure.main$repl$fn__7108.invoke (main.clj:258) + clojure.main$repl.doInvoke (main.clj:258) + clojure.lang.RestFn.invoke (RestFn.java:1523) + clojure.tools.nrepl.middleware.interruptible_eval$evaluate$fn__623.invoke (interruptible_eval.clj:58) + clojure.lang.AFn.applyToHelper (AFn.java:152) + clojure.lang.AFn.applyTo (AFn.java:144) + clojure.core$apply.invoke (core.clj:630) + clojure.core$with_bindings_STAR_.doInvoke (core.clj:1868) + clojure.lang.RestFn.invoke (RestFn.java:425) + clojure.tools.nrepl.middleware.interruptible_eval$evaluate.invoke (interruptible_eval.clj:56) + clojure.tools.nrepl.middleware.interruptible_eval$interruptible_eval$fn__665$fn__668.invoke (interruptible_eval.clj:191) + clojure.tools.nrepl.middleware.interruptible_eval$run_next$fn__660.invoke (interruptible_eval.clj:159) + clojure.lang.AFn.run (AFn.java:22) + java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1142) + java.util.concurrent.ThreadPoolExecutor$Worker.run (ThreadPoolExecutor.java:617) + java.lang.Thread.run (Thread.java:745) + +Oh yeah, *there's* the garbagepile I remember. + +Lisp also hides the traceback at first, but it's easier to get at, and once you +do it's actually *helpful*: + + :::lisp + CL-USER> (declaim (optimize (debug 3))) + + NIL + CL-USER> (defun foo () (/ 1 0)) + ; in: DEFUN FOO + ; (/ 1 0) + ; + ; caught STYLE-WARNING: + ; Lisp error during constant folding: + ; arithmetic error DIVISION-BY-ZERO signalled + ; Operation was /, operands (1 0). + ; + ; compilation unit finished + ; caught 1 STYLE-WARNING condition + + FOO + CL-USER> (foo) + + debugger invoked on a DIVISION-BY-ZERO in thread + #: + arithmetic error DIVISION-BY-ZERO signalled + Operation was /, operands (1 0). + + Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. + + restarts (invokable by number or by possibly-abbreviated name): + 0: [ABORT] Exit debugger, returning to top level. + + (SB-KERNEL::INTEGER-/-INTEGER 1 0) + 0] backtrace + + Backtrace for: # + 0: (SB-KERNEL::INTEGER-/-INTEGER 1 0) + 1: (/ 1 0) + 2: (FOO) + 3: (SB-INT:SIMPLE-EVAL-IN-LEXENV (FOO) #) + 4: (EVAL (FOO)) + 5: (INTERACTIVE-EVAL (FOO) :EVAL NIL) + 6: (SB-IMPL::REPL-FUN NIL) + 7: ((LAMBDA NIL :IN SB-IMPL::TOPLEVEL-REPL)) + 8: (SB-IMPL::%WITH-REBOUND-IO-SYNTAX #) + 9: (SB-IMPL::TOPLEVEL-REPL NIL) + 10: (SB-IMPL::TOPLEVEL-INIT) + 11: ((FLET #:WITHOUT-INTERRUPTS-BODY-86 :IN SAVE-LISP-AND-DIE)) + 12: ((LABELS SB-IMPL::RESTART-LISP :IN SAVE-LISP-AND-DIE)) + +Notice how it actually warns me *at compile time* about the error, whereas +Clojure merrily compiles it without a peep. Also notice how the SBCL backtrace +gives me actual forms and arguments, instead of a line number (which is probably +inaccurate if you've been editing and eval'ing a file piecemeal). + +### Old Bugs + +Running into [four-year old bugs][clj-700] was fun. + +### RIP Your Heap + +Clojure's data structures and STM system make it pretty easy to write code that +both: + +* Runs correctly +* Generates enormous mountains of garbage + +This can be a good or bad thing, depending on how you look at it and what your +needs are. + +Common Lisp has immutable data structures (via fset) and STM if you want them, +but it also lets you use efficient mutable things just as easily if you want. +It doesn't really encourage one specific way of doing things. + +[clj-700]: http://dev.clojure.org/jira/browse/CLJ-700 + +## Conclusion + +Ludum Dare 34 was a lot of fun. I'm definitely going to mark my calendar and do +the next one too. + +If making a game sounds interesting, you should do it too! You don't need a ton +of programming experience, artistic skills, or anything but a love of video +games and a free weekend. Get out there and make a game! + + {% endblock article %} diff -r 47f9b4b91599 -r 97f882678222 media/images/blog/2015/12/silt-initial.gif Binary file media/images/blog/2015/12/silt-initial.gif has changed diff -r 47f9b4b91599 -r 97f882678222 media/images/blog/2015/12/silt-later.gif Binary file media/images/blog/2015/12/silt-later.gif has changed