Sunday, April 22, 2012

Orc Battle In Clojure

The goal of this is to explain how I implemented Orc Battle in Clojure and the changes I made. Part of my intent with implementing the program in Clojure was to rewrite it in a functional style. If you are interested in a straight port of the code, that can be found here: Orc Battle Online. As a bonus, this also includes a web interface using Ring and Hiccup.

The first big change is that this version does not make use of any global variables. A key concept of functional programming is that calling a function does not have any side effects. Any time we modify a value, such as attacking the player and decreasing his health, we cannot modify any external state. Rather we pass in the player as a parameter to the function, and generate a new object with this modified state and use the returned value later on in the program. Since we no longer have the global variables keeping track of the players stats, we need to create a structure to keep track of this:

(defn create-player [max-health max-agility max-strength]
{:health max-health :max-health max-health
:agility max-agility :max-agility max-agility
:strength max-strength :max-strength max-strength})
Not only are we keeping track of the current stats, we have the original values. I plan on adding increasing rounds of difficulty and the ability of the player to level up.

For our first function, lets create one that damages a player's attribute by some amount. So given the player, attribute and damage we want to return a new player structure with the given attribute altered. We can use the assoc function, which takes a hash-map, the key and the new value. This function will return a new hash-map where everything is the same except for the specified key, which now has the new value.

(defn hit-player [player attr dmg]
(assoc player attr (max 0 (- (attr player) dmg))))
orc.core> (def player (create-player 20 20 20))
#'orc.core/player
orc.core> (hit-player player :health 5)
{:health 15, :max-health 20, :agility 20, :max-agility 20, :strength 20, :max-strength 20}
orc.core> player
{:health 20, :max-health 20, :agility 20, :max-agility 20, :strength 20, :max-strength 20}
view raw hit_player.clj hosted with ❤ by GitHub
As you can see above, after calling hit player we have a new object returned with the updated values, but we do not have to worry about our original player being changed. As a result testing become easier. You can reuse the same object over and over while testing a function, since most functions do not have side effects, so only the returned results matter.

The next piece is dealing with the monster methods and dispatching on type. In clojure there are several approaches to dealing with inheritance. One approach uses defrecord and protocols. I tried that way first, but with little experience using it, I found the resulting code less clear. An alternate approach that I used was multi methods. The main compents of a multimethod are the method name and the dispatch function. Since a key (e.g. :some-key) can act as a function, that means you can dispatch the method based on the value of a given key or set of keys of a map object. So in this case we have the following method signatures and implementations:

(defmulti monster-show :type)
(defmulti monster-attack :type)
(defmulti monster-hit :type)
::monster
(derive ::orc ::monster)
(derive ::hydra ::monster)
(defn orc [health club-level]
{:type ::orc :health health :club-level club-level})
(defn hydra [health]
{:type ::hydra :health health})
(defmethod monster-show ::monster [m]
(str "A " (monster-name (:type m)) " with " (:health m) " health."))
(defmethod monster-show ::hydra [m]
(str "A hydra with " (:health m) " heads!"))
view raw monster.clj hosted with ❤ by GitHub
All of the multi-methods are being dispatched on the type property of the maps. We then define the type hierarchy, with monster being the base type and both orc and hydra deriving from that. So now we have the polymorphism we'd expect. In each of the orc and hydra objects the type property is defined. This value will be used for dispatching. We have two implementations of the monster-show method. One defined for a generic monster and one specialized to the hydra. Since no specific orc defined method exists and orc is derived from monster, the generic version will be used when called with an orc. When called with a hydra we get the specialized hydra version.

We also need some addition methods to help with I/O. When using swank in emacs, the regular read-line method does not work. Fortunately swank-clojure provides a method to work around this problem. Also, read-line returns a string with a newline character so we need a method to parse the integer.

(defn read-from-in []
(swank.core/with-read-line-support (read-line)))
(defn read-int []
(let [x (clojure.string/replace (read-from-in) "\n" "")]
(if (every? #(Character/isDigit %) x)
(Integer/parseInt x)
-1)))
view raw read_in.clj hosted with ❤ by GitHub
The last major change was dealing with the monster attacks and player attacks. I struggled with this a little, since this step is really where most of the regular state change was occuring. My first attempt used loop/recur. While it worked, the resulting code was not idiomatic and was not using the sequence library to process the code. With some help (Stackoverflow question) I was able to use a solution which made better use of the sequence library using reduce. Similarly I was able to use reduce for the player attacks, with the list being n copies of the player where n is the number of attacks the player has.

(defn monster-round-original [player monsters]
(loop [n 0 p player]
(if (>= n (count monsters))
p
(recur (inc n)
(if (monster-dead? (nth monsters n))
p
(let [r (monster-attack (nth monsters n) p)]
(print (:attack r))
(:player r)))))))
(defn- attack-player [player monster]
(if (monster-dead? monster)
player
(let [r (monster-attack monster player)]
(print (:attack r))
(:player r))))
(defn monster-round-improved [player monsters]
(reduce attack-player player monsters))
(defn- attack-monsters [monsters player]
(if (monsters-dead? monsters)
monsters
(do
(show-monsters monsters)
(player-attack player monsters))))
(defn num-attacks [player]
(inc (Math/floor (/ (max 0 (:agility player)) 15))))
(defn player-round [player monsters]
(reduce attack-monsters monsters
(repeat (num-attacks player) player)))

The one place where I did stick with a loop/recur was with the main game loop. Since I am not specifically using a data structure to loop on, I am uncertain if there is a better way to implement this.

(defn level-loop [player monsters]
(loop [m monsters p player]
(if (or (monsters-dead? m) (player-dead? p))
p
(do
(print (str "\n" (show-player p)))
(let [m2 (player-round p m)]
(recur m2 (monster-round p m2)))))))
view raw level.clj hosted with ❤ by GitHub

The full code can be found here along with the original common lisp code.

No comments: