The Caves of Clojure: Part 6

Posted on July 30th, 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 six 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-06 tag to see the code as it stands after this post.

Sorry for the long wait for this entry. I've been working on lots of other stuff and haven't had a lot of time to write. Hopefully I can get back to it more often!

  1. Summary
  2. Refactoring
  3. Attacking and Defending
  4. Messaging
  5. Results

Summary

In Trystan's sixth post he adds a combat system and messaging infrastructure. Once again I'm following his lead and implementing the same things, but in the entity/aspect way of doing things.

As usual I ended up refactoring a few things, which I'll briefly cover first.

Refactoring

So far, the functions entities implement to fulfill aspects have looked like this:

(defaspect Digger
  (dig [this world dest]
    ...)
  (can-dig? [this world dest]
    ...))

The entity has to be the first argument, because that's how protocols work. I don't have any flexibility there. I originally made the world always be the second argument, but it turns out that it's more convenient to make the world the last argument.

To see why, imagine we want to allow players to dig and move at the same time, instead of forcing them to be separate actions. Updating the world might look like this:

(let [new-world (dig player world dest)
      new-world (move player world dest)]
  new-world)

You could make this one specific case a bit prettier, but in general chaining together world-modifying actions is going to be a pain. If I change the aspect functions to take the player, then other args, and then the world, I can use ->> to chain actions:

(->> world
  (dig player dest)
  (move player dest))

Much cleaner! I went ahead and switched all the aspect functions to use this new scheme.

I also did some other minor refactoring. You can look through the changesets if you're really curious.

Attacking and Defending

Instead of simply killing everything in one hit, I'm now going to give some creatures a bit of hp. I also added a :max-hp attribute, since I'll likely need that in the future. Here's a sample of what the Bunny creation function looks like:

(defn make-bunny [location]
  (map->Bunny {:id (get-id)
               :name "bunny"
               :glyph "v"
               :color :yellow
               :location location
               :hp 4
               :max-hp 4}))

I started using the map->Foo record constructors because the ->Foo versions that relied on positional arguments started getting hard to read.

Bunnies have 4 hp. It'd be trivial to randomize this in the future, but for now I'll stick with a simple number.

