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 .

The Player class -- a preview

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.

The Player class

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

Lab 6

Homework 6