<< first < prev 1 2 3 4 5 6 7 8 9 next > last >> Looking Around...
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:

(defn describe-location [location game-map]
  (first (location game-map)))

The word defn 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 game map. Since these variables appear in the function's parameters, it means they are local and hence unrelated to the global location and game-map variables we defined earlier. Note that functions in Lisp are often more like functions in math than in other programming languages: Just like in math, this function does not 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...).

Little Wizard Clojure expects the parameter lists in square brackets. Most other Lisps expect parentheses here.

 

Living Room

To find the description for this, our describe-location first needs to look up the spot in the map that points to the living-room. (location game-map) performs the lookup on the game-map hash map and then returns the data describing the living-room. Then the command first trims out the first item in that list, which is the description of the living-room (If you look at the game-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 color)

in the tutorial, paste the following text into your Lisp prompt:

(describe-location 'living-room game-map)
 user=> (describe-location 'living-room game-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 game-map, since in this case we want the list compiler to hunt down the data stored in the game-map variable (i.e. we want the compiler to be in Code Mode and not just look at the word game-map as a chunk of raw data)

The Functional Programming Style

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 does not tell us about the paths in and out of the location to other locations. Let's write a function that describes these paths:

(defn 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))
user=> (describe-path '(west door garden))
(user/there user/is user/a door user/going west user/from user/here clojure.core/-)

What is that !? The result is now cluttered with strange '/' characters and extra words ! This is because Clojure adds namespace information in to expressions that begin with a backquote. We won't go into detail here, but instead provide you with a way to remove that confusing output:

(defn spel-print [list] (map (fn [x] (symbol (name x))) list))
and enter
(spel-print (describe-path '(west door garden)))
user=> (spel-print (describe-path '(west door garden)))
(there is a door going west from here -)

Little Wizard Clojure namespaces are out of the scope of this tutorial. They are an important concept to Clojure, so we recommend to read about them in the language documentation.

So now it's clear: This function takes a list describing a path (just like we have inside our game-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 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 tilde ("~") character:

Backquoted Form

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:

(defn describe-paths [location game-map]
  (apply concat (map describe-path (rest (get game-map location)))))

This function uses another common functional programming technique: The use of Higher Order Functions - This means that the apply and map functions are taking other functions as parameters so that they can call them themselves - map 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 concat" just cleans out some parentheses and isn't so important. Let's try this new function:

(spel-print (describe-paths 'living-room game-map))
user=> (spel-print (describe-paths 'living-room game-map))
(there is a door going west from here -
there is a stairway going upstairs from here -)

Beautiful!

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:

(defn is-at? [obj loc obj-loc] (= (obj obj-loc) loc))

...the = function tells us if the symbol from the object location list is the same as the current location.

Slob

Let's try this out:

(is-at? 'whiskey-bottle 'living-room object-locations)
user=> (is-at? 'whiskey-bottle 'living-room object-locations)
true

The symbol true (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:

(defn describe-floor [loc objs obj-loc]
  (apply concat (map (fn [x]
                       `(you see a ~x on the floor -))
                     (filter (fn [x] 
                               (is-at? x loc obj-loc)) objs))))

This function has a couple of new things: First of all, it has anonymous functions (fn is the command for creating such a function.) That first fn form is just the same as defining a helper function:

(defn blabla [x] `(you see a ~x on the floor.))

and then sending blabla to the map function. The filter function in this case is filtering out any objects from the list that are not at the current location before passing the list on to map to build pretty sentences. Let's try this new function:

(spel-print (describe-floor 'living-room objects object-locations))
user=> (spel-print (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:

(defn look []
  (spel-print (concat (describe-location location game-map)
          (describe-paths location game-map)
          (describe-floor location objects object-locations))))
Functional

Let's try it:

user=> (look)
(you are in the living room of a wizards 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 -)
        

pretty cool!


<< first < prev 1 2 3 4 5 6 7 8 9 next > last >> Looking Around...