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.
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!