Defining the Edges of Congestion City

The map of Congestion City will be an undirected graph with data associated with each node stored in the variable *congestion-city-nodes*. The possible data at each node will include the presence of the Wumpus, a Glowworm team, and various danger signs.

A set of edges stored in *congestion-city-edges* will connect the nodes, and data linked to these edges will alert us to the presence of any police roadblocks. We declare these and other global variables at the top of our program using defparameter:

 (load "graph-util")

  (defparameter *congestion-city-nodes* nil)
  (defparameter *congestion-city-edges* nil)
  (defparameter *visited-nodes* nil)
 (defparameter *node-num* 30)
 (defparameter *edge-num* 45)
 (defparameter *worm-num* 3)
 (defparameter *cop-odds* 15)

We first load our graph utilities with the load command , which evaluates all the code in graph-util.lisp (which we created in the previous chapter) so the graph utility functions will be available. Notice that Congestion City will have 30 locations (nodes, defined with *node-num*), 45 edges (roads, defined with *edge-num*), and 3 worm teams (defined with *worm-num*). Each street will have a 1-in-15 chance of containing a roadblock (defined with *cop-odds*).

Generating Random Edges

Next, we create a random list of edges to connect all the nodes:

 (defun random-node ()
    (1+ (random *node-num*)))

 (defun edge-pair (a b)
    (unless (eql a b)
      (list (cons a b) (cons b a))))

 (defun make-edge-list ()
   (apply #'append (loop repeat *edge-num*
                         collect (edge-pair (random-node) (random-node)))))

First, we declare the random-node function , which returns a random node identifier. It uses the random function, which returns a random natural number less than the integer you pass to it. Since we’re going to be showing the node identifiers in our user interface, we use the 1+ function to number our nodes 1 through 30 (the upper limit because the *node-num* variable is set to 30), instead of 0 through 29.

The make-edge-list function generates the actual list of random edges. It uses the loop command to loop *edge-num* times , and then collects the requisite number of edges . We’ll take a closer look at the loop command in the next section. The graph of the city is undirected, so this function uses a helper function, edge-pair , to create two directed edges between the randomly selected nodes. This extra step makes sense once you remember that an undirected graph is the same as a directed graph, with two opposing directed edges mirroring each undirected edge. (When we build our edges into an alist later in this chapter, this step will ensure that the list is properly formed.)

image with no caption

Let’s try the make-edge-list function in the CLISP REPL:

> (make-edge-list)
((16 . 20) (20 . 16) (9 . 3) (3 . 9) (25 . 18) (18 . 25) (30 . 29)
 (29 . 30) (26 . 13) (13 . 26) (12 . 25) (25 . 12) (26 . 22) (22 . 26)
 (30 . 29) (29 . 30) (3 . 14) (14 . 3) (28 . 6) (6 . 28) (4 . 8) (8 . 4)
 (27 . 8) (8 . 27) (3 . 30) (30 . 3) (25 . 16) (16 . 25) (5 . 21) (21 . 5)
 (11 . 24) (24 . 11) (14 . 1) (1 . 14) (25 . 11) (11 . 25) (21 . 9) (9 . 21)
 (12 . 22) (22 . 12) (21 . 11) (11 . 21) (11 . 17) (17 . 11) (30 . 21) (21 . 30)
 (3 . 11) (11 . 3) (24 . 23) (23 . 24) (1 . 24) (24 . 1) (21 . 19) (19 . 21) (25 . 29)
 (29 . 25) (1 . 26) (26 . 1) (28 . 24) (24 . 28) (20 . 15) (15 . 20)
 (28 . 25) (25 . 28)
 (2 . 11) (11 . 2) (11 . 24) (24 . 11) (29 . 24) (24 . 29)
(18 . 28) (28 . 18) (14 . 15)
 (15 . 14) (16 . 10) (10 . 16) (3 . 26) (26 . 3) (18 . 9) (9 . 18) (5 . 12)
 (12 . 5) (11 . 18) (18 . 11) (20 . 17) (17 . 20) (25 . 3) (3 . 25))

You see the pairs of node numbers that make up the edges. This list of edge pairs will form the skeleton of the Congestion City road system.

Looping with the loop Command

Our make-edge-list function employs the powerful loop command, which can be used to loop over various types of data. We’ll be looking at loop in detail in Chapter 10. However, our game uses loop a few times, so let’s consider some simple examples to clarify how it works.

