Black and White

Assignment 2


Introduction

Othello is a well-known two-player strategy game. For this project, you will develop portions of an intelligent program that plays Othello. We've provided you with a user interface and the game logic (code that implements the rules of the game) that will allow two human players to play the game. You will write the search and strategy routines that will allow "the computer" to play the game.

Othello — also known as Reversi — is a strategy game played on a square board divided into an 8x8 grid. The rules of the game, along with some good ideas for a sound strategy of play, are described in the Wikipedia entry on Reversi--do read it!

Be sure you know how to play the game before attempting to complete this program; it will save you much time and effort. As provided, the program will already play a game of Othello with two human players, but it will not allow you to play against a computer player until you create your AI class and write the necessary code in the OthelloAIFactory class (see below).


Starting Point

All of the code that you'll need to complete the project is included in this Black and While code archive; do download it. Much of the code is provided in compiled .class files. The provided java source code (.java) files are heavily commented.

You'll only need to work on two classes. First, you need to create a new class that implements the OthelloAI interface. Your class' name must begin with OthelloAI, followed by your eight-digit student ID#. So, if your student ID# is 12345678, your class should be called OthelloAI12345678.

Second, you write one line of code in the OthelloAIFactory class; see the class' comments for details.

Leave everything else as is.


How to run the program

The Othello class contains a main() method, so to run the program, execute the Othello class.

When you run Othello, a window will appear with a green area with the label Click here to start game. Click the green area and you'll be asked to specify whether each player should be controlled by a human or the computer; for now, specify human for both, as you haven't implemented your AI yet. Clicking on OK starts the game.

A human-controlled player makes a move by double-clicking an empty square on the grid. Not all squares constitute valid moves; the mouse cursor will turn into a "hand" when a square is a valid one, much like when you hover over a link in your browser. (Note that you have to place the cursor rather precisely in the square for the cursor to change, so move the mouse within the square a bit if you are in a square that you think is legal to use, but in which the cursor is not a hand.) The computer simply moves when it is its turn. The GUI animates the placing and flipping of tiles, so that you can see the moves "in action." Status messages display the score and remind you whose move it is.


Some necesary terminology

You will be building a rudimentary artificial intelligence (AI) so that the computer can play a game of Othello against you (or against another instance of your artificial intelligence). Your task for this project is fairly narrow, so you can disregard the vast majority of the code that we gave you, most of which implements either the GUI or the game logic. In fact, most of the code has been provided in compiled form, rather than as source code, for this very reason.

There are three main abstractions that you need to understand in order to write the code required for this project:


Game trees

You can think of the possible game states as being arranged, conceptually, in a kind of search tree called a game tree. Each node of the tree contains a particular game state g. Its children are the game states that can result from making each valid move from the state g.

The root of the tree is the initial game state, the state of the Othello game before the first move is made. The children of this initial state are all of the possible states that can arise from the black player (who moves first) making a valid opening move. There are four such states, corresponding to the four possible moves that the black player is permitted to make at the opening. (All other moves are illegal and, as such, are not to be considered.)

Take a look at this partial Othello game tree:

From the initial state, the root of the tree, there are four possibilies from which the black player can choose its initial move, so the root has four children. From the first of these moves, the left-most child of the root, there are three possible moves that the white player can make in response, so there are three children of this node. From each of these moves, other legal moves exist; they would appear as children of this node (these moves are not pictured). The tree continues to grow in this fashion, each new move producing more children. (Not surprisingly, the game tree can grow very large very quickly.)

A game ends when there are no legal moves on the board, that is, we reach a final state; at this point, one player or the other has won the game. Since there are no legal moves on these boards, the nodes representing them will have no children; so, final states are leaves of the game tree.


Exhaustively searching all possibilities

Each time a player wants chooses a move, s/he wants to pick the one that will lead to a winning game state. Assuming you had the complete game tree at your disposal, you could determine your best move in three steps:

  1. Choose the final state that gives us the best win. This is typically done by applying an evaluation function to each final game state. This function typically returns a number, where higher numbers are considered better--so we choose the state that has the highest value.
  2. Determine the path from the current game state to the final state that we chose above.
  3. Make the move that takes us from the current game state down the path toward the chosen final state.

Sadly, practical limitations make this easy-to-implment approach impossible. First of all, the number of game states on each level of the tree grows exponentially as you work your way down the tree, since there are a number of possible moves that can be taken from any particular game state. There simply won't be enough memory to store the entire game tree. (You can imagine that, if you build the game tree 20 levels deep, and there are four possible moves that can be made from any particular state, the number of nodes in the tree would be greater than 420, which is a very large number indeed!) Besides, even if there were enough memory available to store the tree, the processing time to create the entire game tree would be prohibitive.

So we'll need to find a compromise — an approach that perhaps doesn't always find the best possible outcome, but that makes a decision in a reasonable amount of time and using a reasonable amount of memory.


Heuristic search

The study of artificial intelligence has much to say about good ways to search toward a goal when it's impractical to check all possible paths toward it.

We can first make use of the following observation: Suppose black has made a move in the game, and white wants to figure out the best move to make, using the search tree approach we've been discussing. Then white need only concern her/himself with the subtree that has the current game state as its root. Once a move is made, all the other moves that could have been made can be ignored, as it is now not possible to take those paths down the tree. Thus, when analyzing the next move to make, we need only generate the part of the search tree that originates from the current game state.

This approach reduces our storage needs significantly — and we don't waste time or memory processing parts of the tree that we can no longer reach.

