Bookmark and Share


View a screen shot of this program.

Game Play

The following variables are used to handle play during a game.

// Game parameters.
private GameState gameState;
private int       currentColor;
private int       moveNumber;

moveNumber should be self-explanatory. currentColor indicates which player has the current turn (Black or White). gameState is set to one of these enumerated values:

// Defines the game states.
private enum GameState
  GameOver,         // The game is over
                    // (also used for the initial state).
  InMoveAnimation,  // A move has been made and the animation is active.
  InPlayerMove,     // Waiting for the user to make a move.
  InComputerMove,   // Waiting for the computer to make a move.
  MoveCompleted     // A move has been completed (including the animation,
                    // if active).

Most of the game play is event driven, so the use of gameState allows the various event handlers to determine the proper actions to take. For example, when the user clicks on a board square, SquareControl_Click() is called. If the game state is InPlayerMove, the move will be made on that square. But if the game is in some other state, it's not the user's turn to move so the click is ignored.

Likewise, if the user clicks the tool bar "Undo Move" button, we want to check the game state to see if any thing needs to be done before resetting the game to the previous move. For example, if the state is InMoveAnimation, the animation timer needs to be stopped and the square controls need their counters and display reset. Or if the state is InComputerMove, the program is currently doing a look-ahead search in a separate thread (see below) which will need to be stopped.

Program Flow

The diagram below illustrates the general program flow during the course of a game.


StartTurn() gets called at beginning of a game, after a move is made by either player and whenever a move undo or redo performed. It is responsible for evaluating the game situation and setting up for the next move.

It first checks to see if the current player can make a legal move. If not, it switches to the other player and checks if that player has any legal move. When neither player can make a move, by rule, the game is over and it will end the game.

Otherwise, the function will set things up for the current player to make a move. If the current player is under user-control, it simply exits. The user can then make a move by clicking the mouse pointer on a valid square or by typing in a valid column letter and row number. This results in a call to MakePlayerMove() which does some housekeeping and then calls MakeMove() to perform the move. If the current player under computer-control, it starts the look-ahead search to find the best move.

Using a Worker Thread

Because the look-ahead search is computationally intensive, it is performed in a worker thread. It this were not done, the main form would freeze and become unresponsive during the time it takes for the computer to calculate its best move. So StartTurn() creates a worker thread to execute the CalculateComputerMove() method and starts it.

A lock is used on the main game board object to prevent race conditions. As an example, both the MakeComputerMove() the UndoMove() methods change to the game board. Both methods first attempt to put a lock on it. So if one method happen to be called while the other is in the middle of updating the board, it will be forced to wait until the those changes are completed and the lock is freed.

Once a move has been found, the CalculateComputerMove() method does a callback to run MakeComputerMove() in the main form. This method gets a lock on the board and calls MakeMove() to perform the move.

Performing a Move

MakeMove() does the actual updating of the board, placing the new disc a the specified location. It also does some maintenance on the undo/redo move history and removes any square highlighting.

Then, if the move animation option is turned off, it simply calls EndMove() which will switch currentColor and start the next turn with a call to StartTurn().

But if the animate option is on, it will instead initialize the discs to be animated and start the animation timer. As discussed previously, the timer causes AnimateMove() to run every few milliseconds, updating the display and decrementing the animation counters each time. Eventually, the counters will reach their end and AnimateMove() will then call EndMove() to complete the move.

Future Enhancements

There is much room for improvement in the computer-player AI. The look-ahead algorithm could be augmented with an book of opening moves or a set of corner and edge patterns that have been pre-ranked. It could be made to utilize selective deepening, where the search depth could be extended for moves that generally have more impact on the game, such as near the corners. Another improvement would be to store the look-ahead tree. This would allow it to be searched to a greater depth because the program would not have to regenerate the same moves each time.