Looking Around in our Game World
The first command we'd want to have is a command that tells us about the location we're standing in. So what would a function need to describe a location in a world? Well, it would need to know the location we want to describe and would need to be able to look at a map and find that location on the map. Here's our function, and it does exactly that:
(defun describe-location (location map) (second (assoc location map)))
The word defun means, as you'd expect, that we're defining a function. The name of the function is describe-location and it takes two parameters: a location and a map. Since these variables do not have stars around them, it means they are local and hence unrelated to the global *location* and *map* variables. Note that functions in Lisp are often more like functions in math than in other programming languages: Just like in math, this function doesn't print stuff for the user to read or pop up a message box: All it does is return a value as a result of the function that contains the description. Let's imagine our location is in the living-room (which, indeed, it is...).|
To find the description for this, it first needs to look up the spot in the map that points to the living-room. The assoc command does this and then returns the data describing the living-room. Then the command second trims out the second item in that list, which is the description of the living-room (If you look at the *map* variable we had created, the snippet of text describing the living-room was the second item in the list that contained all the data about the living room...)
Now let's use our Lisp prompt to test our function- Again, like all the text in
this font and colorin the tutorial, paste the following text into your Lisp prompt:
(describe-location 'living-room *map*)
==> (YOU ARE IN THE LIVING-ROOM OF A WIZARD'S HOUSE. THERE IS A WIZARD SNORING LOUDLY ON THE COUCH.)
Perfect! Just what we wanted... Notice how we put a quote in front of the symbol living-room, since this symbol is just a piece of data naming the location (i.e. we want it read in Data Mode) , but how we didn't put a quote in front of the symbol *map*, since in this case we want the list compiler to hunt down the data stored in the *map* variable (i.e. we want the compiler to be in Code Mode and not just look at the word *map* as a chunk of raw data)|
You may have noticed that our describe-location function seems pretty awkward in several different ways. First of all, why are we passing in the variables for location and map as parameters, instead of just reading our global variables directly? The reason is that Lispers often like to write code in the Functional Programming Style (To be clear, this is completely unrelated in any way to the concept called "procedural programming" or "structural programming" that you might have learned about in high school...). In this style, the goal is to write functions that always follow the following rules:
1. You only read variables that are passed into the function or are created by the function (So you don't read any global variables)
2. You never change the value of a variable that has already been set (So no incrementing variables or other such foolishness)
3. You never interact with the outside world, besides returning a result value. (So no writing to files, no writing messages for the user)
You may be wondering if you can actually write any code like this that actually does anything useful, given these brutal restrictions... the answer is yes, once you get used to the style... Why would anyone bother following these rules? One very important reason: Writing code in this style gives your program referential transparency: This means that a given piece of code, called with the same parameters, always positively returns the same result and does exactly the same thing no matter when you call it- This can reduce programming errors and is believed to improve programmer productivity in many cases.
Of course, you'll always have some functions that are not functional in style or you couldn't communicate with the user or other parts of the outside world. Most of the functions later in this tutorial do not follow these rules.
Another problem with our describe-location function is that it doesn't tells us about the paths in and out of the location to other locations. Let's write a function that describes these paths:
(defun describe-path (path) `(there is a ,(second path) going ,(first path) from here.))
Ok, now this function looks pretty strange: It almost looks more like a piece of data than a function. Let's try it out first and figures out how it does what it does later:
(describe-path '(west door garden))
==> (THERE IS A DOOR GOING WEST FROM HERE.)
So now it's clear: This function takes a list describing a path (just like we have inside our *map* variable) and makes a nice sentence out of it. Now when we look at the function again, we can see that the function "looks" a lot like the data it produces: It basically just splices the first and second item from the path into a declared sentence. How does it do this? It uses back-quoting!|
Remember that we've used a quote before to flip the compiler from Code Mode to Data Mode- Well, by using the the back-quote (the quote in the upper left corner of the keyboard) we can not only flip, but then also flop back into Code Mode by using a comma:
This "back-quoting" technique is a great feature in Lisp- it lets us write code that looks just like the data it creates. This happens frequently with code written in a functional style: By building functions that look like the data they create, we can make our code easier to understand and also build for longevity: As long as the data doesn't change, the functions will probably not need to be refactored or otherwise changed, since they mirror the data so closely. Imagine how you'd write a function like this in VB or C: You would probably chop the path into pieces, then append the text snippets and the pieces together again- A more haphazard process that "looks" totally different from the data that is created and probably less likely to have longevity.
Now we can describe a path, but a location in our game may have more than one path, so let's create a function called describe-paths:
(defun describe-paths (location map) (apply #'append (mapcar #'describe-path (cddr (assoc location map)))))
This function uses another common functional programming technique: The use of Higher Order Functions- This means that the apply and mapcar functions are taking other functions as parameters so that they can call them themselves- To pass a function, you need to put #' in front of the function name... The cddr command chops the first two item from the front of the list (so only the path data remains). mapcar simply applies another function to every object in the list, basically causing all paths to be changed into pretty descriptions by the describe-path function. The "apply #'append" just cleans out some parenthesis and isn't so important. Let's try this new function:|
(describe-paths 'living-room *map*)
==> (THERE IS A DOOR GOING WEST FROM HERE. THERE IS A STAIRWAY GOING UPSTAIRS FROM HERE.)
We still have one thing we need to describe: If there are any objects on the floor at the location we are standing in, we'll want to describe them as well. Let's first write a helper function that tells us wether an item is in a given place:
(defun is-at (obj loc obj-loc) (eq (second (assoc obj obj-loc)) loc))
...the eq function tells us if the symbol from the object location list is the current location.|
Let's try this out:
(is-at 'whiskey-bottle 'living-room *object-locations*)
The symbol t (or any value other than nil) means that it's true that the whiskey-bottle is in living-room.|
Now let's use this function to describe the floor:
This function has a couple of new things: First of all, it has anonymous functions (lambda is just a fancy word for this). That first lambda form is just the same as defining a helper function (defun blabla (x) `(you see a ,x on the floor.)) and then sending #'blabla to the mapcar function. The remove-if-not function removes any objects from the list that are not at the current location before passing the list on to mapcar to build pretty sentences. Let's try this new function:
(describe-floor 'living-room *objects* *object-locations*)
==> (YOU SEE A WHISKEY-BOTTLE ON THE FLOOR. YOU SEE A BUCKET ON THE FLOOR)
Now we can tie all these descriptor functions into a single, easy command called LOOK that uses the global variables (therefore this function is not in the Functional Style) to feed all the descriptor functions and describes everything:|
Let's try it:
==> (YOU ARE IN THE LIVING-ROOM OF A WIZARD'S HOUSE.
THERE IS A WIZARD SNORING LOUDLY ON THE COUCH.
THERE IS A DOOR GOING WEST FROM HERE.
THERE IS A STAIRWAY GOING UPSTAIRS FROM HERE.
YOU SEE A WHISKEY-BOTTLE ON THE FLOOR.
YOU SEE A BUCKET ON THE FLOOR)