Even if we generate only the part of the tree that we need, that part will still be much too large to store until we're nearing the end of the game. This is where a heuristic search comes into play. In a heuristic search, we generate as much of the relevant subtree as is practical, using the resulting game states to guide us in selecting a move that we hope will be the best.

There are several strategies that we could use. At the heart our strategy is using an evaluation function to rate each particular game state in some way, so that we can decide which of a large number of game states is the best outcome for us. A simple approach — though one that ignores some aspects of the game — is the following:

eval(state) = number of tiles belonging to me − number of tiles belonging to my opponent

It's also important to note here that you do not need to actually build a game tree in memory. Our algorithm will perform a sort of depth-first search on the game tree, meaning that we can use parameters in a recursive method (stored on the run-time stack) to perform the search, negating the need to actually build and store a game tree. This will dramatically reduce the amount of memory needed to choose a move, since only one path in the tree will ever need to be stored on the run-time stack at a time.

Putting these ideas together, we can develop a search algorithm that will look for the move that leads to the game state that evaluates to the highest value. That algorithm looks something like this:


int search(OthelloGameState s, int depth)
{
    if (depth == 0)
        return evaluation of s
    else
    {
        if (it's my turn to move)
        {
            for each valid move that I can make from s
            {
                make that move on s yielding a state s'
                search(s', depth - 1)
            }
            
            return the maximum value returned from recursive search calls
        }
        else
        {
            for each valid move that my opponent can make from s
            {
                make that move on s yielding a state s'
                search(s', depth - 1)
            }
            
            return the minimum value returned from recursive search calls
        }
    }
}

There are a few things we need to discuss about this algorithm. First, notice that there are two cases of recursion: either it is the computer player's turn (who is currently making the decision) or its opponent's turn. In each case, the algorithm is almost the same, except:

Either the black or the white player (or both!) might be a computer player. So, when deciding whether it's "my turn" or "my opponent's turn," you'll have to exercise some caution to ensure that you're making the right decision.

Second, notice the depth parameter, used to limit the depth of our search, to make sure that our search is of a manageable length. Each time we recurse one level deeper, the depth is reduced by one, and we stop recursing when it reaches zero.

Experiment to find a depth that can compute a move fairly quickly, but still gives the computer player the most “look ahead” feasible. (Hint: as the game progresses, the number of possible moves changes. At some point, the number of moves might become small enough that you can increase the search depth; you might also have to decrease the search depth if the number of moves increases. Adjusting the depth as the game progresses can make the computer “smarter” without unduly delaying the game.)

Third, observe that when one player makes a move, it isn't necessarily the case that the other player will be making the next move; occasionally, in Othello, the same player gets to move twice in a row. So, care must be taken in deciding whose turn it is. The easiest way to deal with this problem is to remember that the current game state keeps track of whose turn it is--so query the game state when you need this information.

Lastly, note that this algorithm returns the evaluation of the best state, not the best state itself. Calling search(s, 4) for some state s asks the following question: "Looking four moves into the future, and assuming I and my opponent do the best we can do, how well will the state s turn out for me?" You'll need to exercise some care in actually implementing this algorithm so that chooseMove() will be able to call search() and use the result to help it choose the right move.

Be sure to test your program for correct play and reasonable play; a good way to do so is to play against your own program (or have friends or classmates play against it) to see if it can be broken or beaten. Obviously, if playi breaks your program, you need to fix it. If people routinely beat the computer, consider fine-tuning your program’s search depth until it does better. You may also wish to change the approach the evaluation function or the game tree search employs, as discussed in Option Work, below.


A Couple of Technical Points


Optional Work

Modify your evaluation function. The core of your AI — what will set it apart from others — is the evaluation function that it uses to decide how "good" each board configuration is. Our evaluation function may not always be—may even never be—the best to use: a move that puts more of your tiles onthe board right now might set up the board so your opponent could later obtain a larger quantity of tiles than you did. Perhaps another move would have left the board so that your opponent would be in a poor position. So, modify your evaluation function, or write several that you can “swap in and out” of your program, and see how they do against a human and computer player, and against the evaluation function we gave above. (You might want to poke around the web looking for strategy guides or other information, taking into account, for example, that some squares on the Othello board are considered more important than others). Try playing against your own program to see if you can beat it! Write up what approaches you tried, your results and your conclusions, as comments in your Othello AI class. In particular, tell us which function worked best and why you believe it did.

Write searches that use other heuristic techniques. In addition to the heuristic search we used above, there are other approaches to heuristically searching a game tree. Try out one or more of approaches to see how well they perform relative to the depth-first search approach used above and when used against each other. (A simple way to do this is to modify your program so that, if both players are the computer, a different strategy is used by each one. Then, have the computer play itself several times and see what happens.) For instance, an approach called alpha-beta pruning, which is in essence a smarter depth first search, is often a good technique to employ (and perhaps better than the one we have used here). Again write up your results and your conclusions as comments in your Othello AI class. Be sure to tell us which strategy worked best and why you think it did.


A Tournament

After this project's due date has passed, we'll gather all of your AIs together and run a tournament to determine who has the best one. The rules we will use are as follows:

The outcome of the tournament will have no bearing on your grade, but we hope it will motivate you to make the best evaluation function you can.

Good luck!


Deliverables

You need only turn in your OthelloAIFactory.java file and the file containing your AI class, along with any additional inner classes you created, if any. Do not turn in any of the other files that were provided to you.


Originally written by Alex Thornton, Fall 2007, with portions taken from End of the Game
  by Alex Thornton and Norman Jacobson.
Revised for ICS 23 Winter 2008 by Norman Jacobson, December 2007, with portions
  (sometimes revised) from the January 2007 version of End of the Game
  by Alex Thornton and Norman Jacobson.