At this point, we’ve covered most of the basic syntax of Python!
Let’s apply what we’ve learned so far to a Tic-Tac-Toe game. The first step in creating any sort of application (including games) is to spend time thinking about design. You’ll recall from our study of the Waterfall SDLC that every minute spent designing software will save many more minutes in actual coding/debugging time.
Data Design
First, let’s think about how we will represent the Tic-Tac-Toe board data in our program. We could use 9 variables, one for each square to keep track of whether it contains an “X”, “O” or is empty, but by now you should appreciate that this is probably not the most efficient approach. If you think about it, we’re going to have several functions in this program and need to pass the Tic-Tac-Toe board data to these functions. Passing nine arguments every time will be cumbersome!
Because the nine squares of a Tic-Tac-Toe board are related, it makes more sense to use a list to keep this data grouped together. From what we know so far, you could define the board data like this:
1 |
ttt_board = [ " ", " ", " ", " ", " ", " ", " ", " ", " " ] |
Here we have a list containing 9 blank spaces since each square will initially not contain any X’s or O’s. This approach will work, but it’s a little awkward because the Tic-Tac-Toe game board has two dimensions (3 rows and 3 columns). So, for example, to make the middle square an “X” we would need to refer to it like so:
1 |
ttt_board[4] = "X" |
This means that to make this work we’ll need to be able to translate a user move consisting of a row and column into a single index value for our list. Hhmm.. sounds tricky… there must be a more elegant solution.
There is! Recall earlier I said that a list in Python may contain any kind of data? Well, it turns out that a list can actually contain other lists! A list inside a list is called a nested list.
Here’s what we’ll do: We’ll define a list that contains three nested lists. Each nested list will represent one of the rows in our Tic-Tac-Toe board:
1 |
ttt_board = [ [ " ", " ", " " ], [ " ", " ", " " ], [ " ", " ", " " ] ] |
The beauty of nested lists is that we can refer to a specific element of a nested list by providing two indices. The first index refers to which nested list we want (starting from 0, as usual), and the second index refers to the element within that nested list (again starting from 0). For example, to put an “X” in the middle square we can now do it like this:
1 |
ttt_board[1][1] = "X" |
The first index is the row, and the second is the column. This data representation is much more natural than using a flat list with nine elements and one index.
Functional Design
Now that we know how we’ll represent the Tic-Tac-Toe board data, let’s think about the functional design. When we first talked about designing functions (U1-9) I explained the process of top-down design:
-
- The overall task that the program must perform is broken down into a series of subtasks.
- Each of the subtasks is examined to determine whether it can be further broken down into more subtasks. This step is repeated until no more subtasks can be identified.
- Once all of the subtasks have been identified, they are written in code.
If we start at step 1, with paper and pencil in hand we can write some pseudocode that explains how the overall game will work:
Above is an overall description of how the game will work. We’ve broken the problem down into 3 main steps, each of which represents a subtask in our game. We will also require a subtask to check if there is a winner or not (for our main loop condition). These four subtasks will become separate functions in our program, each performing a specific, well-defined role.
Python Implementation
Once we have a clear picture of how we will represent the data in our program, and what the main subtasks of our program will be, it’s time to translate our design into Python. Because the pseudocode above defines the overall flow of the game, I will begin by translating that into my main() function in Python:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
def main(): """Our Main Game Loop:""" free_cells = 9 users_turn = True ttt_board = [ [ " ", " ", " " ], [ " ", " ", " " ], [ " ", " ", " " ] ] while winner(ttt_board) == "" and (free_cells > 0): display_board(ttt_board) if users_turn: make_user_move(ttt_board) users_turn = not users_turn else: make_computer_move(ttt_board) users_turn = not users_turn free_cells -= 1 display_board(ttt_board) if (winner(ttt_board) == 'X'): print("Y O U W O N !") elif (winner(ttt_board) == 'O'): print("I W O N !") else: print("S T A L E M A T E !") print("\n*** GAME OVER ***\n") # Start the game! main() |
Notice how closely the code above reflects our original pseudocode. I’m using a Boolean flag variable called users_turn to keep track of whether it’s the users or computers move next. Also, within the function, I define our nested list (ttt_board) as a local variable. Because local variables are only accessible within the function they are defined in, I will need to pass this as an argument to the other functions in my program. From the code you can see the other four functions I will require (and how they will be called):
display_board() — Accepts the Tic-Tac-Toe board as a parameter. This function will display the Tic-Tac-Toe board grid (using ASCII characters) and show the positions of any X’s and O’s. I’ve also chosen to display the column and row numbers on top and beside the board to help the user figure out the coordinates of their next move. This function does not return anything. Here is the complete code for this function:
1 2 3 4 5 6 7 8 |
def display_board(board): print(" 0 1 2") print("0: " + board[0][0] + " | " + board[0][1] + " | " + board[0][2]) print(" ---+---+---") print("1: " + board[1][0] + " | " + board[1][1] + " | " + board[1][2]) print(" ---+---+---") print("2: " + board[2][0] + " | " + board[2][1] + " | " + board[2][2]) print() |
winner() – Accepts the Tic-Tac-Toe board as a parameter. If there is no winner, the function will return the empty string “”. If the user has won, it will return “X”, and if the computer has won it will return “O”. This function will need to check if each row, column, and diagonal contains all X’s or O’s. Here is the partially complete function:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
def winner(board): # Check rows for winner for row in range(3): if (board[row][0] == board[row][1] == board[row][2]) and\ (board[row][0] != " "): return board[row][0] # Check columns for winner # Check diagonal (top-left to bottom-right) for winner # Check diagonal (bottom-left to top-right) for winner # No winner: return the empty string return "" |
make_user_move() — Accepts the Tic-Tac-Toe board as a parameter. This function will ask the user for a row and column. If the row and column are each within the range of 0 and 2, and that square is not already occupied, then it will place an “X” in that square.
1 2 3 4 5 6 7 8 9 |
valid_move = False while not valid_move: row = int(input("What row would you like to move to (0-2):")) col = int(input("What col would you like to move to (0-2):")) if (0 <= row <= 2) and (0 <= col <= 2) and (board[row][col] == " "): board[row][col] = 'X' valid_move = True else: print("Sorry, invalid square. Please try again!\n") |
make_computer_move() — Accepts the Tic-Tac-Toe board as a parameter. This function will randomly pick row and column values between 0 and 2. If that square is not already occupied it will place an “O” there. Otherwise, another random row and column will be generated. This function is very similar to the make_user_move() function except we can be sure that only valid integers in the range of 0 to 2 are generated.
And there you have it! We’ve walked through the complete design process for a simple (text-based) game.
You Try!
Below is a code skeleton (some parts are incomplete) for our Tic-Tac-Toe game:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
def winner(board): """This function accepts the Tic-Tac-Toe board as a parameter. If there is no winner, the function will return the empty string "". If the user has won, it will return 'X', and if the computer has won it will return 'O'.""" # Check rows for winner for row in range(3): if (board[row][0] == board[row][1] == board[row][2]) and\ (board[row][0] != " "): return board[row][0] # Check columns for winner # Check diagonal (top-left to bottom-right) for winner # Check diagonal (bottom-left to top-right) for winner # No winner: return the empty string return "" def display_board(board): """This function accepts the Tic-Tac-Toe board as a parameter. It will print the Tic-Tac-Toe board grid (using ASCII characters) and show the positions of any X's and O's. It also displays the column and row numbers on top and beside the board to help the user figure out the coordinates of their next move. This function does not return anything.""" print(" 0 1 2") print("0: " + board[0][0] + " | " + board[0][1] + " | " + board[0][2]) print(" ---+---+---") print("1: " + board[1][0] + " | " + board[1][1] + " | " + board[1][2]) print(" ---+---+---") print("2: " + board[2][0] + " | " + board[2][1] + " | " + board[2][2]) print() def make_user_move(board): """This function accepts the Tic-Tac-Toe board as a parameter. It will ask the user for a row and column. If the row and column are each within the range of 0 and 2, and that square is not already occupied, then it will place an 'X' in that square.""" valid_move = False while not valid_move: row = int(input("What row would you like to move to (0-2):")) col = int(input("What col would you like to move to (0-2):")) if (0 <= row <= 2) and (0 <= col <= 2) and (board[row][col] == " "): board[row][col] = 'X' valid_move = True else: print("Sorry, invalid square. Please try again!\n") def make_computer_move(board): """This function accepts the Tic-Tac-Toe board as a parameter. It will randomly pick row and column values between 0 and 2. If that square is not already occupied it will place an 'O' in that square. Otherwise, another random row and column will be generated.""" # Code needed here... def main(): """Our Main Game Loop:""" free_cells = 9 users_turn = True ttt_board = [ [ " ", " ", " " ], [ " ", " ", " " ], [ " ", " ", " " ] ] while winner(ttt_board) == "" and (free_cells > 0): display_board(ttt_board) if users_turn: make_user_move(ttt_board) users_turn = not users_turn else: make_computer_move(ttt_board) users_turn = not users_turn free_cells -= 1 display_board(ttt_board) if (winner(ttt_board) == 'X'): print("Y O U W O N !") elif (winner(ttt_board) == 'O'): print("I W O N !") else: print("S T A L E M A T E !") print("\n*** GAME OVER ***\n") # Start the game! main() |
Your task is to:
-
- Complete the winner() function.
-
- Complete the make_computer_move() function.
-
- Improve the make_user_move() function so that if the user enters a string instead of an integer, the program does not crash with an exception. Use exception handling!
-
- Improve the game so that the user selects a row and column within the range 1 and 3 (instead of 0 and 2).
-
- Only after you have completed the tasks above, try to improve your make_computer_move() function to add some “artificial intelligence” to it (rather than completely random moves). It’s very challenging to do a good job of this, for this reason, this is an optional requirement.