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:
Here is the initial state we will work with:
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:
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)
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:
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:
A stab is the strongest attack we have available to us, but we can only stab at one foe at a time.
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.
A roundhouse is a chaotic attack that attacks random foes multiple times…
It is also very weak though, with only a power of 1.
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!
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)
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:
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)))
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 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.
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.
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!
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*)
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!
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! "))))
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!
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 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 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!
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:
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!
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!