Chapter 8. (Mutant Structs)

#|

At this point, you’ve seen structures and lists. You’ve learned how to write list-processing functions, and you know you can do so concisely. In this chapter, we’ll put that knowledge to good use when we write the Orc Battle game. In the process, we’ll show you a convenient way of dealing with changes to structures.

|#

8.1 Chad’s First Battle

Chad has managed to find himself stuck in an underground coliseum. All around him are monsters, ready for battle. Grabbing a sword, a shield, and some armor, he braces himself for the onslaught. For Chad to have any hope of escaping from his impending doom, you must help him defend himself.

Naturally, it’s all just a game, and to us, it’s all just software. But the Orc Battle is a good training ground to reinforce your list-processing skills and to add a few new ones concerning structs.

8.2 Orc Battle

Orc Battle is a basic turn-based fighting game in which the player fends off some ravenous monsters. The player starts with a certain amount of health, strength, and agility. When the surviving monsters have their turn, they attack the player, damage her health, weaken her strength, or reduce her agility. When it is the player’s turn, she gets some number of attacks and must try to kill as many monsters as possible; the only good monster is a dead monster.

Agility determines the number of attacks the player can perform. The player can attack in two different ways: stab a specific monster or flail, hitting several monsters at the same time. The effectiveness of each kind of attack depends on the player’s strength. As the player’s health dwindles, she can trade an attack for a healing action. While a single healing action doesn’t restore her health, it will extend her life somewhat.

image with no caption
image with no caption

Monsters come in several flavors, yet they all have properties in common. Like the player, every monster has a health status, which determines whether it is alive and the effectiveness of its attack. Each monster, however, attacks the player in its own special way. Orcs, the title characters, hit the player with their clubs. Hydras slice the player, but the ferocity of their attack depends on their health. The slime creature is poisonous. Its attacks slow down the player, meaning they reduce the player’s agility, and it is intent on inflicting some damage to her health. Finally, the brigand is the brains of the horde. It damages the player in random, spur-of-the-moment ways. Depending on its mood, it may inflict damage to the player’s health, decrease her agility, or weaken her strength.

In this chapter, we will show you how to create such a game in Racket. But that’s not all. The real purpose of this chapter is to deepen your understanding of how to program with lists that contain structs, which already contain lists. And that will expand your knowledge of structs.

As always, we start with a data structure that represents the world. The Orc game needs a world with several “moving” parts: a player, who has several changing attributes; a bunch of monsters, which have some common attributes (e.g., their health) and some unique ones; and at least one turn-specific attribute, namely how many attacks the player has left during the current turn.

All of this suggests that we should keep track of the orc world in a three-field struct:

(struct orc-world (player lom attack#))

The player field describes the player’s attributes. The second field is a list of monsters. And the third field counts how many attacks the player may execute.

Next, we should figure out a representation for the player. The player has three changing attributes, so another struct with three fields looks like a good first step:

(struct player (health agility strength))

Each of the three fields will contain a natural number, such as 0, 1, 2, and so on. Here are some constants that describe the range of numbers you might find in a player struct:

(define MAX-HEALTH 35)
(define MAX-AGILITY 35)
(define MAX-STRENGTH 35)

Do you know what this means? As always, we encourage you to open the code in the IDE and experiment. There you will find many more constant definitions, and we hope that all their names explain their meaning as clearly as the three preceding definitions.

You know what lists are, and you should be able to imagine a list of monsters. But what are monsters? Given what we have shown you so far, you could define four struct definitions for the four kinds of monsters:

(struct orc (health club))
(struct hydra (health))
(struct slime (health sliminess))
(struct brigand (health))

All monsters have one property in common: health. Orcs also carry clubs, which come in different strengths; that explains the club field in the orc struct. The sliminess field in the slime struct is similar.

image with no caption

Now let’s make a list of monsters:

> (list (orc MONSTER-HEALTH0 (add1 (random CLUB-STRENGTH))))
(list (orc 9 3))

This is a boring list with just one monster, but it is a list. You could even make an orc-world with this list:

> (orc-world
    (player MAX-HEALTH MAX-AGILITY MAX-STRENGTH)
    (list (orc MONSTER-HEALTH0 (add1 (random CLUB-STRENGTH))))
    0)
(orc-world (player 35 35 35) (list (orc 9 6)) 0)

This world has a player at maximal health, agility, and strength; it contains a list with a single monster; and it says the player has no attacks left. Surely, you can make complex worlds this way, but we find the struct definitions for monsters annoying, and you should, too.

To begin with, they don’t say that these four structs describe a monster. Also, they don’t say that all monsters have a common health field. That’s fixable, and the fix is one of the two ideas we want to introduce in this chapter.

The first idea is called struct inheritance. As it turns out, in Racket you can define a struct as the “child” of another struct. In the context of our game, we can therefore define a monster struct like this:

(struct monster (health))

It says that all monsters have a health field that tells us how healthy a monster is. Next, you can say that all the monsters are like monster:

(struct orc monster (club))
(struct hydra monster ())
(struct slime monster (sliminess))
(struct brigand monster ())
image with no caption

And this means that orc, hydra, slime, and brigand structs are like monster structs. They have a health field, and two of them have additional fields. So when you create an orc, keep in mind that it still has two fields:

> (orc MONSTER-HEALTH0 (add1 (random CLUB-STRENGTH)))
(orc 9 4)

If your functions ever need to select the health field from an orc, they do so with the monster-health function:

> (define my-orc (orc MONSTER-HEALTH0 2))
> (monster-health my-orc)
9
> (orc-club my-orc)
2

For the club, however, you will continue using orc-club. Okay? We will show you more examples as we build the game, and you can always practice with the code in DrRacket.

The second new idea in this chapter concerns what the player’s actions do to our data. We told you that the handler functions in big-bang take the state of the world and return a new one. But there are two distinct ways of getting a new state. We have seen the first one twice now. With this one, the handler functions create new states. So imagine you’re playing the game and you wish to stab an orc:

(define (stab-orc.v1 an-orc)
  (orc (- (monster-health an-orc) DAMAGE) (orc-club an-orc)))

Well, this function isn’t enough. Because the orc struct is somewhere in the middle of the list of monsters, you also need a function that goes through the list, changes the one monster, and re-creates the entire list of monsters. And that isn’t quite enough either because the list of monsters is inside the orc-world struct. You should be able to see what we are getting at. All of this can easily become a mess of code.

The second way to change a world avoids this extensive unzipping and zipping of lists and structs. Instead of creating new worlds, we change some part of the world or, as Lispers say, we mutate the struct. This alternative simplifies stab-orc a lot:

(define (stab-orc.v2 an-orc)
  (set-monster-health! an-orc (- (monster-health an-orc) DAMAGE)))

And stab-orc.v2 is really all you need. In particular, you don’t need to destructure the list, create a new one, restructure the list, and do all of this to orc-world. Mutation eliminates all these computations.

To make this work, we need to say that the health field in an orc struct is mutable:

(struct monster ([health #:mutable]))

With this small addition, the struct definition gives you one more function: the set-monster-health! mutator. Its purpose is to change the value of the orc’s health field. With mutators, just one step lets every other function in the game know that something changed.

The monsters’ health isn’t the only attribute that changes over the course of the game. When it is the monsters’ turn, they can affect all three of the player’s attributes. Again, it is easiest to accommodate such changes by making the entire player struct mutable, like this:

(struct player (health agility strength) #:mutable)

By adding the #:mutable key to the struct definition, we ask Racket to supply one mutator for every field, giving us the set-player-health!, set-player-agility!, and set-player-strength! functions. Again, these mutators are created in addition to the constructor, the predicate, and the accessors.

Each of these mutators consumes an instance of the player struct and a value. It returns nothing and changes the value of a field. By the way, here is how you say “return nothing” in Racket:

> (void)
>

You see, void is a function that produces nothing, and this “nothing” isn’t even worth printing in the interactions panel.

Let’s experiment with player structures in DrRacket, and we will keep encouraging you to do so as well:

> (define player1 (player 1 2 3))
> (set-player-health! player1 33)
>

Nothing seems to have happened. The first line creates an instance of player called player1, and the second line seems to accomplish nothing. Did we say that these mutators return nothing? And did we also mention that instead they change their given struct?

> player1
(player 33 2 3)

Sure enough, player1’s health field is now 33. Okay, take a look at these interactions:

> (define player2 (player 10 20 30))
> (set-player-health! player2 66)
> player2
(player 66 20 30)
> player1
(player 33 2 3)

We did it all over again. And just so you see that nothing but player2 changes this time around, we also looked at player1.

Here are some more interactions:

> (define player3 player1)
> (set-player-agility! player3 666)
> player1
(player 33 666 3)

The first line gives the value of player1 another name: player3. With the second line, we change player3, and this affects player1, too, as the last line shows. We can also do this when such structs are embedded in lists:

> (define players (list player1 player2 player3))

Here, we created a list of three players, though by now you can probably see that the first and the third players refer to the same struct. To test such a conjecture, use the eq? function:

> (eq? (first players) (third players))
#t

Recall from Chapter 4 the difference between eq? and equal?. If not, now would be a good time to reread that section.

Next, we set the second player’s strength field to a large number:

> (set-player-strength! (second players) 999)
> player2
(player 66 20 999)
> (second players)
(player 66 20 999)

And no matter how we refer to this player—by its name or by its position in some list—its strength is now 999.

It’s time to use our new powers: mutators mixed with lambda. For every field, our game will need functions that add numbers to the player’s attributes. Here is the function definition for changing the health of a player:

(define (player-health+ player delta)
  (define nih (interval+ (player-health player) delta HEALTH))
  (set-player-health! player nih))

The function first computes the new internal health value, nih, using interval addition, interval+. This function adds two numbers but makes sure the sum is between 0 and HEALTH. If the result is larger than the desired maximum, internal+ produces the maximum. If the result drops below 0, the function produces 0. In short, you can beat a dead horse, but it won’t get any deader.

In many languages, you would have to repeat this definition three times for health, agility, and strength. Racket, however, gives you more power. It gives you lambda. In this instance, we use lambda to abstract over the essence of the attribute manipulation and create as many of the concrete functions as you want:

(define (player-update! setter selector mx)
  (lambda (player delta)
    (setter player (interval+ (selector player) delta mx)))

This function consumes a setter, a selector, and an mx. It produces a nameless function using lambda, but boy, this nameless function can do a lot:

  • It consumes a player and a number.

  • It accesses the player’s old attribute value, with the help of selector.

  • It computes a new attribute value limiting the interval with the given mx, and then it sets the player’s attribute to this new value with the help of the given setter.

Amazingly, with player-update!, the required three functions become one-liners. Here is one of them:

(define player-health+
  (player-update! set-player-health! player-health MAX-HEALTH))

You can go ahead and define the others, or look them up in the source code.

Mutators are confusing and can really mess with a program if used incorrectly. Make sure that you experiment thoroughly to get a grasp on them before putting them to use.

The dangers of mutation also require us to be more careful with our tests than ever before. If you need to test a function that changes values without returning them, you should have an expression that creates a new instance of the struct, then performs the mutation, and returns the struct. In Racket, we use let expressions for this purpose:

> (let ((p (player 1 2 3)))
    (player-strength+ p -3)
    p)
(player 1 2 0)

A let sets up local definitions. Here, p stands for (player 1 2 3). The inner part of a let expression is called a let body, which includes a sequence of expressions. It is a sequence of actions, as in functions and conds. The first few expressions are evaluated, and their results, if any, are discarded. That’s why the result of this let expression is the modified struct (player 1 2 0). Therefore, in order to formulate a test, we write this:

(check-equal? (let ((p (player 1 2 3)))
                (player-strength+ p -3)
                p)
              (player 1 2 0))

The first argument to check-equal? is a let expression that creates a player and mutates it, using player-strength+. The second argument is a struct that specifies the three desired attributes, and as you can see, its strength field is 0 instead of 3, the strength of the original player.

In summary, mutators make programming complex. We use them because they are powerful; they make changes to structs look concise. But this power is also difficult to control. Testing is no longer a question of calling a function and writing down the expected result. You need to test the effect that a function has by carefully setting up a new struct, returning the modified struct, and describing the expected results. Worse, you don’t know whether the function had other effects. In principle, you should test that nothing else changed unintentionally. Mutators also make it difficult to see which other computations are affected, because the effect of changing a field is visible everywhere—wherever the struct is visible. That is dangerous! Observe how we use mutable structs and fields, and you will learn how to deal with the effects of mutation, too.

When you first have an idea of how to keep track of the state of a game, you should figure out which actions the player can perform. With big-bang, your game players can use the keyboard or the mouse. Keep in mind, clock ticks can change the state of the game.

For the battle with orcs, let’s stick to keyboard actions. They are fast and furious. The “s” key signals stabbing, “f” is for flailing, and an “h” heals the player a bit. What else does attacking mean for the program? It means that at any point in time, some monster is selected as the focus of a potential attack. Since this kind of information is a part of the game state, we equip the orc-world struct with an additional field:

(struct orc-world (player lom attack# target) #:mutable))

We make the struct mutable because the values of the attack# and target fields change during a turn. And we pick arrow keys as a way to change the target.

Next, we need to figure out what kind of values to use with the target field. It tells us which of the monsters in lom is currently targeted. To connect the two fields, we stick a natural number into the target field and use this number to look up the currently targeted monster in lom:

> (define lom (list (orc 9 3) (orc 9 4) (orc 9 1)))
> (list-ref lom 0)
(orc 9 3)
> (define ow1 (orc-world 'some-player lom 2 0))
> (list-ref (orc-world-lom owl) 2)
(orc 9 1)

So 0 in the target field of an orc-world indicates the first monster on the list, and 2 refers to the third one. All set?

It is time to get started:

(define (start)
  (big-bang (initialize-orc-world)
            (on-key player-acts-on-monsters)
            (to-draw render-orc-battle)
            (stop-when end-of-orc-battle? render-the-end)))

This function starts the Orc Battle game. Naturally, it uses big-bang. Let’s look at all the parts. The initialize-orc-world function creates an initial state of the world, and doing so is pretty straightforward:

(define (initialize-orc-world)
  (define player0 (initialize-player))
  (define lom0 (initialize-monsters))
  (orc-world player0 lom0 (random-number-of-attacks player0) 0))

This combines a player, some monsters, a random number of attacks, and an initial target that points to the first monster on the list into an orc-world.

The remaining three clauses of big-bang in start describe how the world is transformed when the player presses a key during her turn, how to render the current state of the world as an image, and how to check whether the battle is over. The last clause also says that big-bang should use render-the-end when the battle is over, because we need to announce who won.

Let’s look at these functions one at a time, starting with end-of-orc-battle?:

(define (end-of-orc-battle? w)
  (or (win? w) (lose? w)))

The battle is over when the player wins or loses. We’ll look at these two helpers later.

Rendering the battle or the end of the battle is also easy:

(define (render-orc-battle w)
  (render-orc-world w (orc-world-target w) (instructions w)))

(define (render-the-end w)
  (render-orc-world w #f (message (if (lose? w) LOSE WIN))))

In both cases, we use render-orc-world, which consumes three arguments: the current state of the world, the current target or #f if there is no target anymore, and a message to the player. No matter what, render-orc-world must display three pieces of information: the player’s status, the monsters, and some additional information. While the player is engaged in battle, the additional information consists of the game instructions, which mention some properties of the current state of the world. At the end of the game, the information tells the player who has won.

Before we move on, let’s sketch out the rendering process just enough to see how a player experiences the game. The key is that the rendering function must display a list of monsters. We don’t know how long the list is, but we do know we want to break it up into several rows so that the player can navigate easily among the monsters. So we pick a constant PER-ROW and say that this many monsters appear on each row. Put differently, if the game is launched with MONSTER#, then the display shows (quotient MONSTER# PER-ROW) rows of monsters, with PER-ROW monsters on each row. Here is what it might look like if you render the game with 12 monsters, 4 to a row:

> (render-orc-battle (initialize-orc-world))
image with no caption

In DrRacket, you don’t need to launch a game to render a game state. You just call the rendering functions on the same game state, and you see what the player may see. She sees an image of her status on the left, the monsters arranged in a rectangular grid on the right, and the instructions below. Also note how the first monster is marked by a circle; it is the current target of the player. Remember that to change the target, the player uses the arrow keys to move around the grid of monsters.

And that brings us to the last function, the one that deals with key strokes:

(define (player-acts-on-monsters w k)
  (cond
    [(zero? (orc-world-attack# w)) (void)]
    [(key=? "s" k) (stab w)]
    [(key=? "h" k) (heal w)]
    [(key=? "f" k) (flail w)]
    [(key=? "e" k) (end-turn w)]
    [(key=? "n" k) (initialize-orc-world)]
    [(key=? "right" k) (move-target w +1)]
    [(key=? "left" k)  (move-target w -1)]
    [(key=? "down" k)  (move-target w (+ PER-ROW))]
    [(key=? "up" k)    (move-target w (- PER-ROW))])
  (give-monster-turn-if-attack#=0 w)
  w)

This function looks more like the first version of Guess My Number than anything else you have seen in this book. So far, you have seen functions that consist of a header, which lists the name of the function and the names of the arguments, and a body, which is usually just one expression that computes the result of the function. Now with player-acts-on-monsters, you see a function whose body consists of three expressions: a conditional, a function call to give-monster-turn-if-attack#=0, and the variable w.

When the function is called, the first expression is evaluated and its result is thrown away. The second expression is evaluated, and the result is thrown away again. Finally, the last expression is evaluated, and its value becomes the result of the whole function.

You may now think that player-acts-on-monsters is a really strange function. According to our explanation, it always returns w, the world that it is given when the player presses the “k” key. So here is the catch. With the introduction of mutators, the evaluation of an expression doesn’t just produce a value—it may also affect the evaluation of other expressions.

The conditional in the body of player-acts-on-monsters determines whether and how to change w in response to the player’s action on the keyboard. The function call to give-monster-turn-if-attack#=0 checks whether the monsters can change w. Both expressions return nothing. In the end, player-acts-on-monsters evaluates the expression w, which has dramatically changed because of the first two expressions.

Now let’s look in detail at the three steps. The conditional first checks whether the player may still attack. If not, the conditional uses void to perform no action. You may want to figure out why it would have been acceptable to write 42 instead of (void). If the player can still attack, the function checks which of the nine keys the player may have pressed. The first three keys trigger calls to functions that change the player or the monsters in the world. The next two are about ending the player’s turn or starting over.

The final four cond clauses correspond to navigation actions; each calls move-target to change the index in the target field. The responses to the “←” and “ →” keys seem obvious: they ask move-target to change the current target by adding +1 or -1 to the current target. The responses to “↓” and “↑” use the PER-ROW constant. Remember that the list of monsters is rendered as a grid with PER-ROW monsters in each row. Thus, if the player presses “↑”, the key event tells the program, “Go up one row in the display.” Similarly, if the player presses “↓”, it means, “Go down one row in the display.” Because all monsters are arranged in a linear list, and the current target monster is just an index into this list, move-target adds or subtracts PER-ROW monsters to this index and keeps the index in range.

Once the conditional is finished checking all conditions, the player may have used up all of her attacks. In that case, it is time to let the monsters loose. All live monsters will attack the player at once. When the monsters’ attack is over, it’s time to hand control back to big-bang by returning the modified world w. Doing so allows the player to take her next turn—unless of course the game is over.

Our initialization process relies on three functions. Two are simple:

(define (initialize-player)
  (player MAX-HEALTH MAX-AGILITY MAX-STRENGTH))

(define (random-number-of-attacks p)
  (random-quotient (player-agility p) ATTACKS#))

The first creates a player with maximal health, agility, and strength. The second picks a random number of attacks, like this:

(define (random-quotient x y)
  (define div (quotient x y))
  (if (> 0 div) 0 (random+ (add1 div))))

(define (random+ n)
  (add1 (random n)))

This random-quotient function picks a random number between 1 and (quotient x y) via random+ unless the quotient is 0. In our specific case, this means that the number of attacks is determined as a random fraction of the player’s agility. Naturally, we define random-quotient and random+ as separate functions because we will use them again. If you can think of another way to determine a random number of attacks, try it. The code is all yours.

The third initialization function creates a list of monsters:

(define (initialize-monsters)
  (build-list
    MONSTER#
    (lambda (_)
      (define health (random+ MONSTER-HEALTH0))
      (case (random 4)
        [(0) (orc ORC-IMAGE health (random+ CLUB-STRENGTH))]
        [(1) (hydra HYDRA-IMAGE health)]
        [(2) (slime SLIME-IMAGE health (random+ SLIMINESS))]
        [(3) (brigand BRIGAND-IMAGE health)]))))

And voilà, you see lambda and build-list in action, and case is thrown in there, too. Since the goal is to create a list, the function uses build-list, which consumes the number of monsters we wish to start with and a function. We use lambda to make this function because it is used only here, it isn’t recursive, and it is straightforward. After picking a random health, a random number is used to return one of four possible monsters.

image with no caption

We are almost finished, except for two little things. First, lambda uses _ as a parameter. In Racket, _ is just a plain old variable name, pronounced “underscore.” Racketeers, by convention, use it to indicate that a function ignores this parameter. Remember that build-list applies this function to all numbers between 0 and MONSTER#. Since we don’t need this number for anything in our world of orcs, the lambda function throws it away.

Second, if you look closely at all four constructors, you’ll notice that they take one more argument than we’ve admitted: an image. Because we want to visualize the monsters at some point, we might as well stick their images into the monster structure once and for all:

(struct monster (image [health #:mutable]))

With the revision of this definition, all monster structs have two fields: image, which never changes, and health, which may change over the course of the game. Every substructure inherits these two fields. Because of inheritance, we needed to make only a single change to the program: the definition of the monster struct. Later on, when our program must render the monsters, it can use monster-image to pull out the images from the monsters.

The basic world setup mentions two small functions that render the instructions and the win and lose messages:

(define (instructions w)
  (define na (number->string (orc-world-attack# w)))
  (define ra (string-append REMAINING na))
  (define txt (text ra INSTRUCTION-TEXT-SIZE ATTACK-COLOR))
  (above txt INSTRUCTION-TEXT))

(define (message str)
  (text str MESSAGES-SIZE MESSAGE-COLOR))

The instructions are always the same, except for the number of remaining attacks. To show the remaining attacks, we convert the number into a string, stick it into an appropriate sentence, and convert the string to an image of the text. Otherwise, instructions assembles the two text images into one. The message function is even simpler than instructions. It merely turns the given string into text using an appropriate size and color.

The real workhorse of the rendering section is render-orc-world, a function that consumes three arguments and produces an image of the current world:

(define (render-orc-world w t additional-text)
  (define i-player  (render-player (orc-world-player w)))
  (define i-monster (render-monsters (orc-world-lom w) t))
  (above V-SPACER
         (beside H-SPACER
                 i-player
                 H-SPACER H-SPACER H-SPACER
                 (above i-monster
                        V-SPACER V-SPACER V-SPACER
                        additional-text)
                 H-SPACER)
         V-SPACER))

You should recall that render-orc-world is used in two different ways: during the game and after the game is over. For the former situation, it is called with the current state of the world, an index that specifies the target in the list of monsters, and instructions for the player. For the latter situation, the function is also applied to the state of the world—or #f if there is no live target left—and a termination message. In short, the second argument is either a number or a Boolean value, and you need to keep this in mind.

As you would expect, the function takes apart the given world struct and uses helper functions to render the two important pieces: the player and the monsters. Once it has images for these, it is simply a question of assembling them into the right layout.

You can imagine that we didn’t arrive at this format without some experimentation. Once we had defined render-player and render-monsters, here is how we started experimenting in the interactions panel:

> (beside (render-player (initialize-player))
          (above (render-monsters (initialize-monsters))
                 (message "You win")))

We didn’t like how everything was squeezed together. So we played around. We added vertical and horizontal spacers, which are just a constant rectangles with a funny x or y dimension:

(define V-SPACER (rectangle 0 10 "solid" "white"))
(define H-SPACER (rectangle 10 0 "solid" "white"))

Moving on, here is the function that creates an image for the player:

(define (render-player p)
  (define s (player-strength p))
  (define a (player-agility p))
  (define h (player-health p))
  (above/align
    "left"
    (status-bar s MAX-STRENGTH STRENGTH-COLOR STRENGTH)
    V-SPACER
    (status-bar a MAX-AGILITY AGILITY-COLOR AGILITY)
    V-SPACER
    (status-bar h MAX-HEALTH HEALTH-COLOR HEALTH)
    V-SPACER V-SPACER V-SPACER
    PLAYER-IMAGE))

By now, you know the routine for this kind of function. The argument is a player struct. The function takes it apart, extracting the three pieces: strength, agility, and health. Then it uses one auxiliary function to create the pieces of the final image: status bars for health, agility, and strength. Guess what above/align does? It jams together a bunch of images, one above the other, and the first argument—a string—tells it how to align these images.

Let’s take a look at the auxiliary function, status-bar:

(define (status-bar v-current v-max color label)
  (define w (* (/ v-current v-max) HEALTH-BAR-WIDTH))
  (define f (rectangle w HEALTH-BAR-HEIGHT 'solid color))
  (define b (rectangle HEALTH-BAR-WIDTH HEALTH-BAR-HEIGHT ...))
  (define bar (overlay/align "left" "top" f b))
  (beside bar H-SPACER (text label HEALTH-SIZE color)))

It consumes the current value, the maximal value, the desired color, and a string label to attach to the bar. The function first computes the width of the health bar: (* (/ v-current v-max) HEALTH-BAR-WIDTH). If v-max should be displayed as HEALTH-BAR-WIDTH, then (/ v-current v-max) is the ratio of this number that determines how v-current should be displayed. Once it has this number, the function creates a filled and framed rectangle to create the bar. The final expression attaches label as a text image.

And that leaves us with rendering all the monsters, the trickiest function of the bunch. It consumes a list of monsters and an argument that indicates which of them is the current target, if any:

(define (render-monsters lom with-target)
  ;; the currently targeted monster (if needed)
  (define target
    (if (number? with-target)
        (list-ref lom with-target)
        'a-silly-symbol-that-cannot-be-eq-to-an-orc))

  (define (render-one-monster m)
    (define image
      (if (eq? m target)
          (overlay TARGET (monster-image m))
          (monster-image m)))
    (define health (monster-health m))
    (define health-bar
      (if (= health 0)
          (overlay DEAD-TEXT (status-bar 0 1 'white ""))
          (status-bar health MONSTER-HEALTH0 MONSTER-COLOR "")))
    (above health-bar image))

(arrange (map render-one-monster lom)))
image with no caption

Now you need to remember the images of the monsters. Clearly, render-monster must turn all monsters into images. With map, that’s easy. All we need is (map render-one-monster lom) because this applies render-one-monster to every monster on the lom list and you have a list of images. If you arrange them properly, you get the full scene of all monsters, but for now, let’s postpone the discussion of arrange and focus on the images of the monsters.

At this point, it is time to exploit the with-target argument. It is either a number or #f. If it is a number, the function must mark the current target. If it is false, there is no current target, and the game is over. The first definition inside of render-monsters turns with-target into one of the monsters from the list, using list-ref, or a symbol. The render-one-monster uses this value to determine whether the given monster is the current target.

To get an image of one monster, render-one-monster extracts the image from the given monster struct and adds the target marker, if the monster is eq? to the target. The rest of the function creates a health bar for the monster. Two things stand out, though. First, look how we use eq? to compare render-one-monster’s argument with the target. We really want this specific struct on the lom list, not a struct with the same content—that is, equal? would be wrong. That’s what eq? is for. Second, the health-bar calculation looks complex because we want the graphics to look good when monsters die. Experiment with the code to see what happens when you simplify the first branch of the if expression that computes the health-bar image. Then figure out what a health bar looks like if it has a current value of 0, a maximum value of 1, white color, and an empty label. It is just an invisible rectangle. So this three-line calculation in render-one-monster just ensures that the rectangle is exactly as tall as an empty health bar.

Here is the final function needed for rendering a world, arrange:

(define (arrange lom)
  (cond
    [(empty? lom) empty-image]
    [else (define r (apply beside (take lom PER-ROW)))
          (above r (arrange (drop lom PER-ROW)))]))

This is a recursive function, though it only superficially looks like the kinds of list-eating functions we have seen before. After making sure that the list isn’t empty, arrange retrieves the first PER-ROW images with take, which is one of those general-purpose built-in list functions you just have to know:

> (take '(a b c d e f) 2)
'(a b)
> (drop '(a b c d e f) 2)
'(c d e f)

With (apply beside ...), the arrange function creates the image of a row, and with (above r ...), it layers this row above the remaining ones. To remove the first PER-ROW images, arrange uses drop, which, like take, comes with Racket. So yes, arrange is recursive, but it doesn’t eat the list on a one-by-one basis. Read it again.

When does the world come to an end? Well, it’s over when all the monsters are dead or one of the player’s attributes sinks to 0:

(define (win? w)
  (all-dead? (orc-world-lom w)))

(define (lose? w)
  (player-dead? (orc-world-player w)))

(define (player-dead? p)
  (or (= (player-health p) 0)
      (= (player-agility p) 0)
      (= (player-strength p) 0)))

(define (all-dead? lom)
  (not (ormap monster-alive? lom)))

(define (monster-alive? m)
  (> (monster-health m) 0))

And that’s all it takes. The only interesting function is all-dead?, which uses ormap to determine whether any of the monsters are alive. Are you wondering why monster-alive? is defined separately? We could have just made it a lambda within the all-dead? function, but monster-alive? is also useful when it comes to executing actions.

We’ve kept the game actions for last. All actions consume the current state of the world and mutate it. The player has five actions, all triggered in response to certain keystrokes: stab, heal, flail, end-turn, and move-target. The monsters have one action, aptly called give-monster-turn-if-attack#=0, because its name tells us that it runs a monster turn if the player has no attacks left.

With that in mind, let’s tackle the implementation of actions. One function has a trivial definition:

(define (end-turn w)
  (set-orc-world-attack#! w 0))

If the player really wishes to end her turn prematurely, just set the number of remaining attacks to 0. Then, give-monster-turn-if-attack#=0 takes over and gives the monsters their turn.

Like end-turn, the definition of heal is obvious:

(define (heal w)
  (decrease-attack# w)
  (player-health+ (orc-world-player w) HEALING))

A player can trade one attack for one healing action. So heal pays for the action and then calls a function that increases the player’s health by some constant factor.

The definition of heal suggests a pattern for other actions, namely, to first call decrease-attack# to pay for the attack and then to execute some action. And yes, we see this pattern again in the definition of stab:

(define (stab w)
  (decrease-attack# w)
  (define target
     (list-ref (orc-world-lom w) (orc-world-target w)))
  (define damage
    (random-quotient (player-strength (orc-world-player w))
                     STAB-DAMAGE))
  (damage-monster target damage))

In addition, stab immediately extracts the targeted monster from the world. As for the stabbing, stab picks a random damage value based on the player’s current strength and a constant dubbed STAB-DAMAGE. Then it calls a function that inflicts the chosen damage on the chosen monster. That’s all. Yes, we owe you a little helper function, damage-monster, but as you will see later, it is really straightforward.

Our player’s most complicated form of attack is flail. Recall that its purpose is to attack a whole bunch of live monsters, starting from the chosen target:

(define (flail w)
  (decrease-attack# w)
  (define target (current-target w))
  (define alive (filter monster-alive? (orc-world-lom w)))
  (define pick#
    (min
     (random-quotient (player-strength (orc-world-player w))
                      FLAIL-DAMAGE)
     (length alive)))
  (define getem (cons target (take alive pick#)))
  (for-each (lambda (m) (damage-monster m 1)) getem))

Like stab, flail also pays for the attack and picks the current target. Then it filters the living monsters from the dead ones and chooses the number of monsters that can be attacked, a number that depends on the player’s strength and the global constant FLAIL-DAMAGE. Finally, flail will cons the current target on to the list of chosen monsters so that it can attack them all—a little bit. Notice that this may damage the currently targeted monster twice, but so it goes when you flail wildly.

We have yet to define the three helper functions that the player’s actions rely on. They aren’t complicated, and we aren’t hiding anything. The names of the functions tell you what they do, and their definitions are one-liners:

(define (decrease-attack# w)
  (set-orc-world-attack#! w (sub1 (orc-world-attack# w))))

(define (damage-monster m delta)
  (set-monster-health! m (interval- (monster-health m) delta)))
(define (current-target w)
  (list-ref (orc-world-lom w) (orc-world-target w)))

The first one, decrease-attack#, subtracts one from the attack# field in the given world w. The second one performs interval arithmetic on the monster’s health—the same kind we used for manipulating the player’s attributes. And the third one extracts the currently targeted monster from the list.

If you are at all confused here, take a breath. Open the code in the chapter directory. Play with some of the functions. Make up a world. Call the function on the world. Interact with the world.

Time to tackle the last function that player-acts-on-monsters demands:

(define (move-target w delta)
  (define new (+ (orc-world-target w) delta))
  (set-orc-world-target! w (modulo new MONSTER#)))

Not surprisingly, move-target adds the given delta to the current target index. But this addition may produce a number that is too large or too small, a number that goes beyond the end of the list or before its beginning. So before we store this new target number into the target field of w, we use the modulo function to find the appropriate number between 0 and the actual number of monsters.

It may have been a while since your teacher tormented you with the modulo function, so let’s experiment. First, we set up the world:

> (define p (player 3 4 5))
> (define lom (list (monster 0 2) (monster 1 3)))
> (define a-world (orc-world p lom 1 0))

Next, we call move-target on a-world:

> (move-target a-world 1)
> a-world
(orc-world (player 5 5 5) (list (monster 0 2) (monster 1 3)) 1 1)

As you can see, move-target has no result, but it has an effect on a-world. So we check to find out what happened to it.

Now it’s your turn. Try move-target on some of the numbers we used here, like +1, -1, (+ PER-ROW), (- PER-ROW). Watch what happens in various situations.

Of course, it’s actually the monsters’ turn. We still need to create all the functionality to make our fearsome monsters attack, suffer, and retreat. The first function we need to define is give-monster-turn-if-attack#=0:

(define (give-monster-turn-if-attack#=0 w)
  (when (zero? (orc-world-attack# w))
    (define player (orc-world-player w))
    (all-monsters-attack-player player (orc-world-lom w))
    (set-orc-world-attack#! w (random-number-of-attacks player))))

We know the function consumes a world. Its name tells us what it does, and its definition shows how it accomplishes all this. Remember that when is like a cond expression with just one branch. This one branch may contain a series of definitions and expressions, just like a function body. In this particular case, the when expression checks whether the player has any attacks left for this turn. If the player has no attacks left, the function extracts the player from the given world and gives the monsters their due. Once they are finished damaging the player, give-monster-turn-if-attack#=0 sets the number of attacks for the next player’s turn. Put differently, the all-important line in this function is the second one in the when expression, which calls all-monsters-attack-player with the player and the list of monsters.

And what do you think all-monsters-attack-player does? It does exactly what its name says: it allows each live monster to attack the player in its orcish, hydranous, slime-a-licious, or brigandy way:

(define (all-monsters-attack-player player lom)
  (define (one-monster-attacks-player m)
    (cond
      [(orc? m)
       (player-health+ player (random- (orc-club m)))]
      [(hydra? m)
       (player-health+ player (random- (monster-health m)))]
      [(slime? m)
       (player-health+ player -1)
       (player-agility+ player
                        (random- (slime-sliminess monster)))]
      [(brigand? m)
       (case (random 3)
         [(0) (player-health+ player HEALTH-DAMAGE)]
         [(1) (player-agility+ player AGILITY-DAMAGE)]
         [(2) (player-strength+ player STRENGTH-DAMAGE)])]))
  (define live-monsters (filter monster-alive? lom))
  (for-each one-monster-attacks-player live-monsters))

When someone explains a function and says, “Every actor executes some action,” you want to use for-each to code it up. It is kind of like map, but it doesn’t collect any results. And when someone says, “All items on this list that are good,” you use filter to extract the good items from this list. And that explains all-monsters-attack-player almost completely. The only point left to explain is what each monster actually does to the player, but as you can see, that’s also spelled out in one single, locally defined function: one-monster-attacks-player. It consumes a monster, determines which kind of monster it is, and from there, it’s all downhill for the player. Go read the definition of this last remaining function and explain to yourself what each monster does.

Then fight the monsters and save Chad. Good luck!

In this chapter, we used structures to model the world:

image with no caption