97f882678222

LD34
[view raw] [browse files]
author Steve Losh <steve@stevelosh.com>
date Tue, 15 Dec 2015 16:36:49 +0000
parents 47f9b4b91599
children 7d304e52e098
branches/tags (none)
files content/blog/2015/12/ludum-dare-34.html media/images/blog/2015/12/silt-initial.gif media/images/blog/2015/12/silt-later.gif

Changes

--- /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
+    #<THREAD "main thread" RUNNING {1002C74703}>:
+      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: #<SB-THREAD:THREAD "main thread" RUNNING {1002C74703}>
+    0: (SB-KERNEL::INTEGER-/-INTEGER 1 0)
+    1: (/ 1 0)
+    2: (FOO)
+    3: (SB-INT:SIMPLE-EVAL-IN-LEXENV (FOO) #<NULL-LEXENV>)
+    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 #<CLOSURE (LAMBDA NIL :IN SB-IMPL::TOPLEVEL-REPL) {1004A790FB}>)
+    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 %}
Binary file media/images/blog/2015/12/silt-initial.gif has changed
Binary file media/images/blog/2015/12/silt-later.gif has changed