One handy thing you can do with loop is create a list of numbers. For instance, the following command will create a list of 10 ones:

> (loop repeat 10
        collect 1)
(1 1 1 1 1 1 1 1 1 1)

Within the loop command, we specify how many times to repeat, and then specify an object to collect with every loop (in this case, the number 1).

Sometimes, we want to keep a running count as we’re looping. We can do this with the following syntax:

> (loop for n from 1 to 10
        collect n)
(1 2 3 4 5 6 7 8 9 10)

In this example, we are saying that n should loop from 1 to 10. Then we collect each n and return it as a list.

Actually, we can put any Lisp code in the collect part of the loop. In the following example, we add 100 as we do our collecting:

> (loop for n from 1 to 10
        collect (+ 100 n))
(101 102 103 104 105 106 107 108 109 110)

Preventing Islands

We now can generate random edges. Of course, if we just connect random nodes with random edges, there’s no guarantee that all of Congestion City will be connected because of all that randomness. For example, some parts of the city might form an island, with no connections to the main road system.

image with no caption

To prevent this, we’ll take our list of edges, find unconnected nodes, and connect these islands to the rest of the city network using this code:

 (defun direct-edges (node edge-list)
   (remove-if-not (lambda (x)
                     (eql (car x) node))
                   edge-list))

 (defun get-connected (node edge-list)
   (let ((visited nil))
      (labels ((traverse (node)
                 (unless (member node visited)
                  (push node visited)
                  (mapc (lambda (edge)
                           (traverse (cdr edge)))
                         (direct-edges node edge-list)))))
        (traverse node))
      visited))

  (defun find-islands (nodes edge-list)
    (let ((islands nil))
     (labels ((find-island (nodes)
                 (let* ((connected (get-connected (car nodes) edge-list))
                        (unconnected (set-difference nodes connected)))
                   (push connected islands)
                  (when unconnected
                     (find-island unconnected)))))
        (find-island nodes))
      islands))

  (defun connect-with-bridges (islands)
   (when (cdr islands)
      (append (edge-pair (caar islands) (caadr islands))
              (connect-with-bridges (cdr islands)))))

 (defun connect-all-islands (nodes edge-list)
    (append (connect-with-bridges (find-islands nodes edge-list)) edge-list))

First, we declare a utility function called direct-edges , which finds all the edges in an edge list that start from a given node. It does this by creating a new list with all edges removed (using remove-if-not ) that don’t have the current node in the car position.

To find islands, we write the get-connected function . This function takes an edge list and a source node and builds a list of all nodes connected to that node, even if it requires walking across multiple edges.

The usual way to find connected nodes is to start a visited list , and then perform a search along connected nodes, starting with the source node. Newly found nodes are added to the visited list with the push command . We also traverse all the children of this found node, using mapc .

If, on the other hand, we encounter a node that has already been visited, we know we can ignore it. Once the search is complete, the visited list will consist of all connected nodes.

Now that we have a function for finding nodes that are connected, we can use it to create a function that will find all the islands in our graph. The find-islands function first defines a local function, called find-island . This function checks which nodes are connected to the first node in our list of nodes using the connected function. It then subtracts these nodes from the full list of nodes using the set-difference function. (set-difference takes two lists, and returns all items that are in the first list but not the second.)

Any remaining nodes are deemed unconnected. If any unconnected node exists , we call the find-islands function again recursively to find additional islands.

Once we’ve found all the islands, we need a way of bridging them together. This is the job of the connect-with-bridges function. It returns a list of additional edges that join all the islands together. To do this, it takes the list of islands and checks if there is a cdr in this list . If there is, it means there are at least two land masses, which can be connected with a bridge. It uses the edge-pair function to create this bridge, and then calls itself recursively on the tail of the island list, in case additional bridges are needed.

Finally, we tie all of our island prevention functions together using the function connect-all-islands . It uses find-islands to find all the land masses, and then calls connect-with-bridges to build appropriate bridges. It then appends these bridges to the initial list of edges to produce a final, fully connected land mass.

Building the Final Edges for Congestion City

To complete our edges for Congestion City, we need to convert the edges from an edge list into an alist. We also will add the police roadblocks, which will appear randomly on some of the edges. For these tasks, we will create the make-city-edges, edges-to-alist, and add-cops functions:

