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 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. Since the provided program allows play with two human players, you can “play against yourself” to get some feel for the game’s rules and strategy. The provided game will not allow you to play against a computer player until you create your AI class and write the necessary OthelloAIFactory class code (see below).


Starting Point

All of the code that you’ll need to complete the assignment is included in the zipped-up BlackandWhile project folder; download the file, unzip it, and add it to your workspace. Much of the code is provided in compiled class files, in the lib folder. The provided java source code (.java) files are heavily commented.

To add this project to your 23 workspace,

  1. Move the BlackAndWhite folder into the folder containing your ICS 23 workspace.
  2. Start up eclipse and select your workspace.
  3. From the file menu, select Import..., which will pop up a dialog box. under General, select Existing projects into workspace, then click Next >.
  4. Next to Select root directory, click the Browse button. Find the BlackAndWhite folder that you copied into your workspace folder, select it, then click OK.
  5. Finally, click Finish. You should now see a project called BlackAndWhite in the Package Explorer. Your Problems window may show some warnings; each of these warnings indicates that there is a part of the program that has yet to be built.

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. (if you are an Access student, use the id number Access provided you, with the “X” removed and as many leading zeroes as needed added to make the number 8 digits.)

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

Leave everything else as is. In particular, do not change the names of files, classes, interfaces or methods that we provide.


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 necessary 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 possibilities 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 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 you 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, choose the state with the highest value.
  2. Determine the path from the current game state to the final state that you chose.
  3. Make the move that takes you from the current game state down the path toward the chosen final state.

Sadly, practical limitations make this easy-to-implement 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 typically a large 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 good decision in a reasonable amount of time while 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 need to be stored on the run-time stack at any one 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 like this:


int search(OthelloGameState s, int depth)
{
    if (depth == 0 or reached a final state)
        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 much “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 move for state s, not the best state itself; nor does it return the evaluation for some other state reachable from s (for example, for one of the s' states). Calling search(s, 10) for some state s asks the following question: "Looking ten 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 playing 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 evaluation function or the game tree search algorithm, as discussed in Optional 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 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 on the 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 worse 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 for heuristically searching a game tree. Try out one or more of them 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). If you do use one or more optional searches, include in your code all you wish us to see, along with your implementation of the min/max algorithm we provided above. Have a flag that determines which search is the one to use, and set it to the one you want to use in the tournament (see below).

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:

Where you rank in the tournament will have no bearing on your grade, but we hope it will help motivate you to make the best evaluation function you can.

Good luck!


Deliverables

Turn in only your the file containing your AI class, (which will include any additional inner classes you created, if any); for example, turn in the file labeled OthelloAI12345678.java if your UCI ID is 12345678. Do not turn in any of the other files provided to you. Do not ZIP up or otherwise archive these files.


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
Minor revisions for clarity, by Norman Jacobson, December 2007 and January 2009
Minor revision to clarify that files turned are not to be ZIPped, by Norman Jacobson, February 2009
Minor revisions for clarity, and to fix a few typos, by Norman Jacobson, March 2009, May 2009 & March 2010
Revised to reflect use of Eclipse, by Norman Jacobson, March 2010
Made explicit that all searaches used (e.g., depth first serach and minmax) are to be left in the code, by Norman Jacobson, April 2010
Minor edits, by Norman Jacobson, March 2011
Revised to discuss how to measure elapsed time of a move, and to clean up a few passages, by Norman Jacobson, April and May 2011