Drawing a Map of Our City

We’re finally ready to draw a map of our new city. We’re using a standard format for our graph data, so writing this function is a breeze:

(defun draw-city ()
  (ugraph->png "city" *congestion-city-nodes* *congestion-city-edges*))

We created the ugraph->png function in the previous chapter, as part of our graph library.

Now call (new-game) from the REPL, and open the city.dot.png picture in your web browser:

image with no caption

Note

Since every city map created by our code is unique, your map will look completely different from the one in this picture.

Finally, we can marvel at the results of our urban planning!

Drawing a City from Partial Knowledge

Of course, it’s awfully boring to hunt something if you already know where it is before the hunt starts. To solve this problem, we want a map of the city that shows only the nodes that we’ve visited so far. To that end, we use a global list called *visited-nodes* that is initially set to the player’s position only, but which we’ll update as we walk around the city visiting other nodes. Using this *visited-nodes* variable, we can calculate a smaller graph that includes only those parts of the city that are known to us.

Known Nodes

First, we can build an alist of just the known nodes:

(defun known-city-nodes ()
   (mapcar (lambda (node)
             (if (member node *visited-nodes*)
                  (let ((n (assoc node *congestion-city-nodes*)))
                   (if (eql node *player-pos*)
                        (append n '(*))
                        n))
                 (list node '?)))
            (remove-duplicates
               (append *visited-nodes*
                       (mapcan (lambda (node)
                                  (mapcar #'car
                                         (cdr (assoc node
                                                     *congestion-city-edges*))))
                                *visited-nodes*)))))

At the bottom of known-city-nodes, we need to figure out which nodes we can “see” based on where we’ve been. We’ll be able to see all visited nodes , but we also want to track all nodes within one node of a visited node . (We will discuss the mapcan function shortly.) We calculate who is “within one” using code similar to the previously discussed within-one function.

Next, we mapcar over this list of relevant nodes, processing each . If the current node is occupied by the player, we mark it with an asterisk . If the node hasn’t been visited yet , we mark it with a question mark .

Known Edges

Now, we need to create an alist stripped of any cop sirens that we haven’t reached yet:

(defun known-city-edges ()
    (mapcar (lambda (node)
              (cons node (mapcar (lambda (x)
                                   (if (member (car x) *visited-nodes*)
                                       x
                                      (list (car x))))
                                 (cdr (assoc node *congestion-city-edges*)))))
            *visited-nodes*))

This function is similar to the known-city-nodes function. The noteworthy line of code is here where we strip the cdr from the edge list for edges so that cops are indicated on the map only if we’ve visited the nodes on both ends of an edge containing cops.

The mapcan Function

The mapcan function we used in known-city-nodes is a variant of mapcar. However, unlike mapcar, mapcan assumes that the values generated by the mapping function are all lists that should be appended together. This is useful when there isn’t a one-to-one relationship between the items in a list and the result you want to generate.

For example, suppose we run a burger shop and sell three types of burgers: the single, the double, and the double cheese. To convert a list of burgers into a list of patties and cheese slices, we could write the following function:

> (defun ingredients (order)
     (mapcan (lambda (burger)
                (case burger
                   (single '(patty))
                   (double '(patty patty))
                   (double-cheese '(patty patty cheese))))
             order))
INGREDIENTS
> (ingredients '(single double-cheese double))
'(PATTY PATTY PATTY CHEESE PATTY PATTY)

Drawing Only the Known Parts of the City

Because we now have functions that can generate the known information about nodes and edges, we can write a function that turns this information into a picture, as follows:

(defun draw-known-city ()
  (ugraph->png "known-city" (known-city-nodes) (known-city-edges)))

Now let’s redefine our new-game function to draw the known city when the game starts:

(defun new-game ()
    (setf *congestion-city-edges* (make-city-edges))
    (setf *congestion-city-nodes* (make-city-nodes *congestion-city-edges*))
    (setf *player-pos* (find-empty-node))
    (setf *visited-nodes* (list *player-pos*))
    (draw-city)
   (draw-known-city))

This function is almost exactly the same as the previous version of new-game, except that we also create a drawing composed only of the known parts of the city .

Now, if we call the new-game function from the REPL, we’ll get a new picture named known-city.dot.png that we can view in our browser. It will look something like this:

image with no caption

Now we’re ready to walk around our map of Congestion City!

Walking Around Town

We’ll need two functions for traveling between the nodes in our city: a regular walk function and one for when we think we’ve found the Wumpus, and we want to charge that location with our final bullet. Since these two functions are very similar, we’ll have both of them delegate the bulk of the work to a common handle-direction function:

(defun walk (pos)
  (handle-direction pos nil))

(defun charge (pos)
  (handle-direction pos t))

The only difference between these two functions is the flag they pass to handle-direction, which is set to either nil or t, depending on the kind of traveling.

The handle-direction function’s main job is to make sure that a move is legal, which it does by checking the edges of the city:

(defun handle-direction (pos charging)
    (let ((edge (assoc pos
                      (cdr (assoc *player-pos* *congestion-city-edges*)))))
      (if edge
         (handle-new-place edge pos charging)
         (princ "That location does not exist!"))))

First, this function looks up the legal directions players can move to from their current location . It then uses the pos the player wants to move to and looks it up in that list of possible directions. Once we’ve determined that a direction is legal (that is, a node with that number shares an edge with the player’s current position), we need to find out what surprises are waiting as the player travels to this new place, using the handle-new-place function, which we’ll create next . Otherwise, we display a helpful error message .

Now let’s create the handle-new-place function, which gets called when the player has traveled to a new place:

(defun handle-new-place (edge pos charging)
   (let* ((node (assoc pos *congestion-city-nodes*))
          (has-worm (and (member 'glow-worm node)
                          (not (member pos *visited-nodes*)))))
     (pushnew pos *visited-nodes*)
     (setf *player-pos* pos)
     (draw-known-city)
     (cond ((member 'cops edge) (princ "You ran into the cops. Game Over."))
           ((member 'wumpus node) (if charging
                                       (princ "You found the Wumpus!")
                                       (princ "You ran into the Wumpus")))
           (charging (princ "You wasted your last bullet. Game Over."))
           (has-worm (let ((new-pos (random-node)))
                        (princ "You ran into a Glow Worm Gang! You're now at ")
                        (princ new-pos)
                        (handle-new-place nil new-pos nil))))))

First, we retrieve the node the player is traveling to from the alist of nodes . Next, we figure out if the node contains a Glowworm gang . We ignore the gang if they’re in a node already visited, because they’ll only attack once.

Next, the handle-new-place function updates *visited-nodes* (adding the new position to the list) and *player-pos* . Then it calls draw-known-city again, since we now have a new place we know about.

Next, it checks to see if there are any cops on the edge , and then whether the Wumpus is at that location . If the player encounters the Wumpus, our handle-new-place function needs to know whether we were charging the location. If we are charging at the Wumpus, we win the game. Otherwise, the Wumpus kills us and the game is over.

If, on the other hand, we charge at a location that does not contain the Wumpus, we waste our single bullet and we lose the game as well . Finally, if the location has a previously unencountered Glowworm gang, jump to a random new location, calling handle-new-place recursively .

Our game is now complete!

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

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