The Caves of Clojure: Part 5

Posted on July 13th, 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 five 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-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, 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.

  1. Summary
  2. Multiple Entities
  3. Lichens
  4. Populating the World
  5. Drawing the Entities
  6. Movement
  7. Killing
  8. Growing Lichens
  9. Results

Summary

In Trystan's fifth post he adds three things:

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:

{: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:

{: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:

(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, Lichens 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:

(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:

(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:

(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:

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

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

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:

(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:

(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:

(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":

(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):

(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:

(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):

(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:

(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:

(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:

(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:

(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:

(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:

Not too bad!

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

And now some screenshots of our hero cutting a swath through some fungus!

Screenshot

Screenshot

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.