The Caves of Clojure: Part 2

Posted on July 8th, 2012.

This post is part of an ongoing series. If you haven't already done so, you should probably start at the beginning.

This entry corresponds to post two in Trystan's tutorial.

If you want to follow along, the code for the series is on Bitbucket and on GitHub. Update to the entry-02 tag to see the code as it stands after this post.

  1. Summary
  2. State
  3. The User Interface
  4. User Input
  5. Implementation
  6. Testing

Summary

In Trystan's second post he introduces the concept of the game loop, as well as what he calls "screens": objects that handle drawing the interface and processing user input.

I could try to port his design directly over to Clojure, but instead I wanted to step back and see if I could find a way to make things more functional.

I think I've figured out a way to make it work, so I'm going to implement that.

State

When I first started thinking about how to model the game's state and the main game loop I had lots of crazy ideas bouncing around in my head. Most of them involved an immutable world (immutable! good!) and agents representing Trystan's "screens".

The more I thought about it, though, the more it looked like the agents would wind up being a tangled mess. I put down the keyboard, took a shower, had dinner with a friend, and let the problem roll around in my head for a bit.

At some point the following train of thought happened somewhere in my brain:

So instead of keeping a "world" as my state, I'm going to keep a "game".

The User Interface

If I'm going to keep track of the user interface in the "game" state, I need a way to represent it.

There are two halves to the user interface: "input" and "output". First let's consider output.

Trystan's screens are objects that handle their own drawing. At any given time there's one "active" screen object, which gets asked to draw itself. If you peek ahead in his tutorial you'll see that he ends up introducing a "subscreen" concept to get screens layered on top of each other.

Instead of having a single active screen with subscreens, I decided to keep a flat vector of screens. The last screen in the vector is the "active" one, and is effectively a subscreen of the one that comes before it.

At this point I'm going to switch terms. Unfortunately Lanterna uses the word "screen" to mean something and I didn't want to try to keep two separate concepts under the same word, so in the code I called screens "UIs", and from now on I'll be using that word.

So what is a UI in the code? Well, it's basically just a map with a :kind mapping specifying what kind of UI it is! It also might have some extra keys and values to represent its state too.

For example, at some point I expect to have a UI stack that looks something like this:

[{:kind :play}
 {:kind :throw}
 {:kind :inventory-select}]

This would be the UI stack when you were throwing something in your inventory at a monster and were choosing what to throw. Once you pick an item you'd need to target a monster, so the stack would become:

[{:kind :play}
 {:kind :throw :item foo}
 {:kind :target}]

Now the last (i.e.: "active") UI is the targetting UI, but also notice that the throwing UI has a bit of state attached to it now (the item the user picked). We'll talk about how that got there a bit later.

As I said before, the "state" for our game is going to be a "game", which consists of the world and the user interface. So our "game" object is going to be a map that looks something like this:

{:world {}
 :uis [,,,]}

For now the :world is empty. In the future it'll contain stuff like the tiles of the map, the monsters, the player, and lots of other stuff. The :uis is the UI stack.

Between the two I have enough information to draw the game fully to the user's terminal by doing something like: (map #(draw-ui ui game) (:uis game)). We'll see the real code shortly, but that's actually pretty close.

User Input

In an imperative programming style our game loop would look something like this:

  1. Draw the screen.
  2. Get some input from the user.
  3. Process that input, modifying the world.
  4. GOTO 1.

In this functional loop, I want it to look more like this:

  1. Draw the screen.
  2. Get some user input.
  3. Process the input and the game to get a new game.
  4. Recur with this new game.

How do I handle user input? Well it depends on the current UI — pressing d at the main screen will do something different than pressing it in an inventory selection screen, for example.

So the UIs need to know how to handle input. There are a number of different ways I can do that. One option might be to have :handle-input (fn ...) as part of the UI. I chose a different route which you'll see below, but that's not important for now.

The important part is that one I glossed over in the last section. How do I go from this:

