Professional Documents
Culture Documents
Four. While expert level play is not required, it should provide a challenge for
those who are only casual players of the game.
User Requirements
Connect Four is a two-player game played on a vertical board. The board is a
grid of six rows and seven columns. Players take turns dropping their game
pieces into one of the columns. Because the columns are vertical, each piece
drops to the lowest unoccupied cell in that column. Each player's goal is to get
four of his or her pieces in a straight line of adjacenct cells, either horizontally,
vertically, or diagonally. If the board is filled before either player wins, the
game is a draw. More information, including an animation of the game, can be
found on the game's Wikipedia page (Links to an external site.)Links to an
external site..
The user is a company that wants to market a version of the game. This
implementation needs to support user play against the program. It must allow
the user the option of having either the first or second play. It also needs to
support seven levels of play difficulty. Because this is a first release of the
game, expert play is not required, but each successive level should play a
stronger game. At the end of the game, it must report the winner, or that the
game was drawn. It only needs to support playing a single game (the
program can be restarted for a rematch), but it needs to leave the final board
visible until the user exits the program.
The tree shown below illustrates the negamax algorithm on some board that
is nearly full. In this example, the tree is searched to a depth of 3. The leaves
at depth 3 show the values given by the evaluation function, assuming that
player 'O' will play from these configurations. The two leaves at depth 2
represent configurations in which 'X' has lost the game. Note that from the
perspective of player 'X', the value of each of the depth-3 leaves is the
negative of the value shown. Thus, because the parent of each of these
nodes is a configuration in which 'X' will play, the best play for 'X' (shown with
darker lines) will be the one that leads to the maximum of the negatives of the
values of its child nodes. The values in these parent nodes are then these
maximum values. In this way, values and best plays can be computed for
higher and higher nodes, until we have a best play for the root node.
One case that is not shown in the above example is a node in which the board
has been completely filled. In this case, if neither player has won, the value of
the configuration is 0.
The algorithm will need a loop to determine the maximum of the negatives of
the values of the children. As it updates this maximum, it will also need to
update the play that gives this maximum. For each legal move from the
current configuration, that move is played, resulting in one of four cases:
If the move wins the game, the value is set to a suitably large number
(1000000 in our implementation), and this play is the best. These are
immediately returned.
Otherwise, if the move fills the board, the value is set to 0. Because this
must be the only legal move, it is the best move. These are immediately
returned.
Otherwise, if the given search depth is 1, we compute the evaluation
function from the perspective of the otherplayer on the resulting
configuration, and use its negative in the maximum we are computing.
Otherwise, we recursively evaluate the resulting configuration for
the other player with a search depth one smaller than the given remaining
depth. We use the negative of the result of this recursive evaluation in the
maximum we are computing.
After each legal move is processed, it must be undone to restore the board to
its previous configuration. Note that this must be done even in the cases that
immediately return a value. If the loop completes, the maximum it computes is
the value of the board configuration for the current player.
Evaluation Function
Our evaluation function will consist of two heuristics:
1. Each unoccupied cell that would win the game if occupied by the current
player will contribute 100 points. Each unoccupied cell that would win the
game for the opponent will contribute -100 points. (A cell might qualify for
both, in which case the two values cancel to 0.)
2. Each occupied cell contributes a certain value to the player who occupies
it (i.e., a positive value if the current player occupies it, or a negative value
if the opposing player occupies it). The value (either positive or negative)
is the number of length-four sequences of cells that contain that cell.
These values are fixed throughout the game, and are given by following
table:
3 4 5 7 5 4 3
4 6 8 10 8 6 4
5 8 11 13 11 8 5
5 8 11 13 11 8 5
4 6 8 10 8 6 4
3 4 5 7 5 4 3
The evaluation function is the sum of these two heuristics. Thus, heuristic 1 is
the main contributor to the evaluation function. Heuristic 2 is mainly for the
purpose of differentiating configurations that heuristic 1 evaluates equally -
particularly early in the game, when heuristic 1 will evaluate many
configurations to 0.
For example, suppose we have the following board configuration with "X" to
play:
X O X
O O O
O O X X
X X X O
For heuristic 1, "X" has one cell on the bottom row that can complete a win -
this gives 100 points. "O" has four open cells (one on the bottom row, two on
the third row from the bottom, and one on the second row from the top) that
can complete a win - this gives 400 points for "O", or -400 points for "X".
Because "X" is player making the move, the total for heuristic 1 is -300. For
heuristic 2, "X" occupies cells with the following values (going from the top
down, and left to right): 13, 8, 8, 6, 5, 7, 5, for a total of 52. "O" occupies cells
with the following values: 11, 13, 11, 8, 8, 10, 4, for a total of -65 for "X". The
total of heuristic 2 is therefore -13, and the overall value is -313. This example
illustrates the need to combine the evaluation function with some sort of
search - even though the evaluation function values this configuration as an
advantage for "O", "X" can, in fact, win immediately.
This evaluation function is rather simple, but it is easy to compute and not
terribly costly. Furthermore, when used with the negamax algorithm, it results
in reasonable game play.
User Interface
The program begins by opening a dialog resembling the following:
Note that this is not the main GUI. You will need to build this dialog by right-
clicking on the project name in the Solution Explorer and selecting
"Add->Windows Form...". It must be impossible to maximize or otherwise
resize this form (set its FormBorderStyle (Links to an external site.)Links to
an external site. property to FixedDialog). The NumericUpDown should
allow the user to select any integer from 1 to 7 (see its Minimum (Links to an
external site.)Links to an external site. and Maximum (Links to an external
site.)Links to an external site. properties). This value should be right-aligned in
the box (see the NumericUpDown's TextAlign (Links to an external
site.)Links to an external site. property). The two controls in the middle
are RadioButton (Links to an external site.)Links to an external site.s - the
first one should be initially checked (see its Checked (Links to an external
site.)Links to an external site. property). If the user checks one of them the
other should be unchecked (this behavior is provided automatically because
they are in the same container). If the user clicks the "OK" button, the dialog
should close, and the main GUI should open, as described below. By setting
the button's DialogResult (Links to an external site.)Links to an external
site. property to OK, you can achieve this behavior without writing an event
handler for the button. If the user closes the dialog by clicking the "X" button in
the upper-right corner, the program should exit.
To make it easier for the program to interact with the GUI, most of the controls
are laid out using FlowLayoutPanels (Links to an external site.)Links to an
external site.. You can find this control in the "Containers" section of the
Design Window Toolbox. One of these panels contains the row of buttons at
the top, and another contains the columns in which the game pieces will be
dropped. When a control such as a button is placed within
a FlowLayoutPanel, it is automatically placed at the next available position
within the panel. By default, controls are added from left to right, though this
behavior can be changed (see below). It will make it easier to work
with FlowLayoutPanels if you set each of their WrapContents (Links to an
external site.)Links to an external site. properties to false.
Each of the seven buttons at the top should be the same size (you can
copy/paste a button once you have it finished). They should each be initially
enabled.
The TextBox at the bottom should be read-only (see its ReadOnly (Links to
an external site.)Links to an external site. property).
If the user selects "Human plays first" in the setup dialog and closes it with the
"OK" button, the GUI should appear as shown above. The message, "Your
move.", indicates that the program is waiting for the user to play. Whenever
this message is shown, the user may play by clicking an enabled button. This
will cause an "O" to be placed in the lowest empty cell below that button. If
that play fills its column, the button clicked is disabled. If it wins the game by
completing a sequence of four "O"s in a row, either horizontally, vertically, or
diagonally, the message will change from "Your move." to "You win!". If it
does not win, but fills the last cell in the board, the message will change to
"The game is a draw." In either of these last two cases, all seven buttons
should be disabled, so that the only action that can be taken by the user is to
exit the program.
If the user selects "Computer plays first" in the setup dialog and closes it with
the "OK" button, the message shown should be, "My move.", while the
program is finding a move to make (at lower levels, this time may be short
enough that the message is not seen). The program finds a play by using the
negamax algorithm to a depth matching the level the user chose when setting
up the game; i.e., if the user chose level n, then the program searches to a
depth of n. Once the computer finds a move, it will display it by placing an "X"
in the lowest open cell in some column. This behavior will also occur following
a user play that doesn't end the game. If the program's play fills its column,
the button above that column is disabled. If it wins the game by completing a
sequence of four "X"s in a row, the message will change to "I win!". If it does
not win, but fills the last cell in the board, the message will change to "The
game is a draw." In either of these last two cases, all seven buttons should be
disabled. If the play does not end the game, the message should change to
"Your move.", and the program will await a play by the user.
A working executable has been posted. Note that the computer always plays
as "X", and the human as "O".
Software Architecture
The following class diagram shows the relationships between the classes to
be included in your program:
UserInterface and SetupDialog are the two forms described above. Board is
an internal representation of the board - the GUI will only be used for
displaying the board to the user, as accessing the GUI is
expensive. ComputerPlayer represents the computer player. Each of these
classes should be public.
The names in your program don't need to match the names shown above,
but they should obey the naming conventions for this course (Links to an
external site.)Links to an external site..
Coding Requirements
Specific requirements for the above classes are given in what follows.
It should also define a private int constant giving the number of cells on the
board. The most maintainable way to define this constant is by multiplying the
number of rows by the number of columns, using the above constants.
To represent the board configuration, it will need the following private fields:
A List<int>[ ] to store the pieces played on the board. Each list in this
array will represent a column on the board. The number of elements in the
list will be the number of pieces in the column. Thus, element 0 of a list will
store the piece in the bottom row of that column. Each element will be
either 1 or -1 to denote which player's piece occupies that cell.
An int to record the player whose turn it is to play.
A Stack<int> to store the history of plays so that they can be undone.
Each element in the stack is the column in which a play was made.
An int to keep track of the heuristic 2 score from the perspective of the
first player.
An int[ , ] field storing the value used by heuristic 2 for each cell. This type
is a 2-dimensional array - you can access elements of it by giving the row
and column as the two array indices, separated by a comma. For
example, if the array is called _cellValues, the element in row 1 and
column 3 is accessed by _cellValues[1, 3]. It can be initialized as
follows:
private int[,] _cellValues =
{3, 4, 5, 7, 5, 4, 3},
{3, 4, 5, 7, 5, 4, 3}
};
The remaining class members are described in what follows. For efficiency,
you should not do any explicit error checking on the parameters to these
methods.
A public constructor
This constructor needs no parameters. It will need to initialize each element of
the List<int>[ ] to a new List<int>.
The vertical and horizontal increments describe the vertical and horizontal
components of a direction, where -1 is down or left and 1 is up or right. Thus,
for example, a vertical increment of 0 and a horizontal increment of -1 would
describe the direction to the left, whereas a vertical increment of 1 and
a horizontal increment of 1 would describe the diagonal direction to the upper
right.
This method should return an int giving the number of consecutive pieces in
the given direction from the given row and column belong to the given player.
The cell at the given row and column should not be examined - its contents
are irrelevant. The consecutive pieces can be terminated by an edge of the
board, an unoccupied cell, or a cell occupied by the other player.
The value of each cell that would complete a win in the computation of
heuristic 1 (100).
The value of a win (1000000).
The identifier of the computer player. This will be either the value of the
constant defined in the Board class to represent the first player, or the
negative of this constant, depending on whether the computer player plays
first.
The difficulty level.
The Board representing the current game.
A public constructor
This constructor will need as its parameters values to assign to each of
the private fields.
It will need nested loops to compute heuristic 1. The outer loop will need to
iterate through the columns of the board. The inner loop will iterate through
unoccupied rows of a column. For efficiency, you should not examine any
rows that are more than 1 above the last occupied row in the current column
or any adjacent columns, as these cells cannot complete a win.
It should return an int giving the value of the current configuration represented
by the Board field, from the given player's perspective. It should implement
the negamax algorithm described in the section, "The Negamax Algorithm",
above. Recall that we are not using a tree data structure, but that the tree is
simply how we think about the relationships between various board
configurations. You should initialize the maximum to Int32.MinValue (Links to
an external site.)Links to an external site. in order to be sure you update the
best play to the maximum value. In order to match the behavior of the posted
executable, iterate through the columns from smallest index to largest, and
only update the maximum when you encounter a strictly larger value. You
may use the Board field to make the play being evaluated - just be sure to
undo that play when you have finished evaluating it, even if you are returning
from inside the loop.
Be careful in passing the out parameter to the recursive call. If you use the
same parameter that was passed in, the recursive call will change the best
play. You should therefore use a new variable. (Note that this method doesn't
actually use this value - only the method below will.)
A property that gets a bool indicating whether the user checked the
"Computer plays first" radio button.
A property that gets an int giving the level that the user chose. Note that
because the value of a NumericUpDown is a decimal, you will need to
cast it to an int (an int is more appropriate for the rest of the program).
The UserInterface Class
Besides the fields defined by the GUI construction, this class will need the
following private fields:
This class will also need the private methods and event handlers described in
what follows. In some of these methods, you will need to use
the Controls (Links to an external site.)Links to an external site. property of
one of the FlowLayoutPanels in order to access individual controls in that
panel. This property gives a collection of the controls contained in that panel.
You can index into this collection as you would an array; for example, if c is
the collection returned by the Controls property of the panel containing the
columns, then c[2] will give the column associated with the button labeled 2.
You can also use a foreach loop to iterate through the controls in this
collection. Finally, you can use the collection's Add (Links to an external
site.)Links to an external site. method to add a control to the panel.
A method to finish the game
This method should take as its only parameter a string giving the message to
show in the TextBox, and should return nothing. It should disable all the
buttons and place the given message in the TextBox.
You will need to construct a Label and set its Text property to the
given string. Because its font is determined by the FlowLayoutPanel that
you will add it to, you will need to set its AutoSize property to true. You can
use the Boardfield to determine whether the column is full.
A method to make the computer's play
The method should take no parameters and return nothing. It is responsible
for placing the message, "My move." in the TextBox, getting the computer's
play using the ComputerPlayer's public method, and using the above
method to display it on the form. Normally, updates to the form won't be
displayed until the event handler is finished. In order for the message in
the TextBox to be seen while the computer is deciding on its move, you will
need to override this behavior by calling the form's Update (Links to an
external site.)Links to an external site. method with no parameters before
getting the computer's play from the ComputerPlayer. This method
is inherited by the UserInterface class from the Form class; hence, you can
call it simply by
Update();
This method's first parameter will be the button that was clicked. In order to
treat it as a button, you will need to cast it to Button. You can then get
its Text property and convert it to an int in order to determine the column in
which the user played. It should record the given play on both the Board and
the form using the appropriate methods above, and if this play does not end
the game, make the computer's play using the above method. Finally, if this
does not end the game, update the TextBox to indicate that it is the user's
turn. Note that the method to determine whether the game is over will take
care of updating the form appropriately if the game is over.
If the user has chosen to play first, you will still need to construct
a ComputerPlayer for the private field.
Human: 3456
Computer: 3 3 3
Computer: 3 2 3 4 3 2 2 3 2 6 6 6 0 0 0 1
Human: 4443521326660001
Human 23224554356
Computer 3 3 3 2 4 4 4 3 5 6 6
Human 135334415551222566000
Comnputer 3 4 3 3 4 4 2 1 6 5 4 1 2 2 1 6 6 6 0 0 0
Result: The game is a draw.
You should also play a few moves at level 7 to compare your program's
performance with that of the posted executable. Yours should perform within a
factor of two of the posted executable.
Important: If the URL you submit does not contain the 40-hex-digit fingerprint
of the commit you want graded, you will receive a 0, as this fingerprint is the
only way we can verify that you completed your code prior to submitting your
assignment. We will only grade the source code that is included in the commit
that you submit. Therefore, be sure that the commit on GitHub contains
all seven ".cs" files, and that they are the version you want graded. This is
especially important if you had any trouble committing or pushing your code.