Keyword parameters

If you've looked up the "definitions" of how some Common LISP functions
work in either of the books, you've probably seen keyword parameters
already.  Here's a quick summary with a few examples.

By now, we know what happens with basic function calls:

   (remove 3 '(1 2 3 4))      -->   (1 2 4)
   (remove '(a) '(b (a) c))   -->   (b (a) c)

Why does that second one behave as it does?  Because EQL is the default
equality predicate for all LISP functions, and so, as far as REMOVE is
concerned, no items in the list EQL the thing we want to remove.

So what if we do want to remove the (a) from our list above?  In other
words, what if we want to use EQUAL instead of EQL as the test for
removal?

We could, of course, simply write our own function, MY-REMOVE, which
works just like REMOVE but uses EQUAL instead of EQL.  Or, we can use
keyword parameters to make the builtin version behave as we desire.
Specifically, we can say

   (remove '(a) '(b (a) c) :test #'equal)   -->   (b c)

Here, ":test" is a keyword parameter, that specifies the equality test
to use.  Its default is EQL, but we can override it by passing any
function as the "test" parameter, and that test will be used instead of
EQL.

The general format for using keyword parameters is

   (func  normal  parameters  :keyword-1  value-1  :keyword-2  value-2)

The keyword parameters are optional, and come after the normal
parameters to the function.  Each value must follow a name, specifying
which keyword-parameter it is.  This is because many functions take
multiple keyword parameters, like

   (find item sequence :test #'eql :key #'car)

That works very much like

   (assoc item sequence)

since ASSOC tries to find an item in a list whose CAR matches the "item"
passed, and it uses EQL to test if they're the same.


One place that keyword parameters will probably be valuable to you in
hw5 is when checking for cycles.  Presuming you're keeping a board
history, you can detect a cycle by seeing if the new board generated is
already in your board history list.  You can make that test with
something like

   (member new-board history-list :test #'equal)

Here we use EQUAL as the test, since boards are not atoms, but
more complicated, nested-list structures.  EQUAL will test that two
boards hold the same values.

That's all I'll say about keyword parameters for now.  You can read more
about them on your own.  The beginning of Chapter 14 of the Guy Steele
book (the one on the web) has a number of decent (basic) examples.


Game Search

We reviewed the material from Wednesday about Game Search and hex.


The minimax procedure

Now that we have a reasonably well-defined static board 
evaluation function, how do we use it?  Remember that the 
idea behind creating this thing was to estimate the 
"goodness" of a board---in this case, the function returns a 
positive number when the board is good for us, and a negative 
number when the board is good for our opponent.  Let's apply 
the function we defined above to all the boards at the bottom 
level of the hexapawn state space we generated way back up 
there:

                                   W W W                   
                                   - - -                   
                                   B B B                   
                                  /                        
                                 /                         
                                /                          
                               /                           
                              /                            
                             /                             
                           - W W                           
                           W - -                           
                           B B B                           
                           / | \                           
                        /    |    \                        
                     /       |       \                     
                  /          |          \                  
               /             |             \               
            /                |                \            
         /                   |                   \         
       - W W               - W W               - W W       
       B - -               W B -               W - B       
       B - B               B - B               B B -       
     /   |   \              / \              /   |   \     
    /    |    \            /   \            /    |    \    
   /     |     \          /     \          /     |     \   
  /      |      \        /       \        /      |      \  
- - W  - - W  - W -    - W -   - W -    - W W  - - W  - - W
W - -  B W -  B - W    W B W   W W -    - - B  W W B  W - W
B - B  B - B  B - B    B - B   B - B    B W -  B B -  B B -

  0      1      1       -10     -1       -10     0     -1

The two boards that have been assigned a value of -10 are, of 
course, the boards that represent victories for white.  In 
the case of the leftmost of those two boards, it's a victory 
for white because it's now our turn (i.e., black's turn) and 
we can't move any of our pawns.  The rightmost of these two 
boards is a victory for white because white has moved one of 
its pawns all the way across the board.

But let's take a look at another board.  The one at the very 
left, for example, has been given a value of zero.  Yet it's 
easy for us to see that, since it's our turn, and we only 
have one black pawn that we can move, if we just move that 
one black pawn forward one space, we'll have blocked any 
possible move by white and we win the game.  So why isn't 
that board given a value of +10?  Because in order to figure 
that out, our static board evaluation function would have to 
look ahead one more move.  But a static board evaluation 
function is exactly that---static.  It doesn't look ahead.  
If we set a limit on the number of moves we want to look 
ahead in order to play the game in a reasonable amount of 
time, but then we have our board evaluation function look 
even further ahead, we're going to eat up additional 
computing resources that we were trying to save, and we're 
also going to end up writing the same code twice.  So there's 
absolutely no advantage to having the board evaluation 
function look ahead an additional move or two or three---
instead, we should just readjust our original depth cutoff so 
that it allows us to look more moves into the future.

Now let's go back and look at those bottom two levels in our 
hexapawn state space:

       - W W               - W W               - W W       
       B - -               W B -               W - B       
       B - B               B - B               B B -       
     /   |   \              / \              /   |   \     
    /    |    \            /   \            /    |    \    
   /     |     \          /     \          /     |     \   
  /      |      \        /       \        /      |      \  
- - W  - - W  - W -    - W -   - W -    - W W  - - W  - - W
W - -  B W -  B - W    W B W   W W -    - - B  W W B  W - W
B - B  B - B  B - B    B - B   B - B    B W -  B B -  B B -

  0      1      1       -10     -1       -10     0     -1

What can we do with those numbers that have been assigned to 
the boards?  Those boards all represent possible results of a 
move by white.  Those numbers can be used to tell us which of 
those moves white is more likely to make.  For example, in 
the leftmost subtree, we might guess that white is more 
likely to make the move that results in a board with value 0 
than the moves that result in boards with value 1, because a 
board with value 0 is better for white than a board with 
value 1, which favors us.  That assumes, of course, that we 
trust our evaluation function.  Similarly, in the middle and 
rightmost subtrees, white is going to prefer the moves that 
result in a board with a value of -10 (a victory for white), 
right?  We can indicate those preferences by taking the 
minimum values among those board values in each subtree and 
propagating them up one level: 

                           - W W                           
                           W - -                           
                           B B B                           
                           / | \                           
                        /    |    \                        
                     /       |       \                     
                  /          |          \                  
               /             |             \               
            /                |                \            
         /                   |                   \         
       - W W               - W W               - W W       
     0 B - -           -10 W B -           -10 W - B       
       B - B               B - B               B B -       
     /   |   \              / \              /   |   \     
    /    |    \            /   \            /    |    \    
   /     |     \          /     \          /     |     \   
  /      |      \        /       \        /      |      \  
- - W  - - W  - W -    - W -   - W -    - W W  - - W  - - W
W - -  B W -  B - W    W B W   W W -    - - B  W W B  W - W
B - B  B - B  B - B    B - B   B - B    B W -  B B -  B B -

  0      1      1       -10     -1       -10     0     -1

OK, now how can we use that information?  We use it in almost 
exactly the same way as we did before.  We can figure out 
which move we should make of the three that are available to 
us by finding the maximum of the values that we just 
propagated upward.  One of those values was a 0 and the other 
two were -10.  Of course, the 0 is better for us, so we'd 
choose to make that move.

Let's go back and see what we've done here.  First we started 
with some arrangement of pawns on the board and the knowledge 
that it was our turn.  We generated all the moves we might 
make, and then we generated all the moves that our opponent 
could make after we made our move.  We arbitrarily chose to 
look only two moves ahead, but we could have looked further 
if we wanted to give up the computational resources to do so.  
We then applied our static board evaluation function to the 
bottom-most boards (i.e., the terminal leaves on the tree 
that is our state space) and assigned a numeric value 
corresponding to "goodness" to each of those boards.  Those 
bottom-most boards are each the result of a possible move by 
white.  We assumed that white would always make the best move 
it possibly could, so we propagated the minimum values up 
from the leaves to the immediate parents.  And then we 
assumed that we would want to make the best possible move 
that we could, and we chose that move by selecting the 
maximum of the values that had just been propagated upwards.

Because we chose to look ahead only two moves, the first 
propagation was of minimum values from the very bottom level, 
followed by a propagation of maximum values upward from that 
level.  If we had chosen to look ahead three moves, we'd 
first propagate maximum values from the bottom, then 
minimums, then maximums.  If we were looking ahead four 
moves, we'd start with minimums, then maximums, then 
minimums, then maximums.  And so on, and so on.  The 
procedure that we just described has a name, "minimax", and 
it's the heart of game-playing computer programs.

The minimax procedure relies on two assumptions.  First, 
there must be an adequate and reasonably accurate board 
evaluation technique.  It doesn't have to be perfect, but it 
does have to work more often than not.  The second assumption 
is that the relative merit of a move becomes more obvious as 
you search deeper and deeper into the state space.  Why?  
Because if that weren't so, there wouldn't be any value in 
doing the search in the first place.  But keep in mind that 
for any given game, or at least any given implementation, one 
or the other (or both) of these assumptions may not be true.


Lecture notes by Kurt Eiselt, 1998.
Minor changes / additions by Brian McNamara, 1998.
Last updated on Sat Aug 1 19:20:39 EDT 1998 by Brian McNamara