ReversiView a screen shot of this program.
This is an implementation of the game Reversi written in C#.
I originally wrote this program as an exercise for learning C# and .Net programming in general. Reversi - or Othello as it is also known - is a popular game that is fun, requires just a few basic elements and has simple rules. This made it a good choice for learning a new programming environment.
The result was a fairly playable game but it lacked some of the more common features of computer board games, such as the ability to undo moves. So, having gained more experience with .Net, the time came for an upgrade. The result adds some new features and improves on the original graphics and AI.
Using the Code
Compile the source files and run the resulting Reversi.exe executable to play the game. You can play around with the various options and settings using the menu or toolbar. Try resizing the window, changing colors or switching sides with the computer in mid-game.
You may note that the program creates a file called Reversi.xml when you exit it. This file is used to save various settings such as game options, window size and position and player statistics which are reloaded when program is run again.
A Windows help file is included with the source code. It can be found in the help files subdirectory within the archive. To make the help file available to the program, simply copy the Reversi.chm file from there to the directory where the Reversi.exe executable is located. You can run the program without it, but clicking the Help Topics option will display an error saying the file cannot be found.
All the source files used to create this help file are included in that subdirectory. You can edit and recompile it using the Microsoft HTML Help Workshop.
Points of Interest
The source is pretty well commented but some general overviews are in order.
A good portion of the code is related to calculating moves for the computer player so it is worth discussing. The program uses a standard min-max look-ahead algorithm to determine the best move for the computer. Alpha-beta pruning is used to improve the efficiency of the look-ahead search. If you're not familiar with the min-max algorithm and/or alpha-beta pruning, a Google search will turn up plenty of information and examples.
Naturally, there are too many possible sequences of moves in the game to do an exhaustive look-ahead search, it would simply take too long to generate all possible move combinations. The exception is towards the end of a game where there are relatively few empty squares left - around ten or twelve. At this point, a complete search can be done and it is possible to find the move with the best possible outcome for a given player.
But in most cases, the look-ahead depth must be limited to a certain number of turns (based on the game's difficulty setting). So for each series of possible moves and counter-moves searched, the resulting game board must be evaluated to determine which player has the best chance of eventually winning the game. This evaluation is done by calculating a rank using the following criteria:
- forfeit - Leaving your opponent with no legal move forces her to forfeit her turn, giving you a big advantage in being able to move two (or more) times in a row.
- mobility - This is a measure of how many legal moves you can make vs. how many legal moves your opponent will be left with. Similar to forfeit, the idea is to reduce your opponent's options while maximizing your own.
- frontier - A frontier disc is one that is adjacent to an empty square. Having a large number of frontier discs will, generally speaking, give your opponent greater mobility on subsequent turns. Conversely, having few frontier discs means your opponent will have limited mobility later on. This score reflects the number of your frontier discs vs. your opponent's.
- stability - Corner discs are stable, they can never be outflanked. As a game progresses other discs will become stable as well. This score reflects the number of your stable discs vs. your opponent's.
- score - This is simply the difference between the number of your discs on the board vs. your opponent's.
Different weights are assigned to each of these scores (again, based on the game's current difficulty setting). A board is assigned a rank by multiplying each criteria score by its corresponding weight and adding these values together. A large negative rank represents a board favorable to Black while a large positive rank represents a board favorable to White. So for a given set of possible moves, the computer will select one that results in the best ranked board for the color being played.
A constant named
maxRank is used to indicate an end-game.
It is set to the value of
System.Int32.MaxValue - 64.
This assures that any move resulting in the end of the game will always have a higher rank (negative or positive) than other moves.
Subtracting 64 from the system's maximum integer value allows us to add the final score to the rank so that a win by 10 discs will rank higher than a win by 2 discs. This causes the computer player to maximize its score when winning (or to minimize the opponent player's score when it's losing).
The current implementation is no match for the better AI players out there, but it plays pretty tough against a puny human opponent (at least, this puny human opponent). Again, a Google search will turn up plenty of resources describing strategies and AI approaches to the game.
Board class represents a game board.
It uses a two-dimensional array to track the contents of each board square, which can be one of the following constant values defined in the class:
Two constructors are provided. One creates a new, empty board while the other creates a copy of an existing board.
It provides public methods like
MakeMove() which adds a disc to the board, flipping over any outflanked opponent discs.
IsValidMove() can be used to determine if a given move is valid for a given player.
HasAnyValidMove() returns false if the given player cannot make any legal moves.
It also tracks disc counts for each player which are used by the computer move AI routine. These counts include the total number of discs, the number of frontier discs and the number of safe (or unflippable discs) of each color.
In the main
ReversiForm class, a couple of structures are defined for storing game moves.
Both contain a row and column index pair which corresponds to a particular board square.
ComputerMove struct is used for the computer AI.
In addition to the move position, it has a
This is used to track how good or bad the move is, as determined during the look-ahead search.
MoveRecord struct is used for storing information about each move made during the course of a game.
To allow the move undo/redo feature, an array of these is kept to track the board during each turn.
A move record contains a
Board representing the game board as it was before the particular move was made, as well as value indicating which player is to make the next move.
An array of these is kept as moves are made by each player, allowing the game to be reset state it was in at any point in the move history.
RestoreGameAt() method handles the resetting of the game to a particular move number.
While it potentially allows the game to be restored to any move currently in the history, the menu and tool bar options on the main form currently only provide for a single move undo/redo at a time or an undo/redo of all moves.
A future enhancement might be to allow the user to click on items in the move list to restore the game to the corresponding move number.
Graphics and the User Interface
The Game Board
Squares on the board are represented by a user control named
There is one of these for every square on the game board display.
The control contains information for displaying the square and its contents (empty or a black or white disc) including the animation of discs and any highlighting.
Displaying the Discs
Each disc is drawn dynamically. The basic shape is a circle with some highlighting and a shadow to give it a pseudo-3D appearance. The shapes are scaled in size based on the current size of the square control. By rendering them this way, instead of using static images, the board may be dynamically resized to fit within the form window.
Click event on the square controls is handled within
ReversiForm allows the user to make a move to a particular square (assuming it's a legal move).
MouseLeave events are handled to update the board display to highlight valid moves or preview a move when those options are active.
Animation of Moves
The disc flip animation is done using counters defined within the
SquareControl class along with a
Basically, this is a thread controlled by the operating system which periodically raises an event that your form application can respond to.
After a move is made, if the move animation option is active, each affected square control has its counter initialized and the timer is activated.
The main form's
AnimateMove() method is called each time the timer ticks (see below).
This method updates the square counters and redraws their display.
The animation basically consists of changing the disc shape from a circle to ever thinner ellipses, then back to a full circle but in the opposite color.
The smoothness and speed of this animation depends on how large the initial counter value is (set by the constant
SquareControl.AnimationStart and how often the timer ticks (set by the constant
animationTimerInterval in the main form).