The development of software for two-player board games is one of the earliest applications of artificial intelligence. Versions of Tic-Tac-Toe, Checkers, and Chess were implemented on some of the first electronic computers. Modern implementations of most two-player games can now beat even the best human players, though remarkably, most computer players are built on the same core ideas as their earliest ancestors.
In this assignment, you will complete the core classes for an implementation of Milton-Bradley’s Connect 4. The original version of the game uses a set of 42 checkers-like pieces, 21 red and 21 black. The game board is a 6 row by 7-column board, oriented vertically. Players take turns dropping pieces into one of the columns, which falls straight down to the lowest unoccupied row. A player wins the game by completing a run of four consecutive pieces of their color, horizontally, vertically, or along either of the diagonals (top left or top right). If all rows and columns are filled without a win, the game ends in a tie.
In our version, Connect-N the number of rows and columns in the game board, along with the minimum length run required to win, are all user-configurable. And while we will not test you on this, it is possible to extend the game to have more than two players.
Just getting a game together that will work for human players can be a challenge, but the real fun is in the construction of an AI that can play the game with reasonable skill. Developing a tournament-strength computer player is beyond the scope of this class (though it is possible to make one that never loses), but even the simple approach you’ll use here can produce surpisingly good play.
This assignment includes material that we have not covered by the time the assignment was posted. However, if you follow the micro-deadlines, you will always have seen the material necessary to do the current task.
Authors: W. White, J. Lasseter
Learning Objectives
This assignment is designed to help you understand the following concepts.
- It gives you practice with writing your own classes.
- It gives you practice with writing code spread across many files.
- It illustrates the importance of class invariants.
- It gives you practice with using data encapsulation in classes.
- It gives you practice with both both 1-dimensional and 2-dimensional data.
- It gives you experience with structuring your code with helper functions.
- It gives you practice with code reuse through inheritance and the construction of subclasses.
- It gives you experience writing code for simple artificial intelligence.
Table of Contents
- Academic Integrity and Collaboration
- Connect-N
- Assignment Source Code
- Assignment Instructions
- Extra Credit
- Finishing Touches
Academic Integrity and Collaboration
This is a brand new assignment; you are the inaugural year. Hence, we are pretty confident that there are no solutions for this assignment available online. This means our concerns about academic integrity are limited to cases where students either violate the collaboration policy, or use AI code assistance. To prevent either of these, we will be using Moss to check for instances of plagiarism.
We also ask that you do not enable violations of academic policy. Do not post your code to Pastebin, GitHub, or any other publicly accessible site.
Collaboration Policy
You may do this assignment with one other person. If you are going to work together, form your group on CMS as soon as possible. If you do this assignment with another person, you must work together. It is against the rules for one person to do some programming on this assignment without the other person sitting nearby and helping.
With the exception of your CMS-registered partner, we ask that you do not look at anyone else’s code or show your code to anyone else (except a CS1110 staff member) in any form whatsoever. This includes posting your code on Ed Discussions to ask for help. It is okay to post error messages on online, but not code. If we need to see your code,we will ask for it.
Connect-N
The Game Rules
The game board is defined by three values:
- The number of rows (the height)
- The number of columns (the width)
- The number needed in a run (the streak)
As the game must allow for wins in all directions, the required streak can never be larger than the number of rows or the number of columns.
Players alternate turns, dropping pieces into a column. The piece will fall down to the first available row. This continues until one of the two players achieves a streak. If so, that player wins the game. If the board fills before that happens, the game ends in a tie.
By default the pieces in our game will have colors red and blue (not black). However, you can change them to any colors that you want once the assignment is done.
An Example Game
As a simple example, suppose we have a game with three rows and four columns. To win this game, we will require the player to make a streak of three (the largest possible streak on this board).
To start this game, the red player drops a piece in the first column. The piece falls to the bottom row.
The blue player responds by dropping a piece in the second column. This piece also falls to the bottom row.
The red player decided to play on top of the blue player by dropping a piece in the second column. This piece falls to the second row, as there is already a piece in the first row.
The blue player sees an opportunity to improve their run by dropping a piece in the third column.
The red player realizes that they need to block the blue player from winning. So they place a piece in the final column, stopping the streak from happening.
The blue decides to try for a vertical streak by placing a piece in the third column. As there is already a piece in the bottom row, this piece falls to the second row.
The red player also places a piece in the third column. This piece falls to the third row, as there are already two pieces in this column. Not only does this block the blue player from creating a streak, this actually creates a diagonal streak from the bottom left corner. The red player has now won the game.
Computer Players
Connect Four (and Connect-N in general) belongs to a family of turn-based games that include chess, checkers, tic-tac-toe, and a few others. These games all share some important characteristics.
No Information Hiding: At every turn, each player has complete knowledge about the entire state of the game. This is in contrast with many card games, including poker, bridge, and so on.
No Randomness: There are no components such as card shuffling, spinners, or dice that produce random values. At every turn, the available moves are completely determined by the moves that have already been played.
Small Number of Possible Moves: At every turn, there is only one move for each unfilled column. In standard Connect Four, this means at most 7 choices.
These facts mean that it is always possible, in principle, to write a computer program to find the best move. This leads to the concept of a rating function. This function evaluates a configuration of the game board and calculates how much value it has for the player who just had their turn.
Given this function, a computer player has a pretty straightforward algorithm to determine its move choice:
for every legal move: try playing the move calculate the value of the resulting board configuration pick the move that had the highest value
Rating Functions
The rating function takes a game board configuration and calculates how much advantage this gives to a player. In general, this calculation is done by identifying certain patterns in a board configuration that are known to give better or worse advantage. In the example below, we look at two possible moves for the blue player: one in column 1 and another in column 2 (remember that we start at 0).
Column 1 is not a great choice, as it only produces a run of length one. The choice of column 2 seems better, as it gives a run of three consecutive blue pieces. A simple rating function that measures the run length resulting from a choice would score the column 2 play above the column 1 choice.
Choosing the right patterns to look for can be pretty challenging, and the problem can become subtle very quickly. For example, an even better rating function would score a column 6 move as worse that either other two, even though it also has a run length of 3. The is because that run cannot possibly lead to a win in the example above.
As another example, consider this board where it is the blue player’s turn to chose a move.
The run length considerations for blue are the same the first example (i.e. column 2 produces a run of three, while column 1 only produces a run of length one). However, any play other than column 1 will allow red to win on their next move.
You will not consider the problem of predicting opponent’s responses in the code you write for this assignment, but incorporating opponent’s possible responses, the computer’s responses to those responses, and so on is what championship-level computer players are built to do. Though the rating function you will write is much simpler, you will have a chance to improve it for extra credit.
Assignment Source Code
Download the source code to get the source code for this assignment. As with previous assignments, you should unzip this file and put the contents in a new directory. This time you will see that the folder contains a lot of files.
You only need to pay attention to the files that start with a6
. There are five of
them. Two are completed and three are only stubs, waiting for you to complete them.
File | Description |
---|---|
a6player.py | The Player class, to be completed in Task 1, as well as
the AIPlayer class, to be completed in Task 5. |
a6board.py | The Board class, to be completed in Tasks 2 and 4. |
a6game.py | The Game class, to be completed in Task 3 |
a6consts.py | A module with several constants, which is completed already |
a6test.py | The test script for the assignment, which is completed already |
The Code Organization
Classes are ideal for breaking up a complicated application into parts. We think about the application that we want to create, and identify the individual elements. Each element will be its own type, with its own attributes and methods. In this application we have many types: the player, the game board, and the game referee who controls the rules.
While we talked about how to design classes in lecture, we have made all the hard decisions for you in this assignment. That is because it is not always easy to get it right the first time. We want you to spend your time implementing rather than designing your classes. You will get a chance to do that in the next assignment.
When we design our classes, we often like to organize them in a dependency diagram. A class A depends on a class B if objects of type A need to create and call methods in objects of type B. This notion of dependency tells us the order in which we need to complete the code. If class A depends on class B, then we need to finish implementing class B before we implement class A.
The dependency diagram for our application is shown below.
You will notice that everything depends on a6consts.py
. However, we have finished that
for you, so you do not need to worry about that. The classes Player
and Board
have
the same dependencies, so we could finish them in either order, as long as we finish them
first (though we will do Player
first as it is easier). Then we have class Game
which depends on both of them. Finally, AIPlayer
has the most dependencies, so we
have to do them last.
You will learn more about these classes in the assignment instructions. However, we give a brief tour of them here.
Class Player
Even for human players, there are a couple of essential attributes that we need to include.
Every instance of the Player
class has a player name, and a player color. The name
attribute is used to display the player’s name on the screen to indicate that it is
their turn (or if they won the game). The color attribute stores the color of the
player’s game pieces.
The player class does not do much else. As a human is responsible for deciding the moves of a player, there are no other computations necessary.
Class Board
The purpose of the board is to store the game pieces. A game piece is represented by the color name. So you can think of a board as a two-dimensional list of color names.
However, we make Board
a class because it needs to do so much more than a simple list.
It needs attributes for the size of the board, as well as the streak necessary to win
the game. More importantly, it has methods that restrict how players can add pieces to
the board, ensuring that everyone plays by the rules.
The class Board
is also where we include our methods for detecting which color has
won the game.
Class Game
While the class Board
implements a lot of the rules of the game, someone needs to
coordinate the players, making sure that they each take their proper turn. That is the
purpose of the class Game
. As we will learn later in the semester, this is what is
known as a controller class. It does not have a lot of methods, but it is the glue
that puts everything together.
Class AIPlayer
The class AIPlayer
is a subclass of Player
. As we said in class, it
inherits all of the attributes of Player
. It has no new attributes, and does not need
a new initializer. However, it does override the method chooseMove
which is used
to chose a piece on the board. Overriding this method is what will allow you to create
a computer opponent.
The Test Script
We have provided a test script for this assignment. As with Assignment 4, there is no need for you to add additional test cases to this script. You will not be graded on your tests. This assignment is complex enough as it is, so we want to give you all the help that we can. With that said, this test script is not guaranteed to be complete. Passing this script will not guarantee you a perfect on the assignment.
To run the test script, simply type
just like any other test script. However, there is one additional feature. At the top
of a6test.py
, there is a variable TASK_LEVEL
. You should assign this variable to
the current task that you are working on in the instructions.
This will help you focus on the code that is relevant for that task.
Alternatively, it is possible to set the TASK_LEVEL
on the command line, when you
run the script. For example, if you type
the script will run the tests for Task 3, no matter what the value of the TASK_LEVEL
variable is.
The GUI Application
Until you finish Task 3, the only way to test your code is with the test script. However, once you complete Task 3, you can actually play the game Connect-N. To do that, we have provided a new tool for you.
In the source code that you have downloaded, you will see another folder named connectn
.
This is actually a python application. Inside this folder you will see a bunch of python
files. You do not need to understand them, and we actually recommend that you do not try
to read them. They will just distract you from the assignment.
To run this application, you run it like a script. But you do not run an individual file. Instead, you run the entire folder, like this
When you do that, you will see a screen asking you to choose the players, like this:
The purpose of this user interface is to allow you to match up humans against humans, humans against AI and even AI players against each other. You can also reassign the color for each player. To do that, click and hold on the colored rectangle. Move the mouse to select a new color. When you release, that will be the color selected.
Once you have decided on the players, click the button Start Game. That will change the screen to look like this:
If you have finished Task 3, you can fill up the game board, but not actually play a game. If you have finished Task 4, two humans can play against each other by taking turns. Finally, if you have finished Task 5, you can use this application to play against a computer opponent. At any time you can use the button Reset to start a new game with these players.
By default, the game will have 6 rows, 7 columns, and require a streak of 4 to win. You can change all of these values and the command line, like this:
So to create a board with 3 rows, 4 columns, and win streak of 3, you would use the command
For this to work, the numbers must be valid. The numbers must be positive, and they cannot be greater than 20 (as that is all our graphics program will allow). In addition, the win streak cannot be greater than the minimum of the rows and the columns.
Assignment Instructions
There are two very important rules that you must obey throughout this assignment. The
first is that you must enforce all precondition. We have actually provided you with
several helper functions throughout the assignment to make this easy. In addition, like
Assignment 4, we do not require that you include any error messages. But if any
precondition is not enforced, you will lose points. Furthermore, when enforcing type
preconditions, we ask that you always use isinstance
instead of type
.
The other important rule is not an issue with Player
and Board
, but it is relevant
once you start working on Game
and AIPlayer
. That rule is that no class may access
the hidden attributes of another class (e.g. the attributes that start with an underscore).
We will talk about the reason for this in class. But it should never be an
issue. If you need to access something inside another class, you should call a method
and not access the attribute directly. A class can access its own hidden attributes,
but if one class accesses the hidden attributes of another, you will lose points.
Suggested Micro-Deadlines
This is a new assignment. We have tested it on the course staff and we are confident that you can complete it. But we also recognize that this is a long assignment. It is substantially more code than the previous programming assignment. If you wait until the last minute to work on this assignment, there is absolutely no way that you will complete it.
To prevent you from doing this, we have included a suggested completion date at the end of each task. These dates are determined by how complex we think each task is. While we do not enforce these dates, we recommend that you try to hit these deadlines. Keeping these deadlines makes sure that you are making slow steady progress. This will help you enjoy the assignment more, and help you retain all the lessons that you learn from this assignment (which will be part of the second exam).
If you cannot make these deadlines, it might be a sign that you are having difficulty and need some extra help. In that case, you should see a staff member as soon as possible.
Task 1: Implement the Player
class
The first thing you should do before working on this class is read the class invariant.
# HIDDEN ATTRIBUTES # Attribute _name: The player name # Invariant: _name is a nonempty string # # Attribute _color: The player color # Invariant: _color is a string representing a valid color (e.g. either # introcs.tk_color or introcs.web_color returns True).
Note that these attributes begin with an underscore. That means they can be accessed
inside the code for Player
, but nowhere else. Any other class (like Game
) can only
access these attributes through a getter or a setter.
This class has five methods: the initializer, two getters, one setter, and chooseMove
.
The specifications of all of these methods are included in the file a6player.py
. The
method chooseMove
has been completed for you and should not be modified. This method
will not be important until you complete
Task 3.
Among the other methods, the getters are the most straight-forward. Most of the time, you just need to return the attribute. The initializer is also very simple, because it mainly just enforces the preconditions.
However, the precondition for the attribute _color
are very interesting. It is not
enough for _color
to be a string. It also has to be a string for a valid color. You
saw these in Assignment 4. You could change the color of the turtle with color names
(e.g. 'red'
, or 'green'
) or web colors (e.g. '#7F00FF'
or 'ffa500'
). To verify
that a string is a valid color, there are two functions at your disposal:
introcs.is_tkcolor
and introcs.is_webcolor
. These functions both take a string and
return a boolean. These functions are actually used in your code for Assignment 4. You
might want to go back and look at how they are used in that assignment.
The final interesting method is the lone setter setName
. Read the specification
for this method carefully. It does something special if you pass it the empty string.
Testing it Out
To test this past of the assignment, open a6test.py
and make sure that your TASK_LEVEL
is set to 1. Or you can run the script using the command
If you pass all the tests, you are done with the task.
We highly recommend that you finish Player
by
Monday, October 30.
This is the Monday after the assignment was posted and should give you more than enough
time to read through the instructions. This is a very simple class and should not be
that much more difficult than what you have completed in lab already. This also ensures
that you have maximum time to work on this assignment.
Task 2: Implement a basic Board
class
The class invariants for the Board
class define the way that the five attributes must
relate to each other in order to give a consistent representation of a game board:
# HIDDEN ATTRIBUTES # Attribute _width: The number of columns in the game board # Invariant: _width is an int > 0 # # Attribute _height: The number of rows in the game board # Invariant: _height is an int > 0 # # Attribute: _streak: Minimum-length run of same color-pieces # needed to win # Invariant: _streak is an int > 0 and < min(_width,_height) # # Attribute _board: a 2d table storing the game pieces. # Invariant: _board is a 2d table of size _height by _width, containing # strings. These strings are empty or a valid color name # # Attribute _moves: The saved moves in the game # Invariant: _moves is a list of (int,int) tuples and has length # less than _width*_height. It's length is equal to the number # of non-empty places in the board.
One aspect of these invariants means that the first dimension of the 2D list _board
will store the row number, while the second dimension gives the column. This
convention is known as row-major order, and was discussed in class. While it is also
possible to represent tables using column-major order , we will be using row-major
order for this assignment.
Your job in this task is to create just enough of the Board
class that it can be used
to store pieces. This will involve a lot of methods, so we have broken this task into
smaller parts.
Part A: Getters and the Initializer
All of the attributes above are somewhat immutable. The first three attributes are
fixed by the initializer and cannot be changed. The other two attributes can only
be changed by the special methods place
and undoPlace
, which you will not work on
right now. This means that there are no traditional setters for any of these attributes.
However, all of the attributes do begin with an underscore. That means that, if we want
to access them in another class, we need to implement getters. And there are quite a
few of these: getWidth
, getHeight
, getStreak
, getColor
(which accesses the elements
of _board
), getMoveCount
and getLastMove
. As with most of getters, these can all
be implemented with a single line, with the exception for getColor
, which needs to
enforce its preconditions.
To test out the getters, you will also need to implement the initializer. This is where
things get a little bit interesting. You will notice a method below the initializer
called clear
. This method is used to reset the board. It creates a new 2d list for
_board
and a new empty list for _moves
. As you need to do this in the initializer
as well, there is no reason to write this code twice. So implement clear
before you
write the initializer, and call this method as a helper in the initializer.
When you create the board, there are no pieces in the board to start with. That means
it should be an nested list (of the appropriate size), where all of the entries are
empty strings. Because the empty string is going to be commonly used throughout the
assignment, we have provided the constant NOBODY
in a6conts.py
to represent this
value. We recommend that you use this in your implementation of clear
.
You will notice that both _board
and _moves
have some fairly sophisticated invariants.
However, you do not need to worry about those right now, since the board starts out
empty. You only need to worry about these once you start placing pieces.
Testing it Out
To test this past of the assignment, open a6test.py
and make sure that your TASK_LEVEL
is set to 2. Or you can run the script using the command
This will actually run tests for all of Task 2, so you will not pass them all. But as long as you pass the tests for Part A, you can be somewhat confident that you have finished this part of the assignment.
We recommend that you finish these first few methods of Board
by
Tuesday, October 31.
This part of the Board
class is fairly straight-forward and you should be able to finish
it in another day. The purpose of making steady progress is to by yourself time for the
more complex parts of the assignment.
Part B: Column Methods
When we drop a piece in the board, we need to know what rows in that column are occupied.
That is the purpose of findAvailableRow
. It returns the first empty row for that
column (or the board height if no row is empty). Remember that a position in the board
is empty if it is the empty string. If it contains a color name, it is not empty.
When computing this row, it is important that you understand that the attribute _board
is essentially “upside-down”. The first row of the table is the bottom row of the board,
and the last row of the table is the top row of the board. If you follow the specification
for findAvailableRow
exactly, this is what you are doing anyway.
There are also the methods isFullColumn
and isFullBoard
which are related (and can even
use findAvailableRow
as a helper). You should implement all of these methods according
to their specification.
Testing it Out
You do not need to make any additional changes to a6test.py
to test these methods.
They are already part of the current TASK_LEVEL
. If you pass the tests for Part B,
then you should be confident that you are doing well.
You should try to finish these new methods of Board
by
Wednesday, November 1.
These methods are a little more complicated than the getters and initializer, but they
are no more complicated than anything you have ever done in lab. So you should be able
to finish them in one more day.
Part C: Piece Placement
Now it is time to make things interesting. The only way that someone can add a piece to
the board is via the place
method. That is to make sure that pieces are added to the
correct row for the requested column. Note that this method returns -1 if the column is
full and a piece cannot be placed.
When you place a piece, you are treating the board as a two-dimensional table. That means the first row of the table is the first row of the board. But note that this means that the board is actually “upside-down”. On the screen, the first row of the board is the bottom row, while the last row is the top row of the board.
Obviously the method place
should assign a color name to the appropriate position. But
actually does more than that. When you place a piece, we want to be able to undo that move
with undoPlace
. This requires us to remember the last move. So when we place a piece
into the board at column c and row r, we add the tuple (r,c)
to _moves
.
Adding this feature this allows us to undo a move with undoPlace
. That method looks
at the last element of _moves
to get a position (r,c)
. It then removes the color at
that position. It also needs to remove this move from the list when done.
Testing it Out
When you complete these two methods, you should now be able to pass all of the tests for
Task 2. But if you want, you can also test the methods directly (and this is a good
idea if you are having trouble). We have implemented the method __str__
for you,
which converts the board into a string that you can print on the screen. This string
shows the contents of the board,
For example, start the python interactive shell and type the following:
>>> import a6board >>> b = a6board.Board(6,7,4) >>> print(b) [['','','','','','',''], ['','','','','','',''], ['','','','','','',''], ['','','','','','',''], ['','','','','','',''], ['','','','','','','']] >>> b.place(2,'red') 0 >>> b.place(3,'blue') 0 >>> b.place(2,'red') 1 >>> print(b) [[' ',' ',' ',' ',' ',' ',' '], [' ',' ',' ',' ',' ',' ',' '], [' ',' ',' ',' ',' ',' ',' '], [' ',' ',' ',' ',' ',' ',' '], [' ',' ','red ',' ',' ',' ',' '], [' ',' ','red ','blue',' ',' ',' ']] >>> b.undoPlace() >>> print(b) [[' ',' ',' ',' ',' ',' ',' '], [' ',' ',' ',' ',' ',' ',' '], [' ',' ',' ',' ',' ',' ',' '], [' ',' ',' ',' ',' ',' ',' '], [' ',' ',' ',' ',' ',' ',' '], [' ',' ','red ','blue',' ',' ',' ']]
As you can see, print the string puts the first row of the board at the bottom of the screen. This makes it easy to visualize the board while you play with the methods.
You should try to finish these last methods of Task 2 by
Friday, November 3.
This gives you two solid days to work on these methods. They are not long methods, but
you do need to wrap your head around the relationship between _moves
and _board
. Plus
giving you two days on this activity gives you a little breather in reward for the work
that you have done so far.
Task 3: Implement the Game
class
Considering its central importance in managing all of the other classes, the invariants
for Game
are refreshingly simple:
# HIDDEN ATTRIBUTES # Attribute _board: The game board # Invariant: _board is a Board object # # Attribute _players: The game players # Attribute _players: The game players # Invariant: _players is a (possibly empty) list of Player objects, each # with distinct colors # # Attribute _current: The index of the current player # Invariant: _current is an int >= 0 and < len(_players), or -1 if _players # is empty # # Attribute _winner: The (color of) the winner of the game # Invariant: _winner is either None (no winner) or a color of one of the # Players in _players
The one attribute that is a little unusual is _current
. The attribute _players
is a
list of players. But only one of those players is active at a time. The other players
wait their turn until the active, or current, player makes a move. The attribute
_current
is an index into the list _players
that tracks which of these players is
the current or active one. Also note that _players
can be empty. When that happens
(and only when that happens), _current
is -1.
Part A: Getters and the Initializer
There several getters in this class: getBoard
, getPlayers
, getCurrent
, getWinner
.
Most of these are straight-forward. The only unusual one is getCurrent
. Despite its
name, it does not return a number. It returns either a Player object, or None.
The list of players is the only mutable attribute, so we have two methods that look like
setters: addPlayer
and clearPlayers
. When you implement these functions, you need
to pay close attention to the invariant. In particular, look at the preconditions for
addPlayer
. It is not enough to check that the argument is a Player object. You also
have to check that the color of the player does not match that of any existing players.
Fortunately, we have provided a function called is_new_player
at the bottom of the
file to make this easy.
Once you have implemented addPlayer
, the initializer is very simple. It creates
the board and an empty list of players. It also initializes all other attributes
to their defaults (following the class invariant).
Testing it Out
To test this past of the assignment, open a6test.py
and make sure that your TASK_LEVEL
is set to 3. Alternatively, you can run the script using the command
Once again, this will actually run tests for all of Task 3, so you will not pass them all. But passing all the tests for Part A is a good sign.
A previous version of the test script would fail, because it was
testing advance
, a method in Part B. A new version of the test script was uploaded
on November 7 to fix this problem.
You should try to finish these first few methods of class Game
by
Saturday, November 4.
The initializer is fairly simple, and you now have a lot of experience with these.
The only complex method is addPlayer
and we have provided a helper to make this simple
All of this should only take you one day.
Part B: The Game Loop
It is now time to run the game. For that you will need to implement three methods:
advance
, takeTurn
and run
. The method takeTurn
is simple. You get the current
player’s move using the method chooseMove
and place it in the board. The method
advance
is used to move to the next player. It does this by updating _current
.
When _current
reaches the end of the list, it loops back to the beginning.
The method run
is a while loop that continues until the game is done. It takes the
turn for the current player and then advances to the next player if the game is not over.
The tricky part is determining when the game is over. You will not be able to do this
until you complete Task 4. So for now, you are going to take a shortcut: continue
until the board is full (e.g. there is a tie).
Note that run
must be prepared for the case where there are no players. If that
is the case, this method does not do anything. There is also the case where takeTurn
might fail to choose a column. But that can only happen when the board is full, so you
do not need to add any new code to handle this case.
Testing it Out
It is still helpful to use the test script a6test.py
to check this portion of your
code. But at this point you can do something even more interesting. It is now possible
to run the GUI application. The method run
was the last
method that this application needs to work properly.
Run the GUI application and make sure that you have two human players. You can now take turns dropping pieces between the two players. The game will not detect a winner, but it will stop when the board is full. Try testing your game on boards of different sizes.
It is also possible to test out a game with more than two players, if you want to see what happens in that case. The selection screen adds those two players to the game, but it does not remove any existing players. So if you start with some players in the initializer for Game (as opposed to an empty list of players), those players will be included as well. However, if you do this remember to remove those players before submission, as we are expecting Game to start with no players when we grade your submission.
You should try to finish these first few methods of class Game
by
Sunday, November 5.
This is the last straight-forward code in this assignment, as everything past this point
is a little more complex. Finishing on time will bank enough time for you to finish the
whole assignment with ease.
Task 4: Complete the full Board
class
By now, you have most of the application completed. In fact, if your Board
class could
successfully report winning runs of pieces, you would have a fully functional game for
two human players. Your job in this task is to make that happen. Specifically, you
should implement the remaining stub methods: findAcross
, findNWSE
, findSWNE
, and
findWins
.
These four methods are likely to be more challenging, and so we are going to break it up
into two parts. But before we do that, it is first worth thinking about the method
findWins
. This method is going to use all of the other methods as helpers.
The parameters r
and c
in this function is an important clue to taming this otherwise
difficult problem. If playing a move results in a win, all winning streaks will include
the piece that was just played. This significantly reduces the number of ways we need
to check the game board. Instead of looking at every position, we can start from the
location (r,c)
and look in the four possible directions that a win could occur.
For example, consider the board depicted below:
In this game, blue is about to play. If the fifth column is chosen, the piece will land
in position _board[3][4]
. Blue wins with this move, and we can find it by looking only
along the four directions that include (3,4)
.
To keep these methods separate from those in Task 2, we have started naming the parts below with Part D.
Part D: The Method findAcross
You should first start by implementing the function findAcross
. Once you understand
how this method works all of the other methods will follow quickly after. Fortunately,
we have given you a very big hint. If you look, you will notice that we have completely
implemented findVertical
for you. Though the other methods have to deal other
directions, an understanding of how findVertical
works should give you the necessary
insights for the others. In particular, you will notice that findVertical
- Starts at the initial row and column
- Searches downwards until it finds a different color, or it goes past the edge
- Backs up one, because we want the location of the last piece that is the same color
The method findAcross
is similar with two differences
- You are searching horizontally instead of vertically
- You need to search in two directions, not just one
The latter is because it is possible to create a winning streak by dropping a piece in the middle of a run. In the picture below, red can win by dropping a piece in column 1.
But this is easier than you think. Simply break the problem into two for-loops, one going left and the other going right.
Testing it Out
When you set TASK_LEVEL
in a6test.py
to 4, you activate a second set of test cases
for Board
to validate these new methods. Alternatively, you can access these tests
by running the script as
As usual, the test script will attempt to validate all of the methods in Task 4, not just Part A. But right now you just need to worry about passing the Part D tests.
You should try to finish the method findAcross
by
Monday, November 6.
This is just one method, so you should be able to do that. More importantly, once you
do that, you have everything you need for the next part.
Part E: Detecting Wins
At this point you should understand how findAcross
works. It is now time to work on
findSWNE
and findNWSE
. To understand these methods, look at the illustration below.
In this illustration we have our starting point (r,c)
show all of our runs horizontally
and diagonally. The horizontal run goes from (r1,c1)
to (r1,c2)
. The run for
findSWNE
goes from (r5,c5)
to (r4,c4)
. Similarly, the run for findNWSE
goes from
(r3,c3)
to (r6,c6)
. With that information you should be able to define those two
methods.
To implement findWins
, you simply need to use all of these functions as helpers. In
particular, findWins
searches for a winning streak in the following order:
- vertically
- horizontally
- diagonally from SW to NE
- diagonally from NW to SE
The method returns the first win that at finds.
Once you have finished that method, there is one more thing to do. You want to update
the Game
class to use these new methods. In particular, you have to modify the
run
method so that its while-loop ends properly. Before, you were stopping the loop
when the board fills. Now you should stop the loop when someone wins the game. Also,
when that happens, you should set the _winner
attribute to indicate who one. But
be careful! Ties are still possible and you need to be prepared for them.
Testing it Out
The remainder of the tests in a6test.py
will help verify that you have completed this
task. However, the code is now getting complicated enough that we are not going to
guarantee that those tests are anywhere near complete. Our tests miss a lot of places
where you could make a mistake.
Fortunately, there is a better way to test! You can now play a real game. Run connectn
and try a game between two human players. Play against yourself or another person. Try
to win in all sorts of different ways and make sure that your code works correctly.
This part of the code is a major step and the first real hurdle for the assignment. Therefore, we are suggesting you take two days to get it right, finishing by Wednesday, November 8. If you can do that, the end of the assignment is in sight.
Task 5: Implement the AIPlayer
class
For the final task of this assignment, you will implement the AIPlayer
class. Once
you have done this, you will have an application that allows you to play Connect-N with
two human players, a human versus an AI Player, or even two AIs against each other. This
is the most challenging part of the assignment, but also the most interesting.
Since AIPlayer
is a subclass of Player
, it inherits all of its attributes. The class
invariants for those attributes will apply in AIPlayer
as well. Since it has no
additional attributes of its own, these are the only invariants to maintain.
Your job here is to override the chooseMove
method, together with the five hidden
methods in the AIPlayer
class. Unlike the Player
version of chooseMove
, this one
takes a bit more work.
Part A: The Rating Function
As we described above, the rating function are the key to implementing an AI for the game. We need some way to give a score to a move. In our implementation, the AI player is going to chose a move that produces the longest possible run (i.e. it will try to get as close as possible to a streak).
To do that we first need two methods. The _score
method takes a run (e.g. the result
of one of the find
methods in Task 4) and computes its length. For the most part this
is simple as you can rely on the dist
function that we have provided. However,
remember that you do need to enforce all preconditions as well. The helper function
is_valid_run
at the bottom of a6player.py
is useful here.
The real work is done by _evaluate
, which is the actual rating function. This function
looks for the longest run in all directions from (r,c)
and returns the length of that
run. To implement this, you will use the find
methods from Task 4. But those methods
want a minimum length, and we don’t know what our minimum length should be yet (as we
are just looking for runs, not streaks). So you will need to loop over all possible
minimum lengths up the streak size, which is the maximum.
There are two special cases to be aware of for the method _evaluate
. If a move is a win,
you should give it the maximum possible score. That is the value SCORE_WIN
defined
in a6consts.py
. Also, if you can find no runs, you should return SCORE_BAD
, which
is the minimum possible score defined in a6consts.py
.
For example, given the board configuration below, if blue is the next palyer, the
seven possible moves (from left to right) should have scores of 2, 2, 2, 2, SCORE_WIN
,
SCORE_BAD
, and SCORE_BAD
.
For the board at below, the scores should be SCORE_BAD
, SCORE_BAD
, 3, 2, 2, 2, and 3.
Testing it Out
There is one last TASK_LEVEL
for the script a6test.py
, and that is level 5. You can
run that last sequence of tests by either setting that variable to 5, or by running the
script as
Once again, this will include all the tests for Task 5, but you just need to pay attention to the tests for Part A. While these tests are not guaranteed to be complete, they are good at catching common errors.
You should try to finish these methods by
Thursday, November 9.
Only the _evaluate
method is complicated, and that is just putting together functions
that you have previously defined as helpers.
Part B: Move Evaluation
It is now time to evaluate possible moves. The method _evaluate
is capable of scoring
a move after it is made. But we want to compute a score before. How do we do this?
This was the purpose of the undoPlace
method that we implemented in Task 2. The
AIPlayer can place a piece, score the move, and then undo the placement, making as if
it never happened. If the AI player does that for every column, computing a score each
time, they can determine the best move.
We break this process up into three methods: _gatherMoves
, _evaluateMoves
and
_findBestMoves
. The first method creates a dictionary of all possible moves. The keys
are columns and the values are the score. For this method, we assume that all scores
are SCORE_BAD
. Why a dictionary? Because not all columns are valid moves. Some
columns are full. We do not want a column to be a key in the dictionary if it is full.
The method _evaluateMoves
takes this dictionary and computes a score (using
_evaluate
) for each key in the dictionary. That means this is the method responsible
for making a move, scoring it, and then undoing the move. In addition, you have to
enforce the precondition for the dictionary. Fortunately, we have provided a function
is_valid_dict
to make that part easy.
Finally, the method _findBestMoves
choses the best move. It loops over the dictionary
and picks the column with the highest score. If there is a tie, it returns all of the
best moves as a list.
Testing it Out
You still have not quite implemented enough to play against an AI player, so you still
need to rely on a6test.py
to do your testing for this part. And while these tests
are not complete, they will get you far enough to do some testing in the next part.
These methods are the last major hurdle for the assignment, so we are going to give you two days to complete them. You should finish them by Saturday, November 11. If you are on target with this deadline, you will have no problem completing this assignment.
Part C: Move Selection
Now that you have determined the best move, it is time to carry it out. You need to
override chooseMove
in AIPlayer
. This method will find the all of the best moves.
If there is more than one, it will pick one at random using the random.choice
. It
will then execute the move. To execute the move, it will call the function set_choice
in module connectn
. This function takes a color name and a column as an argument.
It communicates to the UI where to put the piece.
Of course, now that you have a working AI player, you want to test it out. But fortunately,
since AIPlayer
is a subclass of Player
, you do not need to make changes to you code
to get this to happen. Simply start up connectn
and select the AI player as one of the
two players. These two players have the same methods, so the run
method still works
as normal. This is the power of subclasses.
Testing it Out
At this stage you should be able to run the script a6test.py
with no errors. But that
is not the point! It is time to play Connect-N against the computer. Start up the GUI
application and make one of the players an AI player.
But we should warn you about the AI player. It makes pretty terrible choices. It will find a winning move if one exists, but the simplistic “move with longest run” approach will often fail against a skilled player. The examples given in the discussion of rating functions illustrate this.
We should also warn you about a very common error that we have seen in testing. Sometimes
you have made it this far, but the game seems to hang once it reaches the AI player’s
turn. That generally means you forgot to call the set_choice
function defined in
connectn
. You have to do that, or the GUI will not know what choice the AI player
made.
If you have been following all of the recommended deadlines so far, you will finish this part by Sunday, November 12. That will give you two more days (yes, we extended the deadline) to fully test everything and clean-up your code before submission. And if you are ahead of schedule, you can even think about working on extra credit.
Extra Credit
You have gotten a taste of how to create an AI player. But it is pretty crappy. Do you have ideas on how to improve it? For example, can you create an AI player that can prevent a loss to blue in the example below?
Can you create an AI that realizes a run of almost winning length is useless if there is no possibility of extending it? Or one that can spot a move that creates a double threat (i.e. a run one short of winning with usable empty spots on either end)? Are there other strategies that we have not mentioned?
If you have ideas on how to improve the AI, you should complete the class BetterAIPlayer
,
which is provided for you as a stub in a6player.py
. We will award extra credit for this.
However, be warned that we are not going to award more than 4 points, and most people
who complete this will only get two points. This is meant to be a challenge for people
that aced the rest of the assignment, not a way to boost your grade. So do not waste
your time implementing a min-max strategy if
your base implementation is not solid.
You will notice that the stub for BetterAIPlayer
is a subclass of AIPlayer
.
That way, allows it inherit all of the methods of that class. You do not want to have
to reimplement all of the methods from AIPlayer
. Indeed, all you really need to do
in BetterAIPlayer
is override the rating function _evaluate
, as well as the method
_evaluateMoves
, which choses which moves to evaluate. You can add new methods if you
want, but these two methods are what you should focus on to improve the player.
We do not have any tests in a6test.py
to help you with this part of the assignment.
But you can test your new player in the GUI application. Simply check Better AI player
as one of the opponents, and watch it play.
Finishing Touches
Once you have everything working you should go back and make sure that your program meets the class coding conventions. In particular, you should check that the following are all true:
- You have indented with spaces, not tabs (Pulsar handles this automatically).
- Functions are each separated by two blank lines.
- Lines are short enough (~80 characters) that horizontal scrolling is not necessary.
- Docstrings are only used for specifications, not general comments.
- Your name(s) and netid(s) are in the comments at the top of the modules.
You should also make sure that the Game class starts with an empty list of players, as you might have changed that during testing.
We only want you to upload the three files you modified: a6game.py
, a6board.py
, and
a6player.py
. We do not want any other files, even if you added more tests to a6test.py
.
Upload these files to CMS by the due date:
Tuesday, November 14.
Completing the Survey
One last time, we need you to do a survey. The survey should be done individually (even if you worked in a group). As always, the survey will ask about things such as how long you spent on the assignment and your impression of the difficulty. Please try to complete the survey within a day of turning in this assignment. Remember that participation in surveys comprises 1% of your final grade.