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:
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.
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:
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.
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.
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.
The full code can be found here along with
the original common lisp code.
Sunday, April 22, 2012
Subscribe to:
Posts (Atom)