Homework 6, Problem 3: The Connect
Four Player class
[30 extra
points; individual or pair]
Submission: Submit your hw6pr3.py file to
Canvas
You should copy
your hw6pr2.py file
to hw6pr3.py .
This problem asks you to write another
connect-four-related class named Player.
You should write this new class in a copy of your hw6pr2.py file -- name the new copy hw6pr3.py.
In this problem, you will need to create a class named Player that evaluates connect-four boards and
decides where to move next. The basic approach is the following:
The more detailed descriptions below will provide a
skeleton and a couple of hints for the design of your Player class and how to test it.
Your Player class should have at least
these three data members:
A one-character string representing the checker, either 'X' or 'O', being used by the
connect-four Player. Warning!:
remember that 'O' is capital-o, not zero.
One reasonable name for this data member might be self.ox .
A string, either 'LEFT', 'RIGHT', or 'RANDOM', representing the tiebreaking
type of the player. This is the name for one of the three strategies
described above. One reasonable name for this data member might be self.tbt (for tiebreaking type).
A nonnegative integer representing how many moves into
the future the player will look in order to evaluate possible moves. One
reasonable name for this data member might be self.ply because
one turn of gameplay is sometimes called a ply.
Methods required for the Player class
:
You should provide your Player class
with write the following methods. Be sure to try the hints on how to test each
one after writing it!
__init__
__repr__
class Player:
""" an AI player for Connect
Four """
def __init__( self, ox, tbt, ply ):
""" the
constructor """
self.ox = ox
self.tbt = tbt
self.ply = ply
def __repr__( self ):
""" creates
an appropriate string """
s = "Player for "
+ self.ox + "\n"
s += " with tiebreak type: " + self.tbt +
"\n"
s += " and ply == " + str(self.ply) +
"\n\n"
return s
Testing __init__ and __repr__:
>>> p = Player('X', 'LEFT', 2)
>>> p
Player for X
with tiebreak: LEFT
and ply == 2
>>> p = Player('O', 'RANDOM', 0)
>>> p
Player for O
with tiebreak: RANDOM
and ply == 0
Admittedly, testing at this point is mostly to
familiarize yourself with objects of type Player.
oppCh
>>> p = Player('X', 'LEFT', 3)
>>> p.oppCh()
'O'
>>> Player('O', 'LEFT', 0).oppCh()
'X'
scoreBoard
Testing scoreBoard
You should test all three possible output scores -- here is an example of how
to test the first case. For easy copy-and-paste, many statements are in one
large line:
>>> b = Board(7,6)
>>> b.setBoard( '01020305' )
>>> b
| | | | | | | |
| | | | | | | |
|X| | | | | | |
|X| | | | | | |
|X| | | | | | |
|X|O|O|O| |O| |
---------------
0 1 2 3 4 5 6
>>> p = Player( 'X', 'LEFT', 0 )
>>> p.scoreBoard(b)
100.0
>>> Player('O', 'LEFT', 0).scoreBoard(b)
0.0
>>> Player('O', 'LEFT', 0).scoreBoard( Board(7,6) )
50.0
The last two examples are using objects that have not
yet been assigned variable names.
Note that the tiebreak type does not affect this method at all. You can save
time by having scoreBoard use the winsFor method. Recall that winsFor is in the Board class,
however.
tiebreakMove
Testing tiebreakMove
You should test for all three tiebreaking types. Here are two tests:
>>> scores = [0, 0, 50, 0, 50, 50, 0]
>>> p = Player('X', 'LEFT', 1)
>>> p2 = Player('X', 'RIGHT', 1)
>>> p.tiebreakMove(scores)
2
>>> p2.tiebreakMove(scores)
5
A hint on tiebreakMove: it's helpful to find the
max of the list first (with Python's built-in max function)
and then search for the column in which the max is located -- starting from
an appropriate initial position -- and the self.tbt string
is what determines the appropriate initial position from which to start that
search.
scoresFor
Admittedly, that is a lot to handle! So, here is a
breakdown:
(Below is just one way to
solve the problem. You do NOT have to follow it! One thing is for sure: you should use recursion.)
First, this method creates a list of all zeros (I
called it scores)
with length equal to the number of columns in the board b.
Remember that you can use list multiplication: [0]*b.width.
Then, this method loops over all possible columns.
Base Case
If a particular column is full, it assigns a -1.0 score for that column.
Another Base Case
Next, if the object's ply is 0, no move is made. What's more, the column, if
not full, is evaluated for the self player. (Which method in
the Player class will do this?) When self.ply is 0, this means that all of the non-full
columns will have the same score. After all, this is to be expected if the
player is not looking at all into the future.
And Another Base Case
If the game is already over, then there's no point in making any additional
moves (indeed, it's not allowed!) -- simply evaluate the board and use that
score for the current column under consideration.
Recursive Case
But, if the object's ply is greater than 0 and the game isn't over, the
code should make a move into the column that is under consideration.
This will use some of the methods in Board, such as addMove.
In this case, it first checks if making that one move
-- the first one into the current column under consideration -- will win the
game for self. If so, that column should
score 100.0! Similarly, if it does not win, but does fill up the board, it
scores 50.0.
If it's not a win or a tie, however, the code next
figures out what scores an opponent would give
the resulting board. This means creating an opponent (which will
be of Player class!) You should assume
that this to-be-constructed opponent player has the same tiebreaking-style as
you do -- that is, as self does. The scores reported
by the opponent are NOT the scores that you should use. Rather, you will want
to compute self's evaluation of the board
based on the list of opponent's scores. Then, assign your score to the value of
the current column's move.
Be sure to delete the checker that had
been placed throughout the evaluation of this particular column.
Once all of the possible moves have been evaluated, the scoresFor method should return the complete list of
scores, one per column. Typically there will be seven numbers in the list
returned.
Testing scoresFor
Here is a case that will test almost all of your scoresFor method,
the commands to set up the board have been placed on one line for easy copy-and-paste
into your Python window:
>>> b = Board(7,6)
>>> b.setBoard( '1211244445' )
>>> b
| | | | | | | |
| | | | | | | |
| | | | |X| | |
| |O| | |O| | |
| |X|X| |X| | |
| |X|O| |O|O| |
---------------
0 1 2 3 4 5 6
# 0-ply lookahead doesn't see threats...
>>> Player('X', 'LEFT', 0).scoresFor(b)
[50.0, 50.0, 50.0, 50.0, 50.0, 50.0, 50.0]
# 1-ply lookahead sees immediate wins
# (if only it were 'O's turn!)
>>> Player('O', 'LEFT', 1).scoresFor(b)
[50.0, 50.0, 50.0, 100.0, 50.0, 50.0, 50.0]
# 2-ply lookahead sees possible losses
# ('X' better go to column 3...)
>>> Player('X', 'LEFT', 2).scoresFor(b)
[0.0, 0.0, 0.0, 50.0, 0.0, 0.0, 0.0]
# 3-ply lookahead sees set-up wins
# ('X' sees that col 3 is a win!)
>>> Player('X', 'LEFT', 3).scoresFor(b)
[0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0.0]
# At 3-ply, 'O' does not see any danger
# if it moves to columns on either side...
>>> Player('O', 'LEFT', 3).scoresFor(b)
[50.0, 50.0, 50.0, 100.0, 50.0, 50.0, 50.0]
# But at 4-ply, 'O' does see the danger!
# again, too bad it's not 'O's turn...
>>> Player('O', 'LEFT', 4).scoresFor(b)
[0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0.0]
This last test may take a few seconds, even on a fast
computer... .
nextMove
Testing nextMove
This is similar to the previous example; again, the <<< prompts have been removed at the top for
easy copy-and-paste into your Python window:
>>> b = Board(7,6);
>>> b.setBoard( '1211244445' )
>>> b
| | | | | | | |
| | | | | | | |
| | | | |X| | |
| |O| | |O| | |
| |X|X| |X| | |
| |X|O| |O|O| |
---------------
0 1 2 3 4 5 6
>>> Player('X', 'LEFT', 1).nextMove(b)
0
>>> Player('X', 'RIGHT', 1).nextMove(b)
6
>>> Player('X', 'LEFT', 2).nextMove(b)
3
# the tiebreak does not matter
# if there is only one best move...
>>> Player('X', 'RIGHT', 2).nextMove(b)
3
# again, the tiebreak does not matter
# if there is only one best move...
>>> Player('X', 'RANDOM', 2).nextMove(b)
3
Putting
it all together: Board's playGame method
playGame
Add the following method to
your Board class in hw6pr3.py ! (Do not change hw6pr2.py.)
One additional twist: you should handle the case in
which either px or po is the string 'human' instead of an object of type Player. In this 'human' case, the playGame method
should simply puase and ask the user to input the next column to move for that
player, with error-checking just as in hostGame.
See below for some determininstic -- that is,
non-random -- examples that you can use to test your playGame method:
Testing playGame
Some deterministic examples:
>>> px = Player('X', 'LEFT', 0)
>>> po = Player('O', 'LEFT', 0)
>>> b = Board(7,6)
>>> b.playGame(px, po)
# Lots of boards omitted...
|O|O|O| | | | |
|X|X|X| | | | |
|O|O|O| | | | |
|X|X|X| | | | |
|O|O|O| | | | |
|X|X|X|X| | | |
---------------
0 1 2 3 4 5 6
X wins!
Example #2 (notice the game ends faster!):
>>> px = Player('X', 'LEFT', 1)
>>> po = Player('O', 'LEFT', 1)
>>> b = Board(7,6)
>>> b.playGame(px, po)
# Lots of boards omitted...
|O|O| | | | | |
|X|X| | | | | |
|O|O| | | | | |
|X|X| | | | | |
|O|O|O| | | | |
|X|X|X|X| | | |
---------------
0 1 2 3 4 5 6
X wins!
If you have gotten to
this point, you have completed problem 3! You should submit your hw6pr3.py
file at Canvas .
Links