[{:kind :play}
 {:kind :throw}
 {:kind :inventory-select}]

to this:

[{:kind :play}
 {:kind :throw :item foo}
 {:kind :target}]

Let's follow the proposed game loop and see what happens.

  1. Draw the play UI, then the throw UI, then the inventory UI.
  2. Get a keypress from the user.
  3. Give that keypress and the game itself to the UI input handling function to get a new game.
  4. Recur with this new game.

Step three is the tricky part. What does the inventory handler need to do to give back a new game?

It would need to pop itself off the UI stack (which is okay), put the selected item in the previous UI (a bit scary, but probably not a problem in practice), and create the targeting UI.

This last part is a deal breaker. The inventory selection UI shouldn't know anything about the targeting UI, because then I won't be able to reuse it for other functions (like equipping items, eating food, etc)!

The throw UI is the one that should know about the inventory and targeting UIs. Ideally it would set them up, get their "return values" and process those. How can I send back the values?

There's actually a really elegant way I came up with for this. At least I think it's elegant. I may end up immuting myself into a corner and ragequitting this blog series. We'll see.

Anyway, here's the solution:

And I can change the game loop to look like this:

  1. Draw the screen.
  2. If the game's input is empty, get some from the user to fill it.
  3. Process the game to get a new game. The input is now part of the game and gets processed along with it.
  4. Recur with this new game.

From what little I've used it so far, this method seems very promising.

Enough design talk. Let's look at the code.

Implementation

In Trystan's tutorial he had three "screens": start, win, and lose. The user presses keys to transition between them. Not a very fun game, but it lets you get the game loop up and running before diving into gameplay.

I did the same thing. Right now everything is in one file because I tend to code like that until I feel like something needs to be pulled out into its own namespace. The file is still under a hundred lines of code, so that's not too bad.

Let's walk through the code piece by piece. First the namespace:

(ns caves.core
  (:require [lanterna.screen :as s]))

Next I define some basic data structures:

(defrecord UI [kind])
(defrecord World [])
(defrecord Game [world uis input])

I used Clojure's records here because I feel like they add a bit of helpful structure to the code. They're also a bit faster, but that probably won't be noticeable. You could skip these and just use plain maps if you wanted to, it's really a personal preference.

Next is a helper function:

(defn clear-screen [screen]
  (let [blank (apply str (repeat 80 \space))]
    (doseq [row (range 24)]
      (s/put-string screen 0 row blank))))

Unfortunately Lanterna doesn't provide a method for clearing the screen, so I wrote my own little hacky one that just overwrites everything with spaces. It assumes the terminal is 80 by 24 characters for now.

I'll be adding a feature request in the Lanterna issue tracker for this, so hopefully I'll be able to delete this function in a later post.

Now to the meaty bits:

(defmulti draw-ui
  (fn [ui game screen]
    (:kind ui)))

(defmethod draw-ui :start [ui game screen]
  (s/put-string screen 0 0 "Welcome to the Caves of Clojure!")
  (s/put-string screen 0 1 "Press enter to win, anything else to lose."))

(defmethod draw-ui :win [ui game screen]
  (s/put-string screen 0 0 "Congratulations, you win!")
  (s/put-string screen 0 1 "Press escape to exit, anything else to restart."))

(defmethod draw-ui :lose [ui game screen]
  (s/put-string screen 0 0 "Sorry, better luck next time.")
  (s/put-string screen 0 1 "Press escape to exit, anything else to go."))

(defn draw-game [game screen]
  (clear-screen screen)
  (doseq [ui (:uis game)]
    (draw-ui ui game screen))
  (s/redraw screen))

Here we have the drawing code.