(defun make-city-edges ()
   (let* ((nodes (loop for i from 1 to *node-num*
                        collect i))
          (edge-list (connect-all-islands nodes (make-edge-list)))
          (cops (remove-if-not (lambda (x)
                                  (zerop (random *cop-odds*)))
                                edge-list)))
     (add-cops (edges-to-alist edge-list) cops)))

  (defun edges-to-alist (edge-list)
   (mapcar (lambda (node1)
              (cons node1
                   (mapcar (lambda (edge)
                              (list (cdr edge)))
                            (remove-duplicates (direct-edges node1 edge-list)
                                              :test #'equal))))
            (remove-duplicates (mapcar #'car edge-list))))

  (defun add-cops (edge-alist edges-with-cops)
   (mapcar (lambda (x)
              (let ((node1 (car x))
                    (node1-edges (cdr x)))
                (cons node1
                     (mapcar (lambda (edge)
                                (let ((node2 (car edge)))
                                 (if (intersection (edge-pair node1 node2)
                                                    edges-with-cops
                                                    :test #'equal)
                                      (list node2 'cops)
                                    edge)))
                              node1-edges))))
            edge-alist))

These are the most cumbersome functions in Grand Theft Wumpus. Let’s take a closer look at them.

The make-city-edges Function

First, the make-city-edges function creates a list of nodes, using a loop . (This is simply a list of numbers from 1 to *node-num*.) Next, it creates a random (but fully connected) edge list by calling the make-edge-list and connect-edge-list functions . This result is stored in the edge-list variable. It then creates a random list of edges that contains cops . We define these variables with the let* command, which allows us to reference previously defined variables.

The following example shows the difference between defining variables with let and let*:

> (let ((a 5)
        (b (+ a 2)))
    b)
*** - EVAL: variable A has no value
> (let* ((a 5)
         (b (+ a 2)))
    b)
7

As you can see, let won’t allow you to refer to other defined variables (the variable b can’t reference the value of a). When defining variables with let*, on the other hand, this kind of reference is allowed. For our purposes, using let* allows our definition of cops to contain a reference to edge-list.

Once we’ve created the edge list and determined where the cops are, we need to convert our edge list into an alist and add the cops to it . The edges are converted to an alist with the edges-to-alist function, and the cops are added with the add-cops function.

The edges-to-alist Function

The edges-to-alist function converts a list of edges into an alist of edges. For example, assume we have the following city, with only three locations and two edges connecting those locations:

image with no caption

We would describe this using an edge list as '((1 . 2) (2 . 1) (2 . 3) (3 . 2)). Remember that each of the edges is repeated, since the edges are undirected and can be used in both directions. If we described this same city as an alist, what would that look like?

Remember that an alist is a list that lets us look up a key (in this example, one of the three nodes in our city) and find the information associated with that key (in this case, a list of the roads connected to it). For this small city, the alist would be '((1 (2)) (2 (1) (3)) (3 (2))).

To build this alist, the edges-to-list function first mapcars over the nodes found in the edge list. To build the list of nodes, we use the remove-duplicates function, which removes duplicate items from a list. By default, remove-duplicates uses the eql function to check for equality, though it also allows you to choose a different test function using the :test keyword parameter. Since we’re checking for equality of cons pairs in our make-city-edges function, we set :test to #'equal .

Within this outer mapcar , we use another mapcar to map across all the direct-edges to this node. Together, these nested mapcar functions allow edges-to-alist to convert the edges of a city into an alist.

The add-cops Function

When we wrote the make-city-edges function, we randomly marked some of the edges to show that they have cops on them . We are now going to use this list of cop edges to mark the edges in our alist that contain cops. This is the job of the add-cops function.

To do this, we use nested mapcar commands to map across the edges within each node . We then check whether there are any cops on a given edge, using the intersection function . (The intersection function tells us which items are shared between two lists.)

To understand exactly what the add-cops function is doing, it will help to once again imagine our city with only three locations and two streets. In this example, one of the streets has cops on it:

image with no caption

The generated alist for this city, created by add-cops, would look like this:

((1 (2)) (2 (1) (3 COPS)) (3 (2 COPS)))

This is actually a nested alist. The outer alist is organized based on the first node, and the inner alists are organized based on the second node.

With the edges in this format, we can easily find all edges connected to a given node by calling (cdr (assoc node1 edges)). To see if a given edge contains cops, we can call (cdr (assoc node2 (cdr (assoc node1 edges)))), which goes down two levels to grab the actual data linked to a specific edge between two nodes. (One additional benefit of using this nested alist format is that it is fully compatible with our graph libraries—a feature that we’ll take advantage of shortly.)

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

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