Homework 6,
Problem 3:
The
Connect
Four Player
class
Submission: Submit your hw6pr3.py file
to the TA's email address
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
the
TA's email address.