The UIs are very simple for now. They each just output a couple of lines of text. None of them actually look at the game state at all, but in the future some of them will need to do that (e.g.: when showing the list of items in the player's inventory).

I made the draw-ui function a multimethod to make it easy to define the logic for each UI separately. Each definition could even live in its own file if I wanted it to. There are other ways to do this, but I like the concision and simplicity of this one.

The draw-game function takes the immutable game object and draws some text to the user's terminal. It's fairly simple. The redraw call is needed because Lanterna double buffers the output. Check out the clojure-lanterna documentation for more information if you're curious.

(defmulti process-input
  (fn [game input]
    (:kind (last (:uis game)))))

(defmethod process-input :start [game input]
  (if (= input :enter)
    (assoc game :uis [(new UI :win)])
    (assoc game :uis [(new UI :lose)])))

(defmethod process-input :win [game input]
  (if (= input :escape)
    (assoc game :uis [])
    (assoc game :uis [(new UI :start)])))

(defmethod process-input :lose [game input]
  (if (= input :escape)
    (assoc game :uis [])
    (assoc game :uis [(new UI :start)])))

(defn get-input [game screen]
  (assoc game :input (s/get-key-blocking screen)))

UIs need to know how to process their input. I used a multimethod for this too.

The method takes the game and the input as parameters and returns a modified copy of the game object that represents the new state. Currently none of them use the "returning as input" trick, but we'll see that in one of the next few posts.

Notice how the UIs all simply replace the UI stack in the game they return? This is fine for now, but in the future they'll be more likely to just pop off the last one (themselves) rather than replace the entire stack.

An empty UI stack means "quit the game", as we'll see in a moment.

You'll also see why the input is separate from the game soon.

The get-input function gets a keypress from the user and sticks it into the game object. Nothing crazy there.

And now, the game loop:

(defn run-game [game screen]
  (loop [{:keys [input uis] :as game} game]
    (when-not (empty? uis)
      (draw-game game screen)
      (if (nil? input)
        (recur (get-input game screen))
        (recur (process-input (dissoc game :input) input))))))

Here we go. The run-game function loops on a game object each time.

First: if there are no UIs, we're done and can drop out. Cool.

If there are UIs, draw the game to the user's terminal.

Then it checks if it needs to get a keypress from the user. If so, do that, update the game object, and start again.

I could make this a bit more efficient by continuing on to process the input without another round through the loop, but performance probably isn't a concern at the moment. I'll revisit this in the future if it becomes an issue, but for now I like this structure.

Anyway, if we do have input (i.e.: either we grabbed a keypress or a UI returned something the last time through the loop), process it. Remember that the process-input function is a multimethod that dispatches on the :kind of the last UI in the stack.

Here your can see why process-input takes the game and input separately. I could just pass the game and pull out the :input value, but then I'd also need to dissoc the input from the modified game object in every UI that didn't return a value.

If I didn't dissoc the input, the input would always be present and would cause an infinite loop. You can play around with this by replacing (dissoc game :input) with game and watching what happens.

Next is a simple helper function:

(defn new-game []
  (new Game
       (new World)
       [(new UI :start)]
       nil))

Nothing fancy. You could just inline that body into the next function if you wanted, but I'm thinking ahead to when I'm going to want to generate a random world.

Finally, the bootstrapping:

(defn main
  ([screen-type] (main screen-type false))
  ([screen-type block?]
   (letfn [(go []
             (let [screen (s/get-screen screen-type)]
               (s/in-screen screen
                            (run-game (new-game) screen))))]
     (if block?
       (go)
       (future (go))))))

(defn -main [& args]
  (let [args (set args)
        screen-type (cond
                      (args ":swing") :swing
                      (args ":text")  :text
                      :else           :auto)]
    (main screen-type true)))

-main looks almost the same as before, but main has changed quite a bit. What happened?

The short answer is that most of the change is to work around some Clojure/JVM silliness. The important bit is that I now create a fresh game object and fire up the game loop with (run-game (new-game) screen).

If you're curious about the rest, read this Clojure bug report. I wanted to be able to run the game from the command line as normal, but from a REPL without blocking the REPL itself, so I could play around with things.

That's it! It clocks in at 98 lines of code. Here's the whole file at once:

(ns caves.core
  (:require [lanterna.screen :as s]))


; Data Structures -------------------------------------------------------------
(defrecord UI [kind])
(defrecord World [])
(defrecord Game [world uis input])

; Utility Functions -----------------------------------------------------------
(defn clear-screen [screen]
  (let [blank (apply str (repeat 80 \space))]
    (doseq [row (range 24)]
      (s/put-string screen 0 row blank))))


; Drawing ---------------------------------------------------------------------
(defmulti draw-ui
  (fn [ui game screen]
    (:kind ui)))

(defmethod draw-ui :start [ui game screen]
  (s/put-string screen 0 0 "Welcome to the Caves of Clojure!")
  (s/put-string screen 0 1 "Press enter to win, anything else to lose."))

(defmethod draw-ui :win [ui game screen]
  (s/put-string screen 0 0 "Congratulations, you win!")
  (s/put-string screen 0 1 "Press escape to exit, anything else to restart."))

(defmethod draw-ui :lose [ui game screen]
  (s/put-string screen 0 0 "Sorry, better luck next time.")
  (s/put-string screen 0 1 "Press escape to exit, anything else to go."))

(defn draw-game [game screen]
  (clear-screen screen)
  (doseq [ui (:uis game)]
    (draw-ui ui game screen))
  (s/redraw screen))


; Input -----------------------------------------------------------------------
(defmulti process-input
  (fn [game input]
    (:kind (last (:uis game)))))

(defmethod process-input :start [game input]
  (if (= input :enter)
    (assoc game :uis [(new UI :win)])
    (assoc game :uis [(new UI :lose)])))

(defmethod process-input :win [game input]
  (if (= input :escape)
    (assoc game :uis [])
    (assoc game :uis [(new UI :start)])))

(defmethod process-input :lose [game input]
  (if (= input :escape)
    (assoc game :uis [])
    (assoc game :uis [(new UI :start)])))

(defn get-input [game screen]
  (assoc game :input (s/get-key-blocking screen)))


; Main ------------------------------------------------------------------------
(defn run-game [game screen]
  (loop [{:keys [input uis] :as game} game]
    (when-not (empty? uis)
      (draw-game game screen)
      (if (nil? input)
        (recur (get-input game screen))
        (recur (process-input (dissoc game :input) input))))))

(defn new-game []
  (new Game
       (new World)
       [(new UI :start)]
       nil))

(defn main
  ([screen-type] (main screen-type false))
  ([screen-type block?]
   (letfn [(go []
             (let [screen (s/get-screen screen-type)]
               (s/in-screen screen
                            (run-game (new-game) screen))))]
     (if block?
       (go)
       (future (go))))))


(defn -main [& args]
  (let [args (set args)
        screen-type (cond
                      (args ":swing") :swing
                      (args ":text")  :text
                      :else           :auto)]
    (main screen-type true)))

And here are some screenshots:

Screenshot

Screenshot

Screenshot

It's not a very exciting game yet, but it all works, and I've managed to use an immutable data structure of basic maps and records to represent everything I need.

The drawing functions aren't "pure" in the "no I/O" sense, but they're kind of pure in another way — they take an immutable data structure and draw something to the screen based solely on that. I think this is going to make things easy to work with down the line.

Testing

I'll leave you with one final tidbit to read through if you want more.

Encapsulating the game state as an immutable objects means I can test actions and their effects on the world individually, without a game loop:

(ns caves.core-test
  (:import [caves.core UI World Game])
  (:use clojure.test
        caves.core))

(defn current-ui [game]
  (:kind (last (:uis game))))


(deftest test-start
  (let [game (new Game nil [(new UI :start)] nil)]

    (testing "Enter wins at the starting screen."
      (let [result (process-input game :enter)]
        (is (= (current-ui result) :win))))

    (testing "Other keys lose at the starting screen."
      (let [results (map (partial process-input game)
                         [\space \a \A :escape :up :backspace])]
        (doseq [result results]
          (is (= (current-ui result) :lose)))))))

That's pretty cool!