The Caves of Clojure: Part 3.4

Posted on July 11th, 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 (kind of) corresponds to post three 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-03-4 tag to see the code as it stands after this post.

  1. Summary
  2. Record Creation
  3. update-in
  4. Namespaces
  5. Results

Summary

In the last post I said that the next post would be about Trystan's fourth entry. I lied. I'm going to do a short entry about refactoring before I move on, because I don't want to clutter up later ones with this stuff.

Record Creation

Hacker News user bitsai told me about a new syntax for creating records in Clojure 1.3, so I updated all the (new Foo) calls to use that.

One example is the new-game function. Before:

(defn new-game []
  (assoc (new Game nil [(new UI :start)] nil)
          :location [40 20]))

After:

(defn new-game []
  (assoc (->Game nil [(->UI :start)] nil)
          :location [40 20]))

It's certainly not too impressive from a "characters saved" point of view. But this little change matters more than it might appear at first glance.

Imagine you define a record in Clojure:

(ns a)

(defrecord Foo [])

And then in another namespace you want to use it:

(ns b
  (:use a))

(new Foo)

This will explode, because Foo is actually a Java class, so you need to import it:

(ns b
  (:import a.Foo)
  (:use a))

(new Foo)

This is one example of Clojure's Java underpinnings leaking through.

In Clojure 1.3, defrecord will automatically generate a "factory function" that creates the record, and you can require or use that like any other function, so you don't need to screw around with a Java interop feature (import) to use a pure Clojure feature.

This is a good thing. It means that progress is being made toward patching the places that Java leaks into Clojure. It gives me hope that some day I'll feel okay recommending Clojure to people without Java experience.

I updated all the (new ...) calls to use the new-style factory functions. I won't paste them all here, but if you're following along you'll want to grep -R 'new ' . and update the rest now.

update-in

Next is a tiny change that's just a bit cleaner. Alan Malloy told me about it. In the process-input function for the :play UI, the code to handle smoothing the world looked like this:

\s (assoc game :world (smooth-world (:world game)))

This can be done much more cleanly using update-in:

\s (update-in game [:world] smooth-world)

Nice.

Namespaces

I said in an earlier post that I tend to leave things in one file until I feel like they need to be pulled out. Well, that time has come.

First I pulled the UI drawing code into its own file: ui/drawing.clj. It looks like this (nothing has changed, it's just in a file of its own now):

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


(def screen-size [80 24])

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


(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 any key to continue.")
  (s/put-string screen 0 2 "")
  (s/put-string screen 0 3 "Once in the game, you can use enter to win,")
  (s/put-string screen 0 4 "and backspace 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 restart."))


(defn get-viewport-coords [game vcols vrows]
  (let [location (:location game)
        [center-x center-y] location

        tiles (:tiles (:world game))

        map-rows (count tiles)
        map-cols (count (first tiles))

        start-x (- center-x (int (/ vcols 2)))
        start-x (max 0 start-x)

        start-y (- center-y (int (/ vrows 2)))
        start-y (max 0 start-y)

        end-x (+ start-x vcols)
        end-x (min end-x map-cols)

        end-y (+ start-y vrows)
        end-y (min end-y map-rows)

        start-x (- end-x vcols)
        start-y (- end-y vrows)]
    [start-x start-y end-x end-y]))

(defn draw-crosshairs [screen vcols vrows]
  (let [crosshair-x (int (/ vcols 2))
          crosshair-y (int (/ vrows 2))]
      (s/put-string screen crosshair-x crosshair-y "X" {:fg :red})
      (s/move-cursor screen crosshair-x crosshair-y)))

(defn draw-world [screen vrows vcols start-x start-y end-x end-y tiles]
  (doseq [[vrow-idx mrow-idx] (map vector
                                   (range 0 vrows)
                                   (range start-y end-y))
          :let [row-tiles (subvec (tiles mrow-idx) start-x end-x)]]
    (doseq [vcol-idx (range vcols)
            :let [{:keys [glyph color]} (row-tiles vcol-idx)]]
      (s/put-string screen vcol-idx vrow-idx glyph {:fg color}))))

(defmethod draw-ui :play [ui game screen]
  (let [world (:world game)
        tiles (:tiles world)
        [cols rows] screen-size
        vcols cols
        vrows (dec rows)
        [start-x start-y end-x end-y] (get-viewport-coords game vcols vrows)]
    (draw-world screen vrows vcols start-x start-y end-x end-y tiles)
    (draw-crosshairs screen vcols vrows)))


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

And now I need to use the draw-game function back in core.clj:

(ns caves.core
  (:use [caves.world :only [random-world smooth-world]]
        [caves.ui.drawing :only [draw-game]])
  (:require [lanterna.screen :as s]))

The fact that I moved eleven top-level symbols into a new namespace and only had to bring one of them back into the original is a pretty clear sign that this chunk of code was ready to be moved into its own file.

I also did the same for the input processing code, moving it into ui/input.clj:

(ns caves.ui.input
  (:use [caves.world :only [random-world smooth-world]])
  (:require [lanterna.screen :as s]))


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

(defmethod process-input :start [game input]
  (-> game
    (assoc :world (random-world))
    (assoc :uis [(->UI :play)])))


(defn move [[x y] [dx dy]]
  [(+ x dx) (+ y dy)])

(defmethod process-input :play [game input]
  (case input
    :enter     (assoc game :uis [(->UI :win)])
    :backspace (assoc game :uis [(->UI :lose)])
    \q         (assoc game :uis [])

    \s (update-in game [:world] smooth-world)

    \h (update-in game [:location] move [-1 0])
    \j (update-in game [:location] move [0 1])
    \k (update-in game [:location] move [0 -1])
    \l (update-in game [:location] move [1 0])

    \H (update-in game [:location] move [-5 0])
    \J (update-in game [:location] move [0 5])
    \K (update-in game [:location] move [0 -5])
    \L (update-in game [:location] move [5 0])

    game))

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

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


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

This isn't quite functional yet, because it needs the ->UI factory function, which is created by the (defrecord UI [...]) that's still in core.clj. I can't just use that in this file, because core is going to need to use some functions from this, so I'd have circular imports.

The solution is to move the (defrecord UI [...]) into a separate file. I chose to put it in ui/core.clj:

(ns caves.ui.core)

(defrecord UI [kind])

And now I can pull its creation function back into the input namespace:

(ns caves.ui.input
  (:use [caves.world :only [random-world smooth-world]]
        [caves.ui.core :only [->UI]])
  (:require [lanterna.screen :as s]))

Finally I can update the ns back in the original core.clj to pull in the functions I need, and remove the ones I don't:

(ns caves.core
  (:use [caves.ui.core :only [->UI]]
        [caves.ui.drawing :only [draw-game]]
        [caves.ui.input :only [get-input process-input]])
  (:require [lanterna.screen :as s]))

Whew!

Results

That was a lot of shuffling around, but now I've got five separate files, each pertaining to one specific thing, instead of one big pile of code.

You can view the code on GitHub if you want to see the end result.

Next post I swear I'll add the player so we'll have an actual game!