Thursday, June 28, 2012

Land of Lisp chapter 6

Whoops. Naughty me, I did the 6.5 post before the chapter 6 post. Well, 6.5 was more interesting and easier to write at the same time. So it got done first. Onward; pretend all the lambda navel-gazing hasn't happened yet.

Now this adventure game is pretty ugly, and very user unfriendly. Time to clean it up a bit. That's going to require some better I/O functions.

The basic screen printing functions are:
print: Prints one output per line, strings in quotes. Perfectly machine readable later.
print1: Dispenses with the newline
princ: Human readable output.
For reading input:
read: Just the basic Lisp input, parens and quotes required.
read-line: Reads until the user hits enter and takes the whole input verbatim as a string.
read-from-string: Just like read except it takes a string input instead of reading from the console.

With that it's time to learn one of the more powerful functions. eval takes input and parses it as code. So

executes the form with the (princ "bar") output that you'd expect. This is quite powerful but also dangerous. And it's dangerous in ways that are not necessarily immediately obvious. I'll get to that.

At this point we have all the capabilities to build our very own REPL. Rather like opening a DOS shell within a DOS shell. You might compare it to running Windows in a VM except that the VM handles a lot of background operations and that would be more akin to the next step in this chapter. To the point however, here's the dead simple code for a basic, nofrills, 'custom' REPL (custom in quotes because it's about as bog standard as it gets even though it's a REPL in a REPL).

That's all well and good but when we execute (my-repl) nothing really interesting happens. Visibly at least. Because, of course, the functions we've used have no functionality beyond the basic REPL. We haven't written any. So that's clearly the next step. Now is a good time to note that I'm storing the code for this custom REPL in a separate file because it becomes essentially a general use engine for running simple text adventures. The file will be linked at the bottom along with loading instructions (such as they are).

I'd also like to note in advance that I think the way the game-repl is implemented is a bad paradigm. It basically uses recursion to perform the 'loop' structure. I added a printout to the book's code to make this obvious; basically when you 'quit' you'll get a playback of all the commands entered. Now while this implementation is tiny compared to modern memory sizes it's important to note that recursion has some overhead (which is also made obvious by the printout). It literally does not matter here due to the scales involved, I want to stress that, but it's important to note the point. Maybe this small structural complaint is fixed later in the book, I dunno. But on with it.

First we want to be able to take input. So we build game-read.

Take a moment to look at the flet function quote-it. What does (list 'quote x) do? A heretofore undescribed complexity is that we've been using a shorthand when quoting. 'foo is the same as using the quote function as in (quote foo). So what quote-it does is create a list that is a form of the quote function. It literally quotes its input and returns it. It's important to note that this case doesn't require #'quote because it isn't being passed as a parameter to a higher order function; the full special form is created using list so it will get evaluated normally. So to describe the game-read function fully, first we read a raw line of input, then we create a string (because we requested it by specifying 'string) by concatenating parenthesis around the input. Then we read that string into the cmd variable. Then we quote the cdr of the entered command (if it's more than 1 list item) using mapcar and quote-it, cons the thing back together, and return that result.

Next, we need to evaluate what we just read.


Two things to note. First, I added the case expression. It's completely missing in the book. But I'm afraid 'pickup' is just awkward to me and since get is its own special form it needed to be handled specially by the evaluation function. So I select it with case and convert it to a 'pickup' verb instead. Additional verbs can be added to the case form as needed, though if there are many then a flet might be in order . Second the *allowed-commands* global I'm placing in the game data file. It's specific to the game so doesn't need to be in the custom REPL code. It's defined as (again, I added 'get' myself to get it past the member filter):

So this is pretty straightforward. The only complexity is what I added. An if test is performed to determine if the input verb (the car of the input expression) is allowed and it's either evaled or an error is generated.

Finally there's the output. This is the most complicated. As usual.

Some of the lines probably break in the wrong places which makes it boogers to read. I really need to find a decent scroll block to plug in here, but I'm lazy and have other things to do. Look it up in the code file if it bothers you. (Edit: Should look much better now that it's in gist!)

Basically what happens here is the input list to game-print is printed to a string value by prin1-to-string which works the same as prin1 but returns the string instead of printing it to screen. That string has parenthesis and spaces hacked off the ends by the obvious string-trim function and the output from that is coerced to a list of characters. Then tweak-text is called which handles capitalization (there's nothing new in the function so I'll leave figuring out the logic as an exercise... oh wait, char-upcase and char-downcase... surely those are completely obvious...) and the resultant list of character values is then coerced to a string, princed and a fresh-line is output. Phew.

Now, to plug this all together (with my aforementioned rudimentary stack trace):

So, if you input 'quit' the recursion ends and all the input cmds are re-output.. Dead simple.

As the last word, a caveat about eval. We've pretty thoroughly scrubbed the user input right? We only accept a few verbs past the filters in front of eval, only one of those is a special form command, and that one is re-written as a different verb with no special meanings. There's still a problem though. As with anything that one doesn't fully understand there's a backdoor, apparently, in the read functionality. The book doesn't go into detail but there are apparently read macros that allow all sorts of shenanigans. Like perhaps

I don't know if there's a way to disable those or not. I'm hoping the book will cover them in more detail shortly.

The files:
gameREPL.lisp
TheWizardAdventureGame.lisp

You'll want to do the following:

to get things running (using the correct paths of course).

No comments: