Adding a Custom Interface to Our Game Engine

So far, we’ve been using the Lisp REPL to enter our game commands. It’s amazing how well this works for prototyping our game. But now that you’ve gained an understanding of the basic Common Lisp input and output commands, we can begin to put in place our own custom text game interface, which will be better suited for interacting with the player.

Setting Up a Custom REPL

Creating your own REPL in Lisp is almost laughably easy. Here’s a simple custom REPL for our game, which lets us call the look command in exactly the same way as the standard REPL did:

> (defun game-repl ()
     (loop (print (eval (read)))))
GAME-REPL
> (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.)

Stop me if this explanation of game-repl is confusing: First it reads a command, then evals it, and finally prints it. The only command you haven’t seen before is loop (covered in detail in Chapter 10), which as you might expect, simply loops forever. (In CLISP, you’ll need to hit ctrl-C and type :a to get out of the infinite loop.) As you can see, it’s easy to build your own REPL by simply calling read, eval, print, and loop.

image with no caption

Of course, to customize the behavior of our REPL, we’ll want to call our own versions of these functions. Also, we’ll want a way to exit from our game in a more graceful manner. So, let’s redefine game-repl as follows:

(defun game-repl ()
    (let ((cmd (game-read)))
        (unless (eq (car cmd) 'quit)
            (game-print (game-eval cmd))
            (game-repl))))

In this version, we first capture the command the player types using a local variable, cmd . This way, we can intercept any attempt to call quit and use it to exit our game-repl. In other words, we want to continue running our REPL unless the user typed quit . Otherwise, the function evals and prints , but using our custom versions of these functions, which we’ll write shortly. Finally, the game-repl function calls itself recursively , causing it to loop back, as long as we had not decided to quit earlier.

Writing a Custom read Function

The purpose of our game-read function is to fix the two annoyances that make the standard Lisp read function wrong for playing our game:

  • The standard Lisp read forces us to put parentheses around our commands. As any old-school text adventure player knows, we should be able to just type look without any parentheses. To accomplish this, we can just call read-line and stick in our own parentheses.

  • With read, we must put a quote in front of any function commands. We should be able to type walk east without a quote in front of east. To do this, we’ll stick a quote in front of the parameters after the fact.

image with no caption

Here’s a definition of game-read that does both of these things:

(defun game-read ()
    (let ((cmd (read-from-string
                     (concatenate 'string "(" (read-line) ")"))))
         (flet ((quote-it (x)
                         (list 'quote x)))
             (cons (car cmd) (mapcar #'quote-it (cdr cmd))))))

The read-from-string command works just like the read command, but lets us read a syntax expression (or any other basic Lisp datatype) from a string instead of directly from the console.

The string we use for this is a tweaked version of a string we get from read-line . We tweak it by adding quotes around it using the concatenate command, which can be used for concatenating strings together, as well as some parentheses. The result is that the cmd variable will be set to the player’s requested command and converted into a Lisp syntax expression. For example, if the player types in walk east, the cmd variable will be set to the expression (walk east), which is a list containing two symbols.

Next, we define a local function called quote-it , which we can use to quote any arguments the player has in a command. How exactly does it manage to quote a parameter? Well, it turns out that the single quote is just shorthand for a Lisp command called quote. This means that 'foo and (quote foo) are the same. We can quote a raw parameter by simply putting the parameter in a list with the quote command in front.

Remember that local functions can be defined with labels or flet. Since we are not using any recursion in the quote-it function , we can use the simpler flet command. The final line in the game-read function applies quote-it to every argument in the player’s command. It does this by mapping quote-it across the cdr of the cmd variable (and then attaching the first word in the command back on front with car).

Let’s try our new function:

> (game-read)
walk east
(WALK 'EAST)

As you can see, the game-read function is able to add parentheses and quotes—just what our game needs!

Note

Our custom reader has some limitations that a sufficiently boneheaded game player could conceivably bring to the surface. The player could enter a weird string like "(look", with mismatched parentheses, and it would cause a Lisp exception in the game-read command. There’s nothing wrong with this, per se, since the standard read command will also act strangely when given garbled input. (In this case, it will let you enter another line of input in the hopes that you will eventually supply it with the missing parenthesis.) However, our game-repl doesn’t handle this situation properly, causing the actual game-repl to crash. This would be as if you were playing Zork and typed in a command so vile that it took down the Zork game itself. This rare situation could be addressed by having additional exception handling, as discussed in Chapter 13.

Writing a game-eval Function

Now that we’ve created a nigh-perfect Lisp reader, let’s think about how we could improve the eval command. The main problem with using eval in a game is it allows you to call any Lisp command, even if that command has nothing to do with playing the game. To help protect our program from hackers, we’ll create a game-eval function that allows only certain commands to be called, as follows:

(defparameter *allowed-commands* '(look walk pickup inventory))

  (defun game-eval (sexp)
      (if (member (car sexp) *allowed-commands*)
          (eval sexp)
          '(i do not know that command.)))

The game-eval function checks if the first word in the entered command is in the list of allowed commands, using the member function . If it is, we then use the standard eval to execute the player’s command . By checking that the command called by the player is in the official list, we protect ourselves against any attempts to call malicious commands.

image with no caption

Warning

Our game-eval function does not offer 100 percent protection against hacking. See The Dangers of read and eval in The Dangers of read and eval for details.

Writing a game-print Function

The final missing piece in our game-repl system is the game-print function. Of all the limitations in the Lisp REPL version of our game, one was the most obvious: All the text descriptions printed in the game were in uppercase.

Last I checked, throughout the current millennium, computers have been able to display both uppercase and lowercase characters. By writing our own game-print function, we can solve this problem.

Before we step through the game-print function’s code, let’s look at an example of its output:

> (game-print '(THIS IS A SENTENCE. WHAT ABOUT THIS? PROBABLY.))
This is a sentence. What about this? Probably.

As you can see, the game-print function converts our symbol-based writing into properly capitalized text. By having this function available, we can store the text in our game engine in the most comfortable format possible: lists of symbols. This format makes it easier to manipulate the text. Then, at the point of presentation, we can decorate these symbol lists with presentation details.

Of course, in this example, the decorations are very simple. All we do is adjust the case. But you can already see some small benefits of separating the presentation details from the data model. For instance, suppose we changed the describe-path function to write sentences like “Left of here lies a door.” No further changes would be needed; the program would automatically know to capitalize the Left at the beginning of the sentence.

However, the real benefits come into play when you want to use more sophisticated methods of presentation, such as generating HTML code. You might want to incorporate custom semantics for your text game to enhance the appearance of the text, such as changing colors, fonts, and so on. For instance, you could allow your game descriptions to contain phrases such as “You are being attacked by a (red evil demon).” Then you could just catch the keyword red in the game-print function to write the enclosed text in red. We will be creating an HTML presentation system similar to this in Chapter 17.

image with no caption

Now we’re ready to look at the game-print function’s code:

(defun tweak-text (lst caps lit)
   (when lst
  (let ((item (car lst))
         (rest (cdr lst)))
   (cond ((eq item #space) (cons item (tweak-text rest caps lit)))
         ((member item '(#! #? #.)) (cons item (tweak-text rest t lit)))
         ((eq item #") (tweak-text rest caps (not lit)))
           (lit (cons item (tweak-text rest nil lit)))
         ((or caps lit) (cons (char-upcase item) (tweak-text rest nil lit)))
         (t (cons (char-downcase item) (tweak-text rest nil nil)))))))

  (defun game-print (lst)
   (princ (coerce (tweak-text (coerce (string-trim "() "
                                                    (prin1-to-string lst))
                                       'list)
                               t
                              nil)
                   'string))
    (fresh-line))

The game-print function and its helper function are a bit more complicated than the other functions we’ve looked at so far. The first important part of the code that is executed is in game-print, where it converts the symbol list (containing the text whose layout we want to fix) into a string with prin1-to-string , one of Lisp’s many print variants. The to-string part means this function doesn’t dump the result to the screen, but just returns it as a string. The 1 means it will stay on a single line. The standard print command precedes its output with a newline character and also follows it with a space. The functions prin1 and prin1-to-string variants don’t add these extra characters.

Next, game-print converts the string to a list of characters with the coerce function . By coercing our string into a list, we can reduce the bigger goal of the function into a list-processing problem. This is smack-dab in the Lisp comfort zone. In this case, we’re creating a list of the characters making up the text we want to fix.

We can now send the data to the list-eater function tweak-text . Notice that some of the arguments used in the code of the game-print function are printed on their own line for clarity. You can easily see which arguments are meant for which commands by looking at the indentation. For instance, the t and nil arguments belong to tweak-text.

The tweak-text function looks at each character in the list and modifies it as needed. At the top of this function, we define two local variables, item and rest, which we get by chewing off an item from the front of the sentence we’re tweaking . Then, the tweak-text function uses a cond to check the character at the top of the list for different conditions .

The first condition it checks for is whether the character is a space character . If so, it just leaves the space unchanged and processes the next character in the list. If the character is a period, question mark, or exclamation point , we turn on the cap parameter for the rest of the string (by using the value t as an argument in the recursive call) to indicate that the next symbol is at the beginning of a sentence and needs a capital letter.

We also track whether we’ve encountered a quotation mark . We do this because, infrequently, a symbol list is not adequate for encoding English text. Examples include having a comma (commas are not allowed in standard Common Lisp symbols) or product names with nonstandard capitalization. In these cases, we can just fall back on using text strings. Here’s an example:

> (game-print '(not only does this sentence
 have a "comma," it also mentions the "iPad."))
Not only does this sentence have a comma, it also mentions the iPad.

Our sample game doesn’t actually need the fallback facility. Nonetheless, this feature allows the game-print function to handle many basic exceptional text situations that you may encounter if you try to expand the game on your own. We tell the function to treat the capitalization as shown literally by turning on the lit variable in the recursive call. As long as this value is set, the tweak-text function prevents the capitalization rules (which start at ) from being reached.

The next thing the tweak-text function checks is whether the next character is supposed to be capitalized. If it is, we use the char-upcase function to change the current character to uppercase (if it isn’t already) before processing the next item in the list .

If none of the other conditions were met, we know that the current character should be lowercase , and we can convert it using the char-downcase function.

After tweak-text is finished correcting the text in the character list, the game-print function coerces it back into a proper string and princs it . The fresh-line function at the end of game-print makes sure that the next item appearing on the screen will start on a fresh line.

We have now completed the task of printing the original list of symbols to the screen, using a set of decorations appropriate for the needs of an adventure game engine.

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

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