Assignment 4 - Tic Tac Toe
Due: Thursday, April 18, 2024, at 10pm
You may work alone or with a partner, but you must type up the code yourself. You may also discuss the assignment at a high level with other students. You should list any student with whom you discussed each part, and the manner of discussion (high-level, partner, etc.) in a comment at the top of each file. You should only have one partner for an entire assignment.
You will do all of your work in a single file, tictactoe.py
. You should download this “skeleton” version, and save it as tictactoe.py
in a folder that also has graphics.py
.
You should submit your assignment as a tictactoe.py
file on Moodle.
Parts of this assignment:
- Part 1: Keeping track of game state
- Part 2: Placing pieces
- Part 3: Checking for valid positions
- Part 4: Ending the game
Comments and collaboration
As with all assignments in this course, for each file in this assignment, you are expected to provide top-level comments (lines that start with #
at the top of the file) with your name and a collaboration statement. For this assignment, you have multiple programs; each needs a similar prelude.
You need a collaboration statement, even if just to say that you worked alone.
Note on style:
The following style guidelines are expected moving forward, and will typically constitute 5-10 points of each assignment (out of 100 points).
- Variable names should be clear and easy to understand, should not start with a capital letter, and should only be a single letter when appropriate (usually for
i
,j
, andk
as indices, potentially forx
andy
as coordinates, and maybep
as a point,c
for a circle,r
for a rectangle, etc.). - It’s good to use empty lines to break code into logical chunks.
- Comments should be used for anything complex, and typically for chunks of 3-5 lines of code, but not every line.
- Don’t leave extra print statements in the code, even if you left them commented out.
- Make sure not to have code that computes the right answer by doing extra work (e.g., leaving a computation in a for loop when it could have occurred after the for loop, only once).
Note: The example triangle-drawing program on page 108 of the textbook demonstrates a great use of empty lines and comments, and has very clear variable names. It is a good model to follow for style.
Part 1: Keeping track of game state
# You should be fully equipped to complete this part after Lesson 9 (Friday Apr. 12).
As you look through the code, note that anything with a # TODO
is something you’ll need to complete. If you have questions about how all of these functions work together, just ask!
For this assignment, we will represent the 9 grid locations as a nested list, called boardState
. This list contains three lists, one for each row. Each inner list contains three elements, one for each position within the row. These elements are either 0 (no marker placed), 1 (player 1 placed an “x”), or 2 (player 2 placed an “o”).
Here is an example:
# A board where there is:
# - an "o" in the upper left grid cell,
# - an "x" and an "o" in the middle row, and
# - an "x" in the middle of the bottom row.
[[2, 0, 0],
[1, 2, 0],
[0, 1, 0]]
For this part, you will implement the functions needed by the printBoardState
function. This function is useful for getting a textual representation of the game, to make sure it matches what is displayed in the game window.
The printBoardState
function is already written for you.
def printBoardState(boardState):
"""
Prints out the current board state for debugging.
boardState: a nested list containing the board state
"""
print()
# Print top row
printRow(boardState, 0)
# Divider line
print("-----")
# Print middle row
printRow(boardState, 1)
# Divider line
print("-----")
# Print bottom row
printRow(boardState, 2)
You should fill in the functions printRow
and getCellString
.
def getCellString(gridValue):
"""
Returns a string corresponding to the provided value from the board state.
gridValue: 0 (none) or 1 (player 1) or 2 (player 2)
returns: " " (0) or "x" (player 1) or "o" (player 2)
"""
# TODO: Part 1
return "" # replace with your code
def printRow(boardState, row):
"""
Prints out the current board state of the given row.
boardState: a nested list containing the board state
row: the row for which to print the board state
"""
# TODO: Part 1
pass # replace with your code
For printRow
, it’s a matter of iterating through the list and generating the appropriate characters for each piece (or empty spot). You may find the string function join
helpful: documentation.
You can test your functions using testPrintBoardState()
. By default, this is the only code not commented-out in the if __name__ == "__main__":
if statement, so it is all that will run.
if __name__ == "__main__":
# Part 1
testPrintBoardState()
# Part 2
# testDrawPlayerMarker()
# Part 3
# testPlacingValidMarkers()
# Part 4
# playGame()
By default, it tests the example above. You can change this if you want to test other configurations. Here is the example testing results (your code should give the same output):
Testing getCellString(): passed
Testing printBoardState() now.
The boardState list is: [[2, 0, 0], [1, 2, 0], [0, 1, 0]]
o| |
-----
x|o|
-----
|x|
The expected board state is:
o| |
-----
x|o|
-----
|x|
Part 2: Placing pieces
# You should be fully equipped to complete this part after Lesson 9 (Friday Apr. 12).
For this first part, you will provide the functionality needed by the following function:
def drawPlayerMarker(win, gridX, gridY, player):
"""
Draws a player marker (X for player 1, O for player 2)
at the specified grid position.
win: the GraphWin for the board
gridX: x-coordinate of grid cell (column)
gridY: y-coordinate of grid cell (row)
player: 1 or 2
"""
if player == 1:
drawX(win, gridX, gridY)
else: # must be 2
drawO(win, gridX, gridY)
This function should draw a player marker in the grid cell (gridX, gridY)
. The win
parameter is an instance of the GraphWin
class, and player
is either 1 or 2.
To provide this functionality, you should implement the following two functions:
def drawX(win, gridX, gridY):
"""
Draws an X at the specified grid position.
win: the GraphWin for the board
gridX: x-coordinate of grid cell (column)
gridY: y-coordinate of grid cell (row)
"""
# TODO: Part 2
pass # replace with your code
def drawO(win, gridX, gridY):
"""
Draws an O at the specified grid position.
win: the GraphWin for the board
gridX: x-coordinate of grid cell (column)
gridY: y-coordinate of grid cell (row)
"""
# TODO: Part 2
pass # replace with your code
-
In
drawX
, your code should draw an “x” in that cell. Think about how you can use objects ingraphics.py
to make an “x” shape. -
In
drawO
, your code should draw an “o” in that cell.
You can test your code for this part using the testDrawPlayerMarker
function. Just comment-out the call to testPrintBoardState()
and comment-in the call to testDrawPlayerMarker()
:
if __name__ == "__main__":
# Part 1
# testPrintBoardState()
# Part 2
testDrawPlayerMarker()
# Part 3
# testPlacingValidMarkers()
# Part 4
# playGame()
You should be able to place five markers within the grid. Note that so far, there is no code to make sure that the user can’t place a marker in an occupied space – that comes later.
Part 3: Checking for valid positions
# You should be fully equipped to complete this part after Lesson 10 (Monday Apr. 15).
Your code from Part 2 just blindly drew player markers on the board, even if there was already a marker in a given position. Now, you will fill in the isValidGridCell
and updateBoardState
functions.
def isValidGridCell(boardState, gridX, gridY):
"""
Returns a Boolean indicating whether the given grid position
is a valid selection given the current board state.
Also checks if the grid position is within the bounds of the board.
boardState: a nested list containing the board state
gridX: x-coordinate of grid cell (column)
gridY: y-coordinate of grid cell (row)
returns: True if a piece can be placed at (gridX, gridY),
False otherwise
"""
# TODO: Part 3
return True # replace with your code
def updateBoardState(boardState, gridX, gridY, player):
"""
Updates the board state to indicate a player placed
a marker at the specified grid position on the board.
boardState: a nested list containing the board state
gridX: x-coordinate of grid cell (column)
gridY: y-coordinate of grid cell (row)
player: 1 or 2
"""
# TODO: Part 3
pass # replace with your code
Once you have completed this implementation, you can test your code using the testPlacingValidMarkers
function. This is very similar to the test function for Part 2, except that it waits until the user clicks a valid grid cell (by calling isValidGridCell
), and then updates the board state using updateBoardState
before printing out the board state (using printBoardState
).
Part 4: Ending the game
# You should be fully equipped to complete this part after Lesson 10 (Monday Apr. 15). Read ahead to Chapter 8 if you want to know more about while
loops early.
If you take a look at the function playGame
, which does the actual game play, the main difference between it and the test functions for Parts 2-3 is that instead of only placing 5 pieces, it has a while
loop that continues until the game is over. For this last part, you’ll implement functions that check for the game to have ended.
# Check if the game is over; if not, switch players
if didPlayerWin(boardState, player):
textLabel.setText("Player {0} wins!".format(player))
isGameOver = True
elif isDraw(boardState):
textLabel.setText("The game is a draw.")
isGameOver = True
else:
player = 3 - player # switches between 1 and 2
Part a: It’s a draw
The game ends in a draw when all grid positions are filled but no one has won. For now, you don’t have code to check for a winner, but you can use isDraw
to at least end the game loop.
Implement the function isDraw
. This function should assume neither player has won, and thus only check if any grid position is not marked.
def isDraw(boardState):
"""
Returns a Boolean indicating whether the game has ended in a draw.
Assumes neither player has won.
boardState: a nested list containing the board state
returns: a Boolean (True if the game is a draw, False otherwise)
"""
# TODO: Part 4a
return False # replace with your code
You can use the actual playGame
function to test this code. By default, didPlayerWin
always returns False, so for now the game should only end once nine markers have been placed (even if a player should have won).
Part b: Checking for a victory
The game ends when a player has markers in an entire row, column, or full diagonal. This is checked in the function didPlayerWin
.
def didPlayerWin(boardState, player):
"""
Returns a Boolean indicating whether the player has
won the game.
boardState: a nested list containing the board state
player: 1 or 2
returns: a Boolean (True if the player won, False otherwise)
"""
# First, check the rows
for row in range(3):
if didPlayerWinWithRow(boardState, player, row):
return True
# Second, check the columns
for col in range(3):
if didPlayerWinWithColumn(boardState, player, col):
return True
# Finally, check the diagonals
if didPlayerWinWithDiagonal(boardState, player):
return True
# No win condition was met
return False
For this last sub-part, you should implement all of the helper functions needed to determine if a win condition has been met.
def didPlayerWinWithRow(boardState, player, row):
"""
Returns a Boolean indicating whether the player
won the game due to the given row.
boardState: a nested list containing the board state
player: 1 or 2
row: 0, 1, or 2
returns: a Boolean (True if the player has an entire row,
False otherwise)
"""
# TODO: Part 4b
return False # replace with your code
def didPlayerWinWithColumn(boardState, player, col):
"""
Returns a Boolean indicating whether the player
won the game due to the given column.
boardState: a nested list containing the board state
player: 1 or 2
col: 0, 1, or 2
returns: a Boolean (True if the player has an entire column,
False otherwise)
"""
# TODO: Part 4b
return False # replace with your code
def didPlayerWinWithDiagonal(boardState, player):
"""
Returns a Boolean indicating whether the player
won the game due to either diagonal.
boardState: a nested list containing the board state
player: 1 or 2
returns: a Boolean (True if the player has an entire diagonal,
False otherwise)
"""
# TODO: Part 4b
return False # replace with your code
Now if you test your code with the playGame
function, it should end when there is a victory, and update the text label at the bottom of the window to say who won.
Reflection
# You should be equipped to complete this part after finishing your assignment.
Were there any particular issues or challenges you dealt with in completing this assignment? How long did you spend on this assignment? Write a brief discussion (a sentence or two is fine) in comments at the bottom of your tictactoe.py
file.
Grading
This assignment will be graded out of 100 points, as follows:
-
5 points - submit a valid
tictactoe.py
file with the right name -
5 points -
tictactoe.py
file contains top-level comments with file name, purpose, author names, and collaboration statement -
10 points - code style enables readable programs
-
16 points - Part 1 (8 pts each for
getCellString
andprintRow
) -
16 points - Part 2 (8 pts each for
drawX
anddrawO
) -
16 points - Part 3 (8 pts each for
isValidGridCell
andupdateBoardState
) -
27 points - Part 4 (6 pts for
isDraw
, 7 pts each fordidPlayerWinWithRow
,didPlayerWinWithColumn
, anddidPlayerWinWithDiagional
) -
5 points - reflection in the comments at the bottom of the
tictactoe.py
file
What you should submit
You should submit a single tictactoe.py
file on Moodle.