Trystan uses a simple attack and defense system. I toyed with the idea of using a different one (like Brogue's) but figured I should stick to his tutorial when there's no clear reason not to.

Trystan's system needs attack and defense values, so I added functions to the Attacker and Destructible aspects to retrieve these:

(defaspect Attacker
  (attack [this target world]
    ...)
  (attack-value [this world]
    (get this :attack 1)))

(defaspect Destructible
  (take-damage [this damage world]
    ...)
  (defense-value [this world]
    (get this :defense 0)))

The default attack value is 1. If an entity has an :attack attribute that will be used instead. Or the entity could provide a completely custom version of attack-value (e.g.: werewolves could have a larger attack if there's a full moon in the game or something). Defense values work the same way.

Now that I'm starting to actually use HP I'll display it in the bottom row of info on the screen:

(defn draw-hud [screen game]
  (let [hud-row (dec (second (s/get-size screen)))
        player (get-in game [:world :entities :player])
        {:keys [location hp max-hp]} player
        [x y] location
        info (str "hp [" hp "/" max-hp "]")
        info (str info " loc: [" x "-" y "]")]
    (s/put-string screen 0 hud-row info)))

The bottom row of the screen now looks like: "hp [20/20] loc: [82-103]". I'll probably get rid of the loc soon, but for now it won't hurt to keep it onscreen.

Now for the damage calculation. I added a little helper function to take care of this in attacker.clj:

(defn get-damage [attacker target world]
  (let [attack (attack-value attacker world)
        defense (defense-value target world)
        max-damage (max 0 (- attack defense))
        damage (inc (rand-int max-damage))]
    damage))

This matches what Trystan does. In a nutshell, the damage done is: "If defense is higher than attack, then 1. Otherwise, a random number between 1 and (attack - defense)."

I kept it separate from the attack function so that an entity can override attack without having to reimplement this logic.

The attack default implementation needs to use this new damage calculator:

(defaspect Attacker
  (attack [this target world]
    {:pre [(satisfies? Destructible target)]}
    (let [damage (get-damage this target world)]
      (take-damage target damage)))
  (attack-value [this world]
    (get this :attack 1)))

Destructible already handles reducing HP appropriately. The only thing left is to give my entities some non-1 attack, defense, and/or hp values. For now I used the following values:

These are really just placeholder numbers until I add the ability for monsters to attack back. Once I do that I'll be able to play the game a bit and determine if it's too easy or hard.

Messaging

Now the player can attack things and it may take a few swings to kill them. The problem is that there's no feedback while this is going on, so it's hard to tell that you're actually doing damage until the monster dies.

A messaging system will let me display informational messages to give the player some feedback. I decided to implement this like everything else: as an aspect.

An entity that implements the Receiver protocol will be able to receive messages. Here's entities/aspects/receiver.clj:

(ns caves.entities.aspects.receiver
  (:use [caves.entities.core :only [defaspect]]
        [caves.world :only [get-entities-around]]))

(defaspect Receiver
  (receive-message [this message world]
    (update-in world [:entities (:id this) :messages] conj message)))

(defn send-message [entity message args world]
  (if (satisfies? Receiver entity)
    (receive-message entity (apply format message args) world)
    world))

I've got a helper function send-message which is what entities will use to send messages, instead of performing the (satisfies? Receiver entity) check themselves every time. If the entity they're sending the message to isn't a Receiver it will simply drop the message on the floor by returning the world unchanged. It also handles formatting the message string for them.

The default receive-message simply appends the message to a :messages attribute in the entity.

I have a lot of ideas about extending this system in the future, but for now I'll just keep it simple to match Trystan's.

Now I need to send some messages. The most obvious place to do this is when something attacks something else, so I updated the Attacker aspect once more:

(defaspect Attacker
  (attack [this target world]
    {:pre [(satisfies? Destructible target)]}
    (let [damage (get-damage this target world)]
      (->> world
        (take-damage target damage)
        (send-message this "You strike the %s for %d damage!"
                      [(:name target) damage])
        (send-message target "The %s strikes you for %d damage!"
                      [(:name this) damage]))))
  (attack-value [this world]
    (get this :attack 1)))

This is starting to get a little crowded. If I need to do much more in here I'll refactor some stuff out into helper functions. But for now it's still readable.

Here you can see how making world-altering functions take the world as the last argument pays off by letting me use ->> to chain together actions.

I also added a :name attribute to entities so I can say "You strike the bunny" instead of "You strike the v". Nothing too special there.

Finally, I need a way to notify nearby entities when something happens. Here's what I came up with:

(defn send-message-nearby [coord message world]
  (let [entities (get-entities-around world coord 7)
        sm (fn [world entity]
             (send-message entity message [] world))]
    (reduce sm world entities)))

First I grab all the entities within 7 squares of the message coordinate. Then I create a little helper function called sm that wraps send-message. It will take a world and an entity and return the modified world. I use reduce here to iterate over the entities and send the message to each one. It's a pretty way of handling that looping.

The get-entities-around function is new:

(defn get-entities-around
  ([world coord] (get-entities-around world coord 1))
  ([world coord radius]
     (filter #(<= (radial-distance coord (:location %))
                  radius)
             (vals (:entities world)))))

It looks through all the entities in the world and returns a sequence of those whose "radial" distance is less than or equal to the given radius. The "radial distance" (also called the "king's move" distance by some) looks like this:

3333333
3222223
3211123
3210123
3211123
3222223
3333333

And the function:

(defn radial-distance
  "Return the radial distance between two points."
  [[x1 y1] [x2 y2]]
  (max (abs (- x1 x2))
       (abs (- y1 y2))))

I may end up needing to modify this send-message-nearby function to be a bit more powerful in the future. Trystan's version modifies the verbs and such. For now this is good enough for me.

Now I can make lichens notify nearby creatures when they grow:

(defn grow [{:keys [location]} world]
  (if-let [target (find-empty-neighbor world location)]
    (let [new-lichen (make-lichen target)
          world (assoc-in world [:entities (:id new-lichen)] new-lichen)
          world (send-message-nearby location "The lichen grows." world)]
      world)
    world))

The last step is to actually display these messages to the player. I added a draw-messages function to the ui/drawing.clj file:

(defn draw-messages [screen messages]
  (doseq [[i msg] (enumerate messages)]
    (s/put-string screen 0 i msg {:fg :black :bg :white})))

And then I modified the main draw-ui function for :play UIs to draw the messages on top of the map:

(defmethod draw-ui :play [ui game screen]
  (let [world (:world game)
        {:keys [tiles entities]} world
        player (:player entities)
        [cols rows] (s/get-size screen)
        vcols cols
        vrows (dec rows)
        origin (get-viewport-coords game (:location player) vcols vrows)]
    (draw-world screen vrows vcols origin tiles)
    (doseq [entity (vals entities)]
      (draw-entity screen origin vrows vcols entity))
    (draw-hud screen game)
    (draw-messages screen (:messages player))
    (highlight-player screen origin player)))

This does mean that messages will cover a bit of the screen. For now I'll live with that, but in the future it's something to fix.

Finally we need to clear the message queue out periodically, otherwise it'll grow until it covers the entire screen! I modified the main game loop in core.clj:

(defn clear-messages [game]
  (assoc-in game [:world :entities :player :messages] nil))

(defn run-game [game screen]
  (loop [{:keys [input uis] :as game} game]
    (when (seq uis)
      (if (nil? input)
        (let [game (update-in game [:world] tick-all)
              _ (draw-game game screen)
              game (clear-messages game)]
          (recur (get-input game screen)))
        (recur (process-input (dissoc game :input) input))))))

Results

After fixing a few other bugs (you can read the changelog if you're interested) I've now got a working combat system, and a messaging system so I can tell what's going on:

Screenshot

It's actually starting to feel like a real game now, instead of just a sandbox where you can break things.

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

The next article will move on to Trystan's seventh post, which adds multiple z-levels to the caves.