Creating Custom Game Commands for Wizard's Adventure Game

image with no caption

If you remember, when we last encountered the game starring our wizard and apprentice in Chapter 5 and Chapter 6, we could walk around the world and pick up objects. However, we couldn’t really perform any other interesting or fun actions. To make a game fun, it should include special actions that can be performed with certain objects and/or at certain locations in the game. We need frogs that can be kissed, dragons that can be fought, and perhaps even maidens that can be rescued!

Creating these kinds of interesting activities in the game poses a unique challenge. On the one hand, there are clearly many similarities between such different game actions. For instance, most of them will require us to have an object in our possession. On the other hand, they all need to have unique and idiosyncratic properties (enabled through command-specific Lisp code) or the game becomes boring. As you’ll see, a DSL can help you add many such unique commands to your game.

To run the code from here until the end of this chapter, we’re going to use all the game code from Chapter 5 and Chapter 6. Just put the code from those chapters into a file named wizards_game.lisp (or download wizards_game.lisp from http://landoflisp.com/). As soon as the game is loaded, you can type game commands like look directly in the CLISP REPL. Alternatively, you can use the game-repl command we created in Chapter 6 to get a more polished game experience. Remember that the quit command will take you out of the game REPL.

Here’s what you do to load the game code from the REPL and start running game commands:

> (load "wizards_game.lisp")
;; Loading file wizards_game.lisp ...
;; Loaded file wizards_game.lisp
T
> (look)
(YOU ARE IN THE ATTIC. THERE IS A GIANT WELDING TORCH IN THE CORNER. THERE IS
A LADDER GOING DOWNSTAIRS FROM HERE.)
> (game-repl)
look
You are in the living-room. A wizard is snoring loudly on the couch. There is
a door going west from here. There is a ladder going upstairs
from here. You see a whiskey on the floor. You see a bucket on the floor.
quit

Creating New Game Commands by Hand

So what should our game DSL look like? The only way to really know is to first create some commands by hand. Then we can see if there are any common patterns between different commands that we can use as the basis of our DSL.

A Command for Welding

image with no caption

In the attic of the wizard’s house is a welding machine. Let’s allow the players to weld the chain to the bucket if they bring those items to that location. Here’s the code to make this happen:

 (defun have (object)
    (member object (inventory)))

 (defparameter *chain-welded* nil)

 (defun weld (subject object)
   (if (and (eq *location* 'attic)
             (eq subject 'chain)
             (eq object 'bucket)
             (have 'chain)
             (have 'bucket)
             (not *chain-welded*))
       (progn (setf *chain-welded* t)
               '(the chain is now securely welded to the bucket.))
     '(you cannot weld like that.)))

First, we need an easy way of checking whether the player is currently carrying an object, using the have function . Remember that we created a command for checking what the player is carrying, named inventory. If an object is a member of the inventory, it means the player must “have” that object.

Next, our program needs some way of keeping track of whether or not the chain and bucket are welded together, since there will be actions later in the game that are possible only once this welding has happened. For this purpose, we create a global, dynamic variable named *chain-welded* .

Finally, we need to create the welding command itself . Welding is possible only if a slew of conditions are met :

  • You must be in the attic.

  • You must have chain and bucket as the subject and object of the welding command.

  • You must be carrying the chain and bucket with you.

  • The chain and bucket can’t already be welded together.

If these conditions are met, we set our *chain-welded* variable to true and print a message indicating this success. If any of the conditions fail, we indicate that the welding was unsuccessful .

Let’s try the command in the CLISP REPL:

> (weld 'chain 'bucket)
(YOU CANNOT WELD LIKE THAT.)

Well, that’s exactly the right response. After all, we’re not in the attic, and we aren’t carrying the right objects. So far, so good.

Next, let’s try our new command in our fancy game-repl:

> (game-repl)
weld chain bucket
I do not know that command.
quit

What? Why doesn’t it “know” that command? The answer is simple: Our game-repl has some basic protections against running unauthorized commands. To remedy this, we need to add weld to our list of permitted commands:

> (pushnew 'weld *allowed-commands*)
(WELD LOOK WALK PICKUP INVENTORY)
> (game-repl)
weld chain bucket
You cannot weld like that.

By using the pushnew command, the weld function is added only to the allowed commands if it wasn’t already present in that list. Problem solved!

A Command for Dunking

image with no caption

In the wizard’s garden, there is a well. Let’s create a command that lets the player dunk the bucket in the well to fill it with water:

 (setf *bucket-filled* nil)

 (defun dunk (subject object)
   (if (and (eq *location* 'garden)
             (eq subject 'bucket)
             (eq object 'well)
             (have 'bucket)
             *chain-welded*)
        (progn (setf *bucket-filled* 't)
               '(the bucket is now full of water))
      '(you cannot dunk like that.)))

 (pushnew 'dunk *allowed-commands*)

As with our weld command, we first need a variable to keep track of whether the bucket has been filled yet . Next, we need a dunk function . Notice how, with dunking, we once again have a long list of conditions that need to be met before we can successfully complete the action . Some of these are similar to those we needed for our welding command. For instance, dunking also requires the player to be in a specific location with the correct object. Other conditions are dunking-specific, such as the fact that the player needs to have a welded chain before being able to dunk. Finally, we need to push the dunk function onto our list of allowed actions .

image with no caption

The game-action Macro

Now that we’ve created two custom game actions for our game, it’s obvious that the weld and dunk commands are very similar in some ways. However, as in our SVG library, each game command needs to contain a certain amount of dynamic logic in it, to customize the behavior of the command. Let’s write a game-action macro that addresses these issues. It will make it much easier to create new game commands.

 (defmacro game-action (command subj obj place &body body)
   `(progn (defun ,command (subject object)
             (if (and (eq *location* ',place)
                       (eq subject ',subj)
                       (eq object ',obj)
                      (have ',subj))
                 ,@body
             '(i cant ,command like that.)))
           (pushnew ',command *allowed-commands*)))

This game-action macro embodies the common pattern between our dunk and weld commands. The parameters to game-action are the name of the command, the two objects involved in the action, the place it needs to occur, and some arbitrary additional code in the body parameter that lets us add custom logic to the command .

The main job of the game-action macro is to define a new function for a command . It may be surprising to you that a macro can do something as powerful as define a new function on its own, but there is nothing to stop it from doing this. I hope this example shows you just how flexible and mind-bending the Common Lisp macro system can be.

Since all game actions for this game require the location, subject, and object, we can take care of some of the conditions directly within this macro . However, we’re going to leave other conditions open for each specific command. Notice, for example, that the subject of the game sentence needs to be owned by the player , but the object does not. This makes sense, since there are many actions that can be performed, such as “throw rock dragon,” where the object of the sentence (dragon) does not need to be in the player’s inventory.

Once the basic macro-level conditions have been met, we will defer the rest of the logic to the level of the individual command . If the conditions were not met, we print an error message, customized with the name of the current command . Finally, we pushnew the command into the list of allowed commands for our fancy game-repl .

One thing we do not do in this macro is define or set any global variables. If a game command needs to define a *chain-welded* or *bucket-filled* global variable, it must do this itself. This makes sense, since there is clearly no guarantee that there will be a one-to-one relationship between state variables for our game and particular commands. For instance, some commands may be permitted multiple times, making the state unnecessary. Or an action may depend on multiple state variables. Having this kind of variation in the commands is what makes them unique and fun.

With this macro, we now have a simple DSL for creating new game actions! Essentially, this command gives us our own programming language, specialized for the domain of creating game commands. Let’s rewrite our previous weld and dunk commands using our new game command programming language:

(defparameter *chain-welded* nil)

(game-action weld chain bucket attic
             (if (and (have 'bucket) (not *chain-welded*))
                 (progn (setf *chain-welded* 't)
                        '(the chain is now securely welded to the bucket.))
               '(you do not have a bucket.)))

(setf *bucket-filled* nil)

(game-action dunk bucket well garden
             (if *chain-welded*
                 (progn (setf *bucket-filled* 't)
                        '(the bucket is now full of water))
               '(the water level is too low to reach.)))

As you can see, these commands have become much easier on the eyes. Notice how weld checks for ownership of the bucket, whereas dunk does not need to check for ownership of the well.

To further illustrate the value of using macros to implement our game command DSL, let’s implement a more complicated game command, splash:

(game-action splash bucket wizard living-room
   (cond ((not *bucket-filled*) '(the bucket has nothing in it.))
         ((have 'frog) '(the wizard awakens and sees that you stole his frog.
                         he is so upset he banishes you to the
                         netherworlds- you lose! the end.))
         (t '(the wizard awakens from his slumber and greets you warmly.
              he hands you the magic low-carb donut- you win! the end.))))

For this command, there are three distinct scenarios that might happen:

  • The bucket is empty.

  • Your bucket is full, but you stole the frog. In that case, you lose.

  • Your bucket is full and you didn’t steal the frog. You win!

With our game-action macro, we can support many action commands, each with special idiosyncratic behavior. Still, we are able to avoid unnecessary repetition.

Note

The game-action command exposes the subject and object variables within the body of the macro. This allows game commands to access this information, but it might also cause a name collision if the code that creates the game-action commands also has variables named subject and object. As an exercise, try modifying the game-action macro so that the subject and object variables are replaced by gensym names, as discussed in Chapter 16.

Let's Try the Completed Wizard's Adventure Game!

Here is a sample run through of the Wizard’s Adventure Game that shows off some of the rich functionality we’ve put into this game. Play the game yourself and see if you can win the magic donut!

image with no caption
> (game-repl)
look
You are in the living-room. There is a wizard  snoring loudly on the couch.
There is a door going west from here. There is a ladder going upstairs from
here. You see a whiskey on the floor. You see a bucket on the floor.
pickup bucket
You are now carrying the bucket
pickup whiskey
You are now carrying the whiskey
inventory
Items- whiskey bucket
walk upstairs
You are in the attic. There is a giant welding torch in the corner. There is a
ladder going downstairs from here.
walk east
You cannot go that way.
walk downstairs
You are in the living-room. A wizard is snoring loudly on the couch. There is
a door going west from here. There is a ladder going upstairs from here.
walk west
You are in a beautiful garden. There is a well in front of you. There is a
door going east from here. You see a frog on the floor. You see
a chain on the floor.
dunk bucket well
The water level is too low to reach.
pickup chain
You are now carrying the chain
walk east
You are in the living-room. A wizard is snoring loudly on the couch. There is
a door going west from here. There is a ladder going upstairs
from here.
splash bucket wizard
The bucket has nothing in it.
..................Content has been hidden....................

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