--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/content/blog/2012/07/caves-of-clojure-05.html Fri Jul 13 01:10:32 2012 -0400
@@ -0,0 +1,481 @@
+ {% extends "_post.html" %}
+
+ {% hyde
+ title: "The Caves of Clojure: Part 5"
+ snip: "Fungus and more."
+ created: 2012-07-13 09:35:00
+ flattr: true
+ %}
+
+{% block article %}
+
+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 four in Trystan's tutorial][trystan-tut].
+
+If you want to follow along, the code for the series is [on Bitbucket][bb] and
+[on GitHub][gh]. Update to the `entry-05` tag to see the code as it stands
+after this post.
+
+Also, I live streamed myself writing the code that this entry is based on. You
+can view the recordings [on twitch.tv](http://www.twitch.tv/stevelosh/), though
+as I write this the video links are stuck in an infinite HTTP redirect loop.
+Perhaps they will be fixed eventually.
+
+Finally, I've started hanging out in `##cavesofclojure` on Freenode if you have
+questions. I may or may not be around at any given point.
+
+[the beginning]: /blog/2012/07/caves-of-clojure-01/
+[trystan-tut]: http://trystans.blogspot.com/2011/09/roguelike-tutorial-05-stationary.html
+[bb]: http://bitbucket.org/sjl/caves/
+[gh]: http://github.com/sjl/caves/
+
+[TOC]
+
+Summary
+-------
+
+In Trystan's fifth post he adds three things:
+
+* A stationary monster
+* Attacking
+* A growing mechanic for the monster
+
+I'm going to add all three of those too, though I'll be doing it in the
+entities/aspects fashion of the previous post.
+
+Multiple Entities
+-----------------
+
+First thing's first: I need to change a bit of code around to account for having
+multiple entities instead of just a single player.
+
+At the end of the previous post the player was stored in the world directly,
+meaning the `game` object looked like this:
+
+ :::clojure
+ {:uis [...]
+ :world {:player {...}
+ :tiles [...]}}
+
+I decided to remove some (but not all) of the special casing for the player and
+make them an entity like any other. The `game` object is now structured like
+this:
+
+ :::clojure
+ {:uis [...]
+ :world {:entities {:player {...}}
+ :tiles [...]}}
+
+Notice that the player has been moved into an `:entities` map, keyed by its id
+(which is still the special-cased `:player`). I updated anywhere that needed to
+change to account for this, and made sure it still worked. This was pretty easy
+to do by just searching for `:player`, as the codebase is still small.
+
+Lichens
+-------
+
+Now it's time to actually add another type of entity. I play a lot of Nethack,
+so naturally I decided to make it a simple lichen. I added
+`entities/lichen.clj`:
+
+ :::clojure
+ (ns caves.entities.lichen
+ (:use [caves.entities.core :only [Entity get-id]]))
+
+
+ (defrecord Lichen [id glyph location])
+
+ (defn make-lichen [location]
+ (->Lichen (get-id) "F" location))
+
+
+ (extend-type Lichen Entity
+ (tick [this world]
+ (if (should-grow)
+ world)))
+
+Like the `Player`, `Lichen`s implement the `Entity` protocol. For now they
+don't do anything special during a tick.
+
+You may have noticed the new `get-id` function. Entities must have IDs so I can
+get them in and out of the entity map. The player has a special ID of
+`:player`, but I needed a way to get a unique ID for other entities.
+
+The simplest way I could think of was to use a simple counter over in
+`entities/core.clj`:
+
+ :::clojure
+ (ns caves.entities.core)
+
+
+ (def ids (ref 0))
+
+ (defprotocol Entity
+ (tick [this world]
+ "Update the world to handle the passing of a tick for this entity."))
+
+
+ (defn get-id []
+ (dosync
+ (let [id @ids]
+ (alter ids inc)
+ id)))
+
+Not the prettiest solution, but it works. I might switch to a UUID library or
+something in the future, but this'll do for now.
+
+Populating the World
+--------------------
+
+Unlike the `make-player` function, `make-lichen` takes a location directly
+instead of trying to find an empty space for itself in the world. I figured it
+was better to not have entities deciding where they emerge in the world all the
+time! I went ahead and refactored `make-player` to act like this as well.
+
+During this coding session I wasn't actually running and playing the full game
+through as much as I should have been. I think as I get more and more of the
+basic structure of the game in place I'll be able to do this more. Up to now
+I've been doing large, sweeping refactorings that touch many different pieces of
+code and break everything until they're finished.
+
+Anyway, back to the world. I need a way to spawn some lichens in the world, so
+I edited the `reset-game` function in `input.clj`:
+
+ :::clojure
+ (defn add-lichen [world]
+ (let [{:as lichen :keys [id]} (make-lichen (find-empty-tile world))]
+ (assoc-in world [:entities id] lichen)))
+
+ (defn populate-world [world]
+ (let [world (assoc-in world [:entities :player]
+ (make-player (find-empty-tile world)))
+ world (nth (iterate add-lichen world) 30)]
+ world))
+
+ (defn reset-game [game]
+ (let [fresh-world (random-world)]
+ (-> game
+ (assoc :world fresh-world)
+ (update-in [:world] populate-world)
+ (assoc :uis [(->UI :play)]))))
+
+It should be pretty easy to read. `add-lichen` adds a new lichen to an empty
+tile. `populate-world` takes a world and adds a player, then 30 lichens.
+
+This is getting to be a bit much to keep in `input.clj`. I'll probably pull
+this out into a separate file soon.
+
+Drawing the Entities
+--------------------
+
+So now the lichens are part of the world, but I still need to draw them on the
+screen. I split the `draw-player` function in `drawing.clj` into two separate
+functions:
+
+ :::clojure
+ (defn draw-entity [screen start-x start-y {:keys [location glyph color]}]
+ (let [[entity-x entity-y] location
+ x (- entity-x start-x)
+ y (- entity-y start-y)]
+ (s/put-string screen x y glyph {:fg color})))
+
+ (defn highlight-player [screen start-x start-y player]
+ (let [[player-x player-y] (:location player)
+ x (- player-x start-x)
+ y (- player-y start-y)]
+ (s/move-cursor screen x y)))
+
+And then I use those in the main `draw-ui` function for the `:play` UI:
+
+ :::clojure
+ (defmethod draw-ui :play [ui game screen]
+ (let [world (:world game)
+ {:keys [tiles entities]} world
+ player (:player entities)
+ [cols rows] screen-size
+ vcols cols
+ vrows (dec rows)
+ [start-x start-y end-x end-y] (get-viewport-coords game (:location player) vcols vrows)]
+ (draw-world screen vrows vcols start-x start-y end-x end-y tiles)
+ (doseq [entity (vals entities)]
+ (draw-entity screen start-x start-y entity))
+ (draw-hud screen game start-x start-y)
+ (highlight-player screen start-x start-y player)))
+
+Long but straightforward. I'm going to be cleaning this part of the code up
+very soon, as I've just added a bunch of really useful stuff to the
+[clojure-lanterna][] library that will let me delete a bunch of fiddly code
+here.
+
+[clojure-lanterna]: http://sjl.bitbucket.org/clojure-lanterna/
+
+If you're particularly eagle-eyed you might have noticed this new `color`
+attribute that seems to be a part of entities. I didn't realize I had forgotten
+to specify colors until I actually wrote this bit of code. Once I did I went
+back and added the field to the `Player` and `Lichen` records, as well as
+`make-player` and `make-lichen`.
+
+Now the lichens appear on the screen!
+
+![Screenshot](/media/images{{ parent_url }}/caves-05-01.png)
+
+Movement
+--------
+
+At this point the lichens are on the screen but the player can walk straight
+through them. I took care of that by first creating a few helper functions in
+`world.clj`:
+
+ :::clojure
+ (defn get-entity-at [world coord]
+ (first (filter #(= coord (:location %))
+ (vals (:entities world)))))
+
+ (defn is-empty? [world coord]
+ (and (#{:floor} (get-tile-kind world coord))
+ (not (get-entity-at world coord))))
+
+They'll handle the grunt world of traversing the `world` data structure. Then
+I updated the player's `can-move?` function:
+
+ :::clojure
+ (extend-type Player Mobile
+ (move ...)
+ (can-move? [this world dest]
+ (is-empty? world dest)))
+
+Previously `can-move` checked the world's tile itself -- now it delegates to
+a basic helper function instead. I have a feeling a lot of things are going to
+need to use the idea of "empty tiles" so this function will probably get a lot
+of mileage.
+
+Now the player can't walk through fungus. Great.
+
+Killing
+-------
+
+It's time to give the player a way to cut the lichens into little licheny bits.
+I implemented this with another pair of aspects: `Attacker` and `Destructible`.
+
+`Attacker` should be implemented by anything that can attack other things:
+
+ :::clojure
+ (ns caves.entities.aspects.attacker)
+
+ (defprotocol Attacker
+ (attack [this world target]
+ "Attack the target."))
+
+`Destructible` should be implemented by anything that can "take damage and go
+away once it takes enough":
+
+ :::clojure
+ (ns caves.entities.aspects.destructible)
+
+ (defprotocol Destructible
+ (take-damage [this world damage]
+ "Take the given amount of damage and update the world appropriately."))
+
+Lichens will be `Destructible` (for now the player will remain invincible):
+
+ :::clojure
+ (extend-type Lichen Destructible
+ (take-damage [{:keys [id] :as this} world damage]
+ (let [damaged-this (update-in this [:hp] - damage)]
+ (if-not (pos? (:hp damaged-this))
+ (update-in world [:entities] dissoc id)
+ (update-in world [:entities id] assoc damaged-this)))))
+
+The logic here is pretty basic. When a `Destructible` entity takes some damage,
+first its hit points are updated. If they wind up to be zero or fewer, the
+entity gracefully removes itself from the world.
+
+I have a feeling there's a more elegant way to write the updatey bits of that
+function. If you've got suggestions please let me know.
+
+If I'm going to be basing damage on the entity's `:hp` attribute they'd better
+have one! I added a simple `:hp` of `1` to lichens:
+
+ :::clojure
+ (defrecord Lichen [id glyph color location hp])
+
+ (defn make-lichen [location]
+ (->Lichen (get-id) "F" :green location 1))
+
+Next I added the corresponding implementation of `Attacker` to the `Player` (for
+now lichens can't strike back):
+
+ :::clojure
+ (extend-type Player Attacker
+ (attack [this world target]
+ {:pre [(satisfies? Destructible target)]}
+ (let [damage 1]
+ (take-damage target world damage))))
+
+Again, a very basic system for the moment: all attacks do one damage. Lichens
+only have one hit point, so this will kill them instantly.
+
+Notice the precondition here: an attacker can attack something if and only if
+it's something that satisfies the `Destructible` protocol.
+
+Instead of doing something like checking if the target has `:hp` I simply check
+if it's `Destructible`. This opens the door for things that don't necessarily
+use hit points, like a monster whose mana and hit points are a single number.
+
+Finally, I need to hook up the attacking functionality in the `move-player`
+helper function:
+
+ :::clojure
+ (defn move-player [world dir]
+ (let [player (get-in world [:entities :player])
+ target (destination-coords (:location player) dir)
+ entity-at-target (get-entity-at world target)]
+ (cond
+ entity-at-target (attack player world entity-at-target)
+ (can-move? player world target) (move player world target)
+ (can-dig? player world target) (dig player world target)
+ :else world)))
+
+This once again overloads the `hjkl` keys, so now the player will attack
+a monster when they try to move into it. Otherwise the player will move or dig
+as before.
+
+Growing Lichens
+---------------
+
+Now for the last part of Trystan's post. Lichens should have a chance of
+spreading slowly every turn. Unlike Trystan, I'm not going to limit the number
+of times the lichen can spread, so the player will need to use their newfound
+attacking ability if they want to stem the tide of invading fungus!
+
+This turned out to be surprisingly painless:
+
+ :::clojure
+ (defn should-grow []
+ (< (rand) 0.01))
+
+ (defn grow [lichen world]
+ (if-let [target (find-empty-neighbor world (:location lichen))]
+ (let [new-lichen (make-lichen target)]
+ (assoc-in world [:entities (:id new-lichen)] new-lichen))
+ world))
+
+ (extend-type Lichen Entity
+ (tick [this world]
+ (if (should-grow)
+ (grow this world)
+ world)))
+
+Every tick, the lichen has a one percent chance to spread to an empty
+neighboring tile. If there are no empty neighboring tiles, it can't spread.
+
+The `find-empty-neighbor` function is new, and located in `world.clj`:
+
+ :::clojure
+ (defn find-empty-neighbor [world coord]
+ (let [candidates (filter #(is-empty? world %) (neighbors coord))]
+ (when (seq candidates)
+ (rand-nth candidates))))
+
+It uses `neighbors`, which is another function I created after a quick refactor
+of `coords.clj`:
+
+ :::clojure
+ (ns caves.coords)
+
+ (def directions
+ {:w [-1 0]
+ :e [1 0]
+ :n [0 -1]
+ :s [0 1]
+ :nw [-1 -1]
+ :ne [1 -1]
+ :sw [-1 1]
+ :se [1 1]})
+
+ (defn offset-coords
+ "Offset the starting coordinate by the given amount, returning the result coordinate."
+ [[x y] [dx dy]]
+ [(+ x dx) (+ y dy)])
+
+ (defn dir-to-offset
+ "Convert a direction to the offset for moving 1 in that direction."
+ [dir]
+ (directions dir))
+
+ (defn destination-coords
+ "Take an origin's coords and a direction and return the destination's coords."
+ [origin dir]
+ (offset-coords origin (dir-to-offset dir)))
+
+ (defn neighbors
+ "Return the coordinates of all neighboring squares of the given coord."
+ [origin]
+ (map offset-coords (vals directions) (repeat origin)))
+
+Nothing too crazy here. The small, composable functions build on top of each
+other to create more interesting ones.
+
+But there's one thing left to do, which is actually `tick` entities in the main
+game loop in `core.clj`:
+
+ :::clojure
+ (defn tick-entity [world entity]
+ (tick entity world))
+
+ (defn tick-all [world]
+ (reduce tick-entity world (vals (:entities world))))
+
+ (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 (update-in game [:world] tick-all) screen))
+ (recur (process-input (dissoc game :input) input))))))
+
+Notice how the `tick-all` function reduces over the values in the entities map.
+Maps aren't deterministically ordered (or at least they're not *guaranteed* to
+be), so this means that our entities may process their ticks in a different
+order each turn.
+
+I think I'm okay with that. Yes, it means that ticking the world isn't going to
+be a pure function, but it won't be pure no matter what since we're going to
+have random numbers involved in attacking and damage soon enough.
+
+Results
+-------
+
+All-in-all it took roughly an hour and a half to code the stuff in this entry.
+This might sound like a lot, but remember what was added:
+
+* The entire concept of "multiple entities in a map".
+* Support for drawing arbitrary entities on the map.
+* A new creature.
+* Entities blocking movement of others.
+* A rudimentary attacking gameplay mechanic.
+* A rudimentary killing mechanic.
+* Ticking entities.
+* Growing/spreading of creatures.
+* Lots of refactoring and helper functions.
+
+Not too bad!
+
+You can view the code [on GitHub][result-code] if you want to see the end
+result.
+
+[result-code]: https://github.com/sjl/caves/tree/entry-05/src/caves
+
+And now some screenshots of our hero cutting a swath through some fungus!
+
+![Screenshot](/media/images{{ parent_url }}/caves-05-02.png)
+
+![Screenshot](/media/images{{ parent_url }}/caves-05-03.png)
+
+I'll be moving on to Trystan's sixth post soon, but before that I'm going to
+have another interlude where I explain some quick refactoring and then work
+a bit of the blackest magic in Clojure: a non-trivial macro.
+
+{% endblock article %}