The Orc Battle Game

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!

image with no caption

Global Variables for the Player and Monsters

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)

Main Game Functions

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 .

Player Management Functions

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

Helper Functions for Player Attacks

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.

Monster Management Functions

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

The Monsters

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.

The 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 Wicked Orc

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*)

Note

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.

image with no caption

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 Malicious Hydra

image with no caption

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 Slimy Slime Mold

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

image with no caption
(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 Cunning Brigand

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)))))
image with no caption

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 Battle!

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!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.180.43