Skip to content

Latest commit

 

History

History
750 lines (595 loc) · 22.8 KB

orcBattle.org

File metadata and controls

750 lines (595 loc) · 22.8 KB

Orc Battle!

Orc Battle is a game where you are a knight surrounded by 12 monsters, and you have to fight and defeat all of them using a repertoire of sword-fighting abilities.

First! We need to define some state the game will use:

Globals

Here is the initial state we will work with:

Player

We need to keep track of several aspects of the player, all which will be modified during normal gameplay.

First of all, we need to track the health of the player… If this reaches zero, the player dies:

(defparameter *player-health* nil)

Then we have to keep track of the player’s agility. This will determine how many attacks the player can perform in a single turn:

(defparameter *player-agility* nil)

And finally, we have the player’s strength, which will determine how powerful the player’s attacks are:

(defparameter *player-strength* nil)

Now for the baddies:

Monsters

First of all, we will have an array of monsters; we know how many monsters there are, so this would be the perfect time to break out an array.

This array will be heterogeneous, which means it will contain different types of data inside of it.

(defparameter *monsters* nil)

We will also create a number of “builder” functions, functions that will create different types of monsters for use in our game, these will be stored in a global list:

(defparameter *monster-builders* nil)

And finally, we want to define the number of baddies to pit the player up against:

(defparameter *monster-num* 12)

The main functions

This time around, we will do things slightly different from what we usually do: we will write the main function for the game first.

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

This gives us a pretty meta look at the way the game will work, with the above code, we can infer that we will initialize monsters and the player first, and that game-loop will run until the player wins or loses, and the conditions for either of these are done with the player-dead and monsters-dead functions.

The suggested game-loop function is nothing too serious either:

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

More easy stuff here… Though a detail we should note is that we are using map here because we are working with an array; mapcar doesn’t do the right thing here, it would just error when attempting to iterate since there are no linked cons cells to walk through.

The idea here is that we will use methods to implement the monster-attack functions for each monster.

Now, lets write some of the functions that we have used above, first up is of course, player information:

Player actions, information, initialization and status

First thing we need to do before we can check or modify any of a player’s stats, is to initialize them:

(defun init-player ()
  (setf *player-health* 30)
  (setf *player-agility* 30)
  (setf *player-strength* 30))

And now we can check if the player is dead:

(defun player-dead ()
  (<= *player-health* 0))

And now we can output the player’s stats:

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

And now that we have a way to display all of the information that we need to show about the player, we can write a function that allows us to 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))))))

We have three different types of attacks here that we are capable of using… This is where the strategy comes into play, the attacks are:

Stab - [s]

A stab is the strongest attack we have available to us, but we can only stab at one foe at a time.

Double Swing [d]

A double swing is weaker than a stab, but we have the ability to attack two enemies with it simultaneously.

We also can see the power of the attack before we use it, so we can choose more optimally the enemies we want to attack.

Roundhouse [r]

A roundhouse is a chaotic attack that attacks random foes multiple times…

It is also very weak though, with only a power of 1.

Player helper functions

The previous definitions called some helper functions we have not yet defined. The simplest is probably the randval function, which can be defined as follows:

(defun randval (n)
  (1+ (random (max n 1))))

The reasoning behind this definition is that we always want the random value returned to be at least a “1”, since 0 really doesn’t make sense for a fair number of numbers in the game… For example, attacks should always have a t least a power of 1, and monsters should always have a strength larger than 0.

Now we need a function to select a target, the suggested code is as follows:

(defun random-monster ()
    (let ((m (aref *monsters* (random (length *monsters*)))))
      (if (monster-dead m)
          (random-monster)
          m)))

I notice with a cursory glance that this function would fail terribly, and busy-loop if all monsters are dead. But I suppose the code in the book has a reason… So, time to read on!

We also need a way to select a monster for non-random attacks, which is done with pick-monster:

(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 already dead.")
                     (pick-monster))
              m)))))

This is another simple function that just checks if your input is valid, and if not, recursively calls itself.

Now time to build some monsters!

The generic monster!

Here is where we start actually doing some generic programming!

First of all, we will create a struct that will define what our “generic” monster looks like:

(defstruct monster (health (randval 10)))

As you can see here, we can actually define an initial value for the slots in a structure… So when we call make-monster, we will always get a monster with a random quantity of health:

(make-monster)
#S(MONSTER :HEALTH 7)

Generic attacking

When we attack any monster, their health will go down, so we can create a method with defmethod that will have the generic code for all 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! "))))

This method uses a new type-of function, that, handily, we don’t actually have to code, as it will return the type of its parameter as a string. For example, if called with our generic monster structure, we would get:

(type-of (make-monster))
MONSTER

We also use decf, which works to subtract an amount from a value and setf it, which is the exact opposite of incf.

Now, for the other generic monster functions:

Generic monster showing

With the neat typeof, we don’t really need to be able to determine the type of the structure we are looking at with specific implementations that merely return their name, so we can simply do the following:

(defmethod monster-show (m)
  (princ "A fierce ")
  (princ (type-of m)))

Generic monster attacking

Monster attacks are unique, with no resemblance to each other, so the method with no qualifiers for monsters is just empty:

(defmethod monster-attack (m))

Now, we are ready to implement individual enemies!

Orcs!

Orcs are the simplest enemy. They can attack with a strong attack from their club, and each orc club has a unique attack value that determines how strong it is.

They, overall, aren’t terribly dangerous. Though if one has a killer club, you will still want to watch out for it.

We will create this monster by creating another struct, except we will use a feature of defstruct that we haven’t seen before: We will include all of the fields of monster on orc:

(defstruct (orc (:include monster)) (club-level (randval 8)))

As you can see, we can define a struct that this struct “inherits” from by replacing the symbol name with a list containing the name, and a nested list that has an :include keyword followed by the struct to inherit from.

Now, we can simply push the automatically-created make-orc function into our *monster-builders list.

(push #'make-orc *monster-builders*)

Now we can specialize the methods we implemented above for orcs.

Orc showing

Despite the fact we can see that we are looking at an orc when we use the existing monster-show method on orcs, we are going to write a more specialized version of it to provide more information about the orc. (Or more specifically, the club it is holding.)

(defmethod monster-show ((m orc))
  (princ "A wicked orc with a level ")
  (princ (orc-club-level m))
  (princ " club"))

That way, the player can see which orcs are the most dangerous at a glance.

Orc attacks

Orcs have different types of clubs, so, we need to account for that in our orc implementation of monster-attack:

(defmethod monster-attack ((m orc))
  (let ((x (randval (orc-club-level m))))
    (princ "An orc swings at you and knocks off ")
    (princ x)
    (princ " of your health points. ")
    (decf *player-health* x)))

In the implementation above, we take a random amount of health between 1 and the level of the club and princ out a message indicating such.

Whelp, that’s one enemy implementation down!

Horrible Hydras!

Hydras are nasty. They have many heads…all of which can attack you. They can also grow a head every turn, making them more dangerous the longer you put off tackling them. To top it off, you need to chop off every hydra head to defeat it, so it also regenerates.

So, let’s make another monster-inheriting struct, the hydra!

(defstruct (hydra (:include monster)))

Why no heads field? Because we will use its health to determine this!

And push the automatically created make-hydra:

(push #'make-hydra *monster-builders*)

Show, the Hydra implementation

Hydras can have many heads, so we will want to take this into account for the hydra-displaying method:

(defmethod monster-show ((m hydra))
  (princ "A malicious hydra with ")
  (princ (monster-health m))
  (princ " heads."))

Now for the other unique implementations!

Hydras act differently when hit

Since we are using the health of the hydra as the head-count, we want to create a more specialized method for monster-hit that will communicate this:

(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! "))))

Hydras can attack with multiple heads!

Hydra heads aren’t terribly dangerous on their own, but they have a lot of them, and can attack with half of them at the same time… And they grow more!

Our monster-attack implementation for hydras will allow it to attack for one point of damage per head, but only a random number of possible heads will actually attack.

After each attack, it is also a good time to let the hydra grow another head and regenerate a little.

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

And that’s it for the hydra implementation!

Slime mold

While a delectable consumable in the likes of Nethack, in our game here, it is a foe that instead restricts you movements and makes it easier for other foes to finish you off due to your reduced mobility.

Of course, first of all we need to define a struct for slime molds:

(defstruct (slime-mold (:include monster)) (sliminess (randval 5)))

The pushing of make-slime-mold:

(push #'make-slime-mold *monster-builders*)

And then the unique implementations:

Slime mold showing

Slime molds have a special sliminess field, so our monster-show implementation should show it, allowing our valiant hero to decide where their priorities lie.

(defmethod monster-show ((m slime-mold))
  (princ "A slime mold with a sliminess of ")
  (princ (slime-mold-sliminess m)))

Simple enough. Now for the unique aspects of the monster!

Slime mold attacks

Slime molds can attack, but they aren’t really a threat damage-wise since they can only deal one point of damage to you. No, rather they are a threat because they decrease the player’s agility and decrease the amount of times that they can attack. This means that they are easier taken-out by other enemies.

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

And that’s it for the Slime mold implementation!

Brainy Brigands

A brigand in our game is a clever enemy. They aren’t very strong, but they will try to make your best stats weaker, be it health, agility, or strength.

They are stat drainers like slime molds, so the player will need to be careful around these.

So, we should know by now, we will need to make a struct for brigands:

(defstruct (brigand (:include monster)))

…and push the maker function to the *monster-builders*:

(push #'make-brigand *monster-builders*)

Now, the only thing really unique about a brigand is that they are smart in how they attack, so all we need to do is implement a specialized monster-attack method, the rest is handled by our generic monster:

Brigand attacking style

Brigands have a slingshot and a whip they can use to damage the player, injure them to decrease their strength, or trip them up decreasing their agility.

Here is our brigand attacking code:

(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 points1 ")
           (decf *player-strength* 2)))))

It just gets our highest stat, and then the brigand then chooses to attack our highest stat, decreasing it by 2 points.

That’s it!

Starting the game

Now that we have developed all of the functions we promised, we have our game complete!

We can do so by running:

(orc-battle)

Happy battling!

Metadata