In the Orc Battle game, you’re a knight surrounded by 12 monsters, engaged in a fight to the death. With your superior wits and your repertoire of sword-fighting maneuvers, you must carefully strategize in your battle with orcs, hydras, and other nasty enemies. One wrong move and you may be unable to kill them all before being worn down by their superior numbers. Using defmethod
and defstruct
, let’s dispatch some whoop ass on these vermin!
We’ll want to track three player stats: health, agility, and strength. When a player’s health reaches zero, that player will die. Agility will control how many attacks a player can perform in a single round of battle, and strength will control the ferocity of the attacks. As the game progresses, each of these will change and affect gameplay and strategy in subtle ways.
(defparameter *player-health* nil) (defparameter *player-agility* nil) (defparameter *player-strength* nil)
We’ll store our monsters in an array called *monsters*
. This array will be heterogeneous, meaning it can contain different types of monsters, be they orcs, hydras, or anything else. We’ll create our monster types with defstruct
. Of course, we still need to figure out how to handle each type in the list in a meaningful way—that’s where we’ll use Lisp’s generic features.
We’ll also define a list of functions for building monsters that we’ll store in the variable *monster-builders*
. As we write the code for each type of monster, we’ll create a function that builds a monster of each type. We’ll then push each of these monster builders onto this list. Having all the builder functions in this list will make it easy for us to create random monsters at will for our game.
Finally, we’ll create the variable *monster-num*
to control how many opponents our knight must fight. Change this variable to increase (or decrease) the difficulty level of Orc Battle.
(defparameter *monsters* nil) (defparameter *monster-builders* nil) (defparameter *monster-num* 12)
Now we’re ready to write our first real code for the game, starting with the big picture functions that drive the rest of the system.
First, we’ll define a function called orc-battle
. This function will initialize the monsters and start the game loop and, once the battle ends, it will determine the victor and print the appropriate ending message for the game. As you can see, orc-battle
calls plenty of helper functions to do the actual work:
(defun orc-battle () (init-monsters) (init-player) (game-loop) (when (player-dead) (princ "You have been killed. Game Over.")) (when (monsters-dead) (princ "Congratulations! You have vanquished all of your foes.")))
At the top, we call the initialization functions for the monsters and the player . Then we start the main game loop . The game loop will keep running until either the player or the monsters are dead. We’ll print a game-ending message depending on whether the player or monsters died.
Next, we’ll create the function game-loop
to handle the game loop. This function handles a round of the battle, and then calls itself recursively for the following round:
(defun game-loop () (unless (or (player-dead) (monsters-dead)) (show-player) (dotimes (k (1+ (truncate (/ (max 0 *player-agility*) 15)))) (unless (monsters-dead) (show-monsters) (player-attack))) (fresh-line) (map 'list (lambda(m) (or (monster-dead m) (monster-attack m))) *monsters*) (game-loop)))
The game-loop
function handles the repeated cycles of monster and player attacks. As long as both parties in the fight are still alive, the function will first show some information about the player in the REPL .
Next, we allow the player to attack the monsters. The game-loop
function uses the player’s agility to modulate how many attacks can be launched in a single round of battle, using some fudge factors to transform the agility to a small, appropriate number . When the game begins, the player will have three attacks per round. Later stages of battle could cause this number to drop to a single attack per round.
The calculated agility factor for our player attack loop is passed into the dotimes
command, which takes a variable name and a number n, and runs a chunk of code n times:
>(dotimes (i 3)
(fresh-line)
(princ i)
(princ ". Hatchoo!"))
0. Hatchoo! 1. Hatchoo! 2. Hatchoo!
The dotimes
function is one of Common Lisp’s looping commands (looping is covered in more detail in Chapter 10).
After the player has attacked, we allow the monsters to attack. We do this by iterating through our list of monsters with the map
function . Every type of monster has a special monster-attack
command, which we’ll call as long as the monster is still alive .
Finally, the game-loop
function calls itself recursively, so that the battle can continue until one side or the other has been vanquished .
The functions we need for managing the player’s attributes (health, agility, and strength) are very simple. Following are the functions we need to initialize players, to see if they’ve died, and to output their attributes:
(defun init-player () (setf *player-health* 30) (setf *player-agility* 30) (setf *player-strength* 30)) (defun player-dead () (<= *player-health* 0)) (defun show-player () (fresh-line) (princ "You are a valiant knight with a health of ") (princ *player-health*) (princ ", an agility of ") (princ *player-agility*) (princ ", and a strength of ") (princ *player-strength*))
The player-attack
function lets us manage a player’s attack:
(defun player-attack () (fresh-line) (princ "Attack style: [s]tab [d]ouble swing [r]oundhouse:") (case (read) (s (monster-hit (pick-monster) (+ 2 (randval (ash *player-strength* −1))))) (d (let ((x (randval (truncate (/ *player-strength* 6))))) (princ "Your double swing has a strength of ") (princ x) (fresh-line) (monster-hit (pick-monster) x) (unless (monsters-dead) (monster-hit (pick-monster) x)))) (otherwise (dotimes (x (1+ (randval (truncate (/ *player-strength* 3))))) (unless (monsters-dead) (monster-hit (random-monster) 1))))))
First, this function prints out some different types of attacks from which the player can choose . As you can see, the player is offered three possible attacks: a stab, a double swing, and a roundhouse swing. We read in the player’s selection, and then handle each type of attack in a case
statement .
The stab attack is the most ferocious attack and can be delivered against a single foe. Since a stab is performed against a single enemy, we will first call the pick-monster
function to let the player choose whom to attack . The attack strength is calculated from the *player-strength*
, using a random factor and some other little tweaks to generate a nice, but never too powerful, attack strength . Once the player has chosen a monster to attack and the attack strength has been calculated, we call the monster-hit
function to apply the attack .
Unlike the stab attack, the double swing is weaker, but allows two enemies to be attacked at once. An additional benefit of the attack is that the knight can tell, as the swing begins, how strong it will be—information that can then be used to choose the best enemies to attack midswing. This extra feature of the double swing adds strategic depth to the game. Otherwise, the double-swing code is similar to the stab code, printing a message and allowing the player to choose whom to attack. In this case, however, two monsters can be chosen .
The final attack, the roundhouse swing, is a wild, chaotic attack that does not discriminate among the enemies. We run through a dotimes
loop based on the player’s strength and then attack random foes multiple times. However, each attack is very weak, with a strength of only 1 .
These attacks must be used correctly, at the right stages of a battle, in order to achieve victory. To add some randomness to the attacks in the player-attack
function, we used the randval
helper function to generate random numbers. It is defined as follows:
(defun randval (n) (1+ (random (max 1 n))))
The randval
function returns a random number from one to n, while making sure that no matter how small n is, at least the number 1 will be returned. Using randval
instead of just the random
function for generating random numbers gives a reality check to the randomness of the game, since 0 doesn’t make sense for some of the values we use in our calculations. For instance, even the weakest player or monster should always have an attack strength of at least 1.
The random
function used by randval
is the canonical random value function in Lisp. It can be used in several different ways, though most frequently it is used by passing in an integer n and receiving a random integer from 0 to n−1:
>(dotimes (i 10)
(princ (random 5))
(princ " "))
1 2 2 4 0 4 2 4 2 3
Our player-attack
function needs two helper functions to do its job. First, it needs a random-monster
function that picks a monster to target for the chaotic roundhouse attack, while ensuring that the chosen monster isn’t already dead:
(defun random-monster () (let ((m (aref *monsters* (random (length *monsters*))))) (if (monster-dead m) (random-monster) m)))
The random-monster
function first picks a random monster out of the array of monsters and stores it in the variable m
. Since we want to pick a living monster to attack, we recursively try the function again if we inadvertently picked a dead monster . Otherwise, we return the chosen monster .
The player-attack
function also needs a function that allows the player to pick a monster to target for the nonrandom attacks. This is the job of the pick-monster
function:
(defun pick-monster () (fresh-line) (princ "Monster #:") (let ((x (read))) (if (not (and (integerp x) (>= x 1) (<= x *monster-num*))) (progn (princ "That is not a valid monster number.") (pick-monster)) (let ((m (aref *monsters* (1- x)))) (if (monster-dead m) (progn (princ "That monster is alread dead.") (pick-monster)) m)))))
In order to let the player pick a monster to attack, we first need to display a prompt and read in the player’s choice . Then we need to make sure the player chose an integer that isn’t too big or too small . If this has happened, we print a message and call pick-monster
again to let the player choose again. Otherwise, we can safely place the chosen monster in the variable m
.
Another error the player could make is to attack a monster that is already dead. We check for this possibility next and, once again, allow the player to make another selection . Otherwise, the player has successfully made a choice, and we return the selected monster as a result .
Now let’s work on our monsters.
We’ll use the init-monsters
function to initialize all the bad guys stored in the *monsters*
array. This function will randomly pick functions out of the *monster-builders*
list and call them with funcall
to build the monsters:
(defun init-monsters () (setf *monsters* (map 'vector (lambda (x) (funcall (nth (random (length *monster-builders*)) *monster-builders*))) (make-array *monster-num*))))
First, the init-monsters
function builds an empty array to hold the monsters . Then it maps
across this array to fill it up . In the lambda
function, you can see how random monsters are created by funcall
ing random functions in our list of monster builders .
Next, we need some simple functions for checking if the monsters are dead. Notice how we use the every command on the *monsters*
array to see if the function monster-dead
is true for every monster. This will tell us whether the entire monster population is dead.
(defun monster-dead (m) (<= (monster-health m) 0)) (defun monsters-dead () (every #'monster-dead *monsters*))
We’ll use the show-monsters
function to display a listing of all the monsters. This function will, in turn, defer part of the work to another function, so it doesn’t actually need to know a lot about the different monster types:
(defun show-monsters () (fresh-line) (princ "Your foes:") (let ((x 0)) (map 'list (lambda (m) (fresh-line) (princ " ") (princ (incf x)) (princ ". ") (if (monster-dead m) (princ "**dead**") (progn (princ "(Health=") (princ (monster-health m)) (princ ") ") (monster-show m)))) *monsters*)))
Since our player will need to choose monsters with a number, we will maintain a count as we loop through monsters in our list, in the variable x
. Then we map
through our monster list, calling a lambda
function on each monster, which will print out some pretty text for each monster . We use our x
variable to print out the number for each monster in our numbered list . As we do this, we use the incf
function, which will increment x
as we work through the list.
For dead monsters, we won’t print much about them, just a message showing that they are dead . For living monsters, we call generic monster functions, calculating the health and generating the monster description in a specialized way for each different type of foe.
So far, we haven’t seen any functions that really give life to the monsters. Let’s fix that.
First, we’ll describe a generic monster.
As you would expect, orcs, hydras, and other bad guys all have one thing in common: a health meter that determines how many hits they can take before they die. We can capture this behavior in a monster
structure:
(defstruct monster (health (randval 10)))
This use of the defstruct
function takes advantage of a special feature: When we declare each slot in the structure (in this case, health
) we can put parentheses around the name and add a default value for that slot. But more important, we can declare a form that will be evaluated when a new monster
is created. Since this form calls randval
, every monster will start the battle with a different, random, health.
Let’s try creating some monsters:
>(make-monster)
#S(MONSTER :HEALTH 7) >(make-monster)
#S(MONSTER :HEALTH 2) >(make-monster)
#S(MONSTER :HEALTH 5)
We also need a function that takes away a monster’s health when it’s attacked. We’ll have this function output a message explaining what happened, including a message to be displayed when the monster dies. However, instead of creating this function with defun
, we’ll use the generic defmethod
, which will let us display special messages when the knight beats on particular monsters:
(defmethod monster-hit (m x) (decf (monster-health m) x) (if (monster-dead m) (progn (princ "You killed the ") (princ (type-of m)) (princ "! ")) (progn (princ "You hit the ") (princ (type-of m)) (princ ", knocking off ") (princ x) (princ " health points! "))))
The decf
function is a variant of setf
that lets us subtract an amount from a variable. The type-of
function lets monster-hit
pretend it knows the type of the monster that was hit . This function can be used to find the type of any Lisp value:
>(type-of 'foo)
SYMBOL >(type-of 5)
INTEGER >(type-of "foo")
ARRAY >(type-of (make-monster))
MONSTER
Currently, the type of a monster will always be monster
, but soon we’ll have this value change for each monster type.
We can also use two more generic methods to create monsters: monster-show
and monster-attack
.
The monster-attack
function doesn’t actually do anything. This is because all our monster attacks will be so unique that there’s no point in defining a generic attack. This function is simply a placeholder.
(defmethod monster-show (m) (princ "A fierce ") (princ (type-of m))) (defmethod monster-attack (m))
Now that we have some generic monster code, we can finally create some actual bad guys!
The orc is a simple foe. He can deliver a strong attack with his club, but otherwise he is pretty harmless. Every orc has a club with a unique attack level. Orcs are best ignored, unless there are orcs with an unusually powerful club attack that you want to cull from the herd at the beginning of a battle.
To create the orc, we define an orc
datatype with defstruct
. Here, we will use another advanced feature of defstruct
to declare that the orc
includes all the fields of monster
.
By including the fields from our monster
type in our orc
type, the orc
will be able to inherit the fields that apply to all monsters, such as the health
field. This is similar to what you can accomplish in popular languages such as C++ or Java by defining a generic class and then creating other, more specialized, classes that inherit from this generic class.
Once the structure is declared, we push the make-orc
function (automatically generated by the defstruct
) onto our list of *monster-builders*
:
(defstruct (orc (:include monster)) (club-level (randval 8))) (push #'make-orc *monster-builders*)
Notice how powerful this approach is. We can create as many new monster types as we want, yet we’ll never need to change our basic Orc Battle code. This is possible only in languages like Lisp, which are dynamically typed and support functions as first-class values. In statically typed programming languages, the main Orc Battle code would need some hardwired way of calling the constructor for each new type of monster. With first-class functions, we don’t need to worry about this.
Now let’s specialize our monster-show
and monster-attack
functions for orcs. Notice these are defined in the same way as the earlier versions of these functions, except that we explicitly declare that these functions are orc-specific in the argument lists:
(defmethod monster-show ((m orc)) (princ "A wicked orc with a level ") (princ (orc-club-level m)) (princ " club")) (defmethod monster-attack ((m orc)) (let ((x (randval (orc-club-level m)))) (princ "An orc swings his club at you and knocks off ") (princ x) (princ " of your health points. ") (decf *player-health* x)))
The one unique thing about our orc
type is that each orc has an orc-club-level
field. These orc-specific versions of monster-show
and monster-attack
take this field into account. In the monster-show
function, we display this club level , so that the player can gauge the danger posed by each orc.
In the monster-attack
function, we use the level of the club to decide how badly the player is hit by the club .
The hydra is a very nasty enemy. It will attack you with its many heads, which you’ll need to chop off to defeat it. The hydra’s special power is that it can grow a new head during each round of battle, which means you want to defeat it as early as possible.
(defstruct (hydra (:include monster))) (push #'make-hydra *monster-builders*) (defmethod monster-show ((m hydra)) (princ "A malicious hydra with ") (princ (monster-health m)) (princ " heads.")) (defmethod monster-hit ((m hydra) x) (decf (monster-health m) x) (if (monster-dead m) (princ "The corpse of the fully decapitated and decapacitated hydra falls to the floor!") (progn (princ "You lop off ") (princ x) (princ " of the hydra's heads! ")))) (defmethod monster-attack ((m hydra)) (let ((x (randval (ash (monster-health m) −1)))) (princ "A hydra attacks you with ") (princ x) (princ " of its heads! It also grows back one more head! ") (incf (monster-health m)) (decf *player-health* x)))
The code for handling the hydra is similar to the code for handling the orc. The main difference is that a hydra’s health also acts as a stand-in for the number of hydra heads. In other words, a hydra with three health points will have three heads, as well. Therefore, when we write our hydra-specific monster-show
function, we use the monster’s health to print a pretty message about the number of heads on the hydra .
Another difference between the orc and the hydra is that an orc doesn’t do anything particularly interesting when it is hit by the player. Because of this, we didn’t need to write a custom monster-hit
function for the orc; the orc simply used the generic monster-hit
function we created for a generic monster
.
A hydra, on the other hand, does something interesting when it is hit: It loses heads! We therefore create a hydra-specific monster-hit
function, where heads are removed with every blow, which amounts to lowering the hydra’s health . Also, we can now print a dramatic message about how the knight lopped off said heads .
The hydra’s monster-attack
function is again similar to that for the orc. The one interesting difference is that we increment the health with every attack, so that the hydra grows a new head every turn .
The slime mold is a unique monster. When it attacks you, it will wrap itself around your legs and immobilize you, letting the other bad guys finish you off. It can also squirt goo in your face. You must think quickly in battle to decide if it’s better to finish the slime off early in order to maintain your agility, or ignore it to focus on more vicious foes first. (Remember that by lowering your agility, the slime mold will decrease the number of attacks you can deliver in later rounds of battle.)
(defstruct (slime-mold (:include monster)) (sliminess (randval 5))) (push #'make-slime-mold *monster-builders*) (defmethod monster-show ((m slime-mold)) (princ "A slime mold with a sliminess of ") (princ (slime-mold-sliminess m))) (defmethod monster-attack ((m slime-mold)) (let ((x (randval (slime-mold-sliminess m)))) (princ "A slime mold wraps around your legs and decreases your agility by ") (princ x) (princ "! ") (decf *player-agility* x) (when (zerop (random 2)) (princ "It also squirts in your face, taking away a health point! ") (decf *player-health*))))
The monster-attack
function for the slime mold must do some special things, which allow it to immobilize the player. First, it uses the slime mold’s sliminess (which is generated when each slime mold is built) to generate a random attack against the player, stored in the variable x
. Unlike most other attacks in the game, this slime mold attack affects the agility of players, rather than their health .
However, it would be pointless if the slime mold couldn’t attack the player’s health at least a little, or the battle could end awkwardly, with the player and slime mold frozen in place for all time. Therefore, the slime mold also has a superwimpy squirt attack that happens during half of all attacks , but subtracts only a single health point from the player .
The brigand is the smartest of all your foes. He can use his whip or slingshot and will try to neutralize your best assets. His attacks are not powerful, but they are a consistent two points for every round.
(defstruct (brigand (:include monster))) (push #'make-brigand *monster-builders*) (defmethod monster-attack ((m brigand)) (let ((x (max *player-health* *player-agility* *player-strength*))) (cond ((= x *player-health*) (princ "A brigand hits you with his slingshot, taking off 2 health points! ") (decf *player-health* 2)) ((= x *player-agility*) (princ "A brigand catches your leg with his whip, taking off 2 agility points! ") (decf *player-agility* 2)) ((= x *player-strength*) (princ "A brigand cuts your arm with his whip, taking off 2 strength points! ") (decf *player-strength* 2)))))
The first thing the wily brigand does when performing an attack is to look at the player’s health, agility, and strength, and choose the max
of those three as the focus of his attack . If several of the attributes are equally large, the brigand will choose health over agility and agility over strength as the focus of attack. If health is the largest value, the player is hit with a slingshot . If agility is the largest, the brigand will whip the player’s leg . If strength is the largest, the brigand will whip the player’s arm .
We have now completely defined all of our monsters for our game!
To start the game, call orc-battle
from the REPL:
> (orc-battle)
You are a valiant knight with a health of 30, an agility of 30, and a strength of 30
Your foes:
1. (Health=10) A wicked orc with a level 5 club
2. (Health=3) A malicious hydra with 3 heads.
3. (Health=9) A fierce BRIGAND
4. (Health=3) A malicious hydra with 3 heads.
5. (Health=3) A wicked orc with a level 2 club
6. (Health=7) A malicious hydra with 7 heads.
7. (Health=6) A slime mold with a sliminess of 2
8. (Health=5) A wicked orc with a level 2 club
9. (Health=9) A fierce BRIGAND
10. (Health=2) A wicked orc with a level 6 club
11. (Health=7) A wicked orc with a level 4 club
12. (Health=8) A slime mold with a sliminess of 2
That hydra with seven heads looks pretty gnarly—let’s finish it off first with a stab:
Attack style: [s]tab [d]ouble swing [r]oundhouse:s
Monster #:6
The corpse of the fully decapitated and decapacitated hydra falls to the floor! Your foes: 1. (Health=10) A wicked orc with a level 5 club 2. (Health=3) A malicious hydra with 3 heads. 3. (Health=9) A fierce BRIGAND 4. (Health=3) A malicious hydra with 3 heads. 5. (Health=3) A wicked orc with a level 2 club 6. **dead** 7. (Health=6) A slime mold with a sliminess of 2 8. (Health=5) A wicked orc with a level 2 club 9. (Health=9) A fierce BRIGAND 10. (Health=2) A wicked orc with a level 6 club 11. (Health=7) A wicked orc with a level 4 club 12. (Health=8) A slime mold with a sliminess of 2
No other bad guy really stands out, so we’ll try a roundhouse to bring down some of those health numbers overall:
Attack style: [s]tab [d]ouble swing [r]oundhouse:r
You hit the SLIME-MOLD,
knocking off 1 health points! You hit the SLIME-MOLD, knocking off 1 health points!
You hit the ORC, knocking off 1 health points! You lop off 1 of the hydra's heads!
You lop off 1 of the hydra's heads! You lop off 1 of the hydra's heads! You hit the
ORC, knocking off 1 health points! The corpse of the fully decapitated and decapaci
tated hydra falls to the floor! You hit the ORC, knocking off 1 health points! You hit
the ORC, knocking off 1 health points! You hit the ORC, knocking off 1 health points!
Your foes:
1. (Health=9) A wicked orc with a level 5 club
2. (Health=2) A malicious hydra with 2 heads.
3. (Health=9) A fierce BRIGAND
4. **dead**
5. (Health=2) A wicked orc with a level 2 club
6. **dead**
7. (Health=4) A slime mold with a sliminess of 2
8. (Health=3) A wicked orc with a level 2 club
9. (Health=9) A fierce BRIGAND
10. (Health=2) A wicked orc with a level 6 club
11. (Health=6) A wicked orc with a level 4 club
12. (Health=8) A slime mold with a sliminess of 2
Great! That even killed one of the weaker enemies. Now, with full agility, we have three attacks per round. This means we should use our last attack to strategically take out some of the more powerful bad guys. Let’s use the double swing:
Attack style: [s]tab [d]ouble swing [r]oundhouse:d
Your double swing has a strength of 3
Monster #:8
You killed the ORC!
Monster #:10
You killed the ORC!
An orc swings his club at you and knocks off 5 of your health points. A hydra
attacks you with 1 of its heads! It also grows back one more head! A
brigand catches your leg with his whip, taking off 2 agility points! An orc
swings his club at you and knocks off 1 of your health points. A slime mold wraps
around your legs and decreases your agility by 2! It also squirts in your face,
taking away a health point! A brigand cuts your arm with his whip, taking off 2
strength points! An orc swings his club at you and knocks off 1 of your health
points. A slime mold wraps around your legs and decreases your agility by 1!
You are a valiant knight with a health of 21, an agility of 25, and a strength of 28
Your foes:
1. (Health=9) A wicked orc with a level 5 club
2. (Health=3) A malicious hydra with 3 heads.
3. (Health=9) A fierce BRIGAND
4. **dead**
5. (Health=2) A wicked orc with a level 2 club
6. **dead**
7. (Health=4) A slime mold with a sliminess of 2
8. **dead**
9. (Health=9) A fierce BRIGAND
10. **dead**
11. (Health=6) A wicked orc with a level 4 club
12. (Health=8) A slime mold with a sliminess of 2
They got us pretty good, but we still have plenty of fight left. This battle isn’t over yet!
As you can see, careful strategy is needed if you want to survive Orc Battle. I hope you enjoy this new game!
18.189.180.43