14.5. Non-OOP vs. OOP Examples: Tic-Tac-Toe

At first, it can be difficult to see how to use classes in your programs. Let’s look at an example of a short tic-tac-toe program that doesn’t use classes, and then rewrite it so it does.

Open a new file editor window and enter the following program; then save it as tictactoe.py:

>>> # tictactoe.py, A non-OOP tic-tac-toe game.
>>> ALL_SPACES = list('123456789') # The keys for a TTT board dictionary.
>>> X, O, BLANK = 'X', 'O', ' ' # Constants for string values.
>>> def main():
>>> # """Runs a game of tic-tac-toe."""
>>>     print('Welcome to tic-tac-toe!')
>>>     gameBoard = getBlankBoard() # Create a TTT board dictionary.
>>>     currentPlayer, nextPlayer = X, O # X goes first, O goes next.
>>>     while True:
>>>         print(getBoardStr(gameBoard))
>>> # Display the board on the screen.
>>> # Keep asking the player until they enter a number 1-9:
>>>         move = None
>>>         while not isValidSpace(gameBoard, move):
>>>             print(f'What is {currentPlayer}\'s move? (1-9)')
>>>             move = input()
>>>         updateBoard(gameBoard, move, currentPlayer) # Make the move.
>>> # Check if the game is over:
>>>         if isWinner(gameBoard, currentPlayer): # First check for victory.
>>>             print(getBoardStr(gameBoard))
>>>             print(currentPlayer + ' has won the game!')
>>>             break
>>>         elif isBoardFull(gameBoard): # Next check for a tie.
>>>             print(getBoardStr(gameBoard))
>>>             print('The game is a tie!')
>>>             break
>>>         currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns.
>>>     print('Thanks for playing!')
>>> def getBlankBoard():
>>> # """Create a new, blank tic-tac-toe board."""
>>>     board = {} # The board is represented as a Python dictionary.
>>>     for space in ALL_SPACES:
>>>         board[space] = BLANK # All spaces start as blank.
>>>     return board
>>> def getBoardStr(board):
>>> # """Return a text-representation of the board."""
>>>     return f'''
>>>     {board['1']}|{board['2']}|{board['3']} 1 2 3
>>>     -+-+-
>>>     {board['4']}|{board['5']}|{board['6']} 4 5 6
>>>     -+-+-
>>>     {board['7']}|{board['8']}|{board['9']} 7 8 9'''
>>> def isValidSpace(board, space):
>>> # """Returns True if the space on the board is a valid space number
>>> # and the space is blank."""
>>>     return space in ALL_SPACES or board[space] == BLANK
>>> def isWinner(board, player):
>>> # """Return True if player is a winner on this TTTBoard."""
>>>     b, p = board, player # Shorter names as "syntactic sugar".
>>> # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals.
>>>     return ((b['1'] == b['2'] == b['3'] == p) or # Across the top
>>> (b['4'] == b['5'] == b['6'] == p) or # Across the middle
>>> (b['7'] == b['8'] == b['9'] == p) or # Across the bottom
>>> (b['1'] == b['4'] == b['7'] == p) or # Down the left
>>> (b['2'] == b['5'] == b['8'] == p) or # Down the middle
>>> (b['3'] == b['6'] == b['9'] == p) or # Down the right
>>> (b['3'] == b['5'] == b['7'] == p) or # Diagonal
>>> (b['1'] == b['5'] == b['9'] == p))
>>> # Diagonal
>>> def isBoardFull(board):
>>> # """Return True if every space on the board has been taken."""
>>>     for space in ALL_SPACES:
>>>         if board[space] == BLANK:
>>>             return False # If a single space is blank, return False.
>>>     return True # No spaces are blank, so return True.
>>> def updateBoard(board, space, mark):
>>> # """Sets the space on the board to mark."""
>>>     board[space] = mark
>>> if __name__ == '__main__':
>>>     main() # Call main() if this module is run, but not when imported.
Welcome to tic-tac-toe!

     | |  1 2 3
    -+-+-
     | |  4 5 6
    -+-+-
     | |  7 8 9
---------------------------------------------------------------------------

KeyError                                  Traceback (most recent call last)

<ipython-input-2-26f9f77f44b5> in <module>
     68     board[space] = mark
     69 if __name__ == '__main__':
---> 70     main() # Call main() if this module is run, but not when imported.


<ipython-input-2-26f9f77f44b5> in main()
     12 # Keep asking the player until they enter a number 1-9:
     13         move = None
---> 14         while not isValidSpace(gameBoard, move):
     15             print(f'What is {currentPlayer}\'s move? (1-9)')
     16             move = input()


<ipython-input-2-26f9f77f44b5> in isValidSpace(board, space)
     44 # """Returns True if the space on the board is a valid space number
     45 # and the space is blank."""
---> 46     return space in ALL_SPACES or board[space] == BLANK
     47 def isWinner(board, player):
     48 # """Return True if player is a winner on this TTTBoard."""


KeyError: None

When you run this program, the output will look something like this:

Welcome to tic-tac-toe! | | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is X’s move? (1-9) 1 X| | 1 2 3 -+-+- | | 4 5 6 -+-+- | | 7 8 9 What is O’s move? (1-9) –snip– X| |O 1 2 3 -+-+- |O| 4 5 6 -+-+- X|O|X 7 8 9 What is X’s move? (1-9) 4 X| |O 1 2 3 -+-+- X|O| 4 5 6 -+-+- X|O|X 7 8 9 X has won the game! Thanks for playing!

Briefly, this program works by using a dictionary object to represent the nine spaces on a tic-tac-toe board. The dictionary’s keys are the strings ‘1’ through ‘9’ , and its values are the strings ‘X’ , ‘O’ , or ’ ’ . The numbered spaces are in the same arrangement as a phone’s keypad. The functions in tictactoe.py do the following:

  • The main() function contains the code that creates a new board data structure (stored in the gameBoard variable) and calls other functions in the program.

  • The getBlankBoard() function returns a dictionary with the nine spaces set to ’ ’ for a blank board.

  • The getBoardStr() function accepts a dictionary representing the board and returns a multiline string representation of the board that can be printed to the screen. This is what renders the tic-tac-toe board’s text that the game displays.

  • The isValidSpace() function returns True if it’s passed a valid space num- ber and that space is blank.

  • The isWinner() function’s parameters accept a board dictionary and either ‘X’ or ‘O’ to determine whether that player has three marks in a row on the board.

  • The isBoardFull() function determines whether the board has no blank spaces, meaning the game has ended. The updateBoard() function’s parameters accept a board dictionary, a space, and a player’s X or O mark and updates the dictionary.

Notice that many of the functions accept the variable board as their first parameter. That means these functions are related to each other in that they all operate on a common data structure.

When several functions in the code all operate on the same data struc- ture, it’s usually best to group them together as the methods and attributes of a class. Let’s redesign this in the tictactoe.py program to use a TTTBoard class that will store the board dictionary in an attribute named spaces . The functions that had board as a parameter will become methods of our TTTBoard class and use the self parameter instead of a board parameter.

Open a new file editor window, enter the following code, and save it as tictactoe_oop.py:

>>> # tictactoe_oop.py, an object-oriented tic-tac-toe game.
>>> ALL_SPACES = list('123456789') # The keys for a TTT board.
>>> X, O, BLANK = 'X', 'O', ' ' # Constants for string values.
>>> def main():
>>> # """Runs a game of tic-tac-toe."""
>>>     print('Welcome to tic-tac-toe!')
>>>     gameBoard = TTTBoard() # Create a TTT board object.
>>>     currentPlayer, nextPlayer = X, O # X goes first, O goes next.
>>>     while True:
>>>         print(gameBoard.getBoardStr())
>>> # Display the board on the screen.
>>> # Keep asking the player until they enter a number 1-9:
>>>         move = None
>>>         while not gameBoard.isValidSpace(move):
>>>             print(f'What is {currentPlayer}\'s move? (1-9)')
>>>             move = input()
>>>         gameBoard.updateBoard(move, currentPlayer) # Make the move.
>>> # Check if the game is over:
>>>         if gameBoard.isWinner(currentPlayer): # First check for victory.
>>>             print(gameBoard.getBoardStr())
>>>             print(currentPlayer + ' has won the game!')
>>>             break
>>>         elif gameBoard.isBoardFull(): # Next check for a tie.
>>>             print(gameBoard.getBoardStr())
>>>             print('The game is a tie!')
>>>             break
>>>         currentPlayer, nextPlayer = nextPlayer, currentPlayer # Swap turns.
>>>     print('Thanks for playing!')
>>> class TTTBoard:
>>>     def __init__(self, usePrettyBoard=False, useLogging=False):
>>> # """Create a new, blank tic tac toe board."""
>>>         self._spaces = {} # The board is represented as a Python dictionary.
>>>         for space in ALL_SPACES:
>>>             self._spaces[space] = BLANK # All spaces start as blank.
>>>     def getBoardStr(self):
>>> # """Return a text-representation of the board."""
>>>         return f'''
>>> {self._spaces['1']}|{self._spaces['2']}|{self._spaces['3']}
>>> -+-+-
>>> {self._spaces['4']}|{self._spaces['5']}|{self._spaces['6']}
>>> -+-+-
>>> {self._spaces['7']}|{self._spaces['8']}|{self._spaces['9']}
>>> 1 2 3
>>> 4 5 6
>>> 7 8 9'''
>>>     def isValidSpace(self, space):
>>> # """Returns True if the space on the board is a valid space number
>>> # and the space is blank."""
>>>         return space in ALL_SPACES and self._spaces[space] == BLANK
>>>     def isWinner(self, player):
>>> # """Return True if player is a winner on this TTTBoard."""
>>>         s, p = self._spaces, player # Shorter names as "syntactic sugar".
>>> # Check for 3 marks across the 3 rows, 3 columns, and 2 diagonals.
>>>         return ((s['1'] == s['2'] == s['3'] == p) or # Across the top
>>> (s['4'] == s['5'] == s['6'] == p) or # Across the middle
>>> (s['7'] == s['8'] == s['9'] == p) or # Across the bottom
>>> (s['1'] == s['4'] == s['7'] == p) or # Down the left
>>> (s['2'] == s['5'] == s['8'] == p) or # Down the middle
>>> (s['3'] == s['6'] == s['9'] == p) or # Down the right
>>> (s['3'] == s['5'] == s['7'] == p) or # Diagonal
>>> (s['1'] == s['5'] == s['9'] == p))
>>> # Diagonal
>>>     def isBoardFull(self):
>>> # """Return True if every space on the board has been taken."""
>>>         for space in ALL_SPACES:
>>>             if self._spaces[space] == BLANK:
>>>                 return False # If a single space is blank, return False.
>>>         return True # No spaces are blank, so return True.
>>>     def updateBoard(self, space, player):
>>> # """Sets the space on the board to player."""
>>>         self._spaces[space] = player
>>> if __name__ == '__main__':
>>>     main() # Call main() if this module is run, but not when imported.
Welcome to tic-tac-toe!

 | |
-+-+-
 | |
-+-+-
 | |
1 2 3
4 5 6
7 8 9
What is X's move? (1-9)
8
 | |
-+-+-
 | |
-+-+-
 |X|
1 2 3
4 5 6
7 8 9
What is O's move? (1-9)
6
 | |
-+-+-
 | |O
-+-+-
 |X|
1 2 3
4 5 6
7 8 9
What is X's move? (1-9)
7
 | |
-+-+-
 | |O
-+-+-
X|X|
1 2 3
4 5 6
7 8 9
What is O's move? (1-9)
1
O| |
-+-+-
 | |O
-+-+-
X|X|
1 2 3
4 5 6
7 8 9
What is X's move? (1-9)
1
What is X's move? (1-9)
1
What is X's move? (1-9)
1
What is X's move? (1-9)
26
What is X's move? (1-9)
7
What is X's move? (1-9)
8
What is X's move? (1-9)
9
O| |
-+-+-
 | |O
-+-+-
X|X|X
1 2 3
4 5 6
7 8 9
X has won the game!
Thanks for playing!

Functionally, this program is the same as the non-OOP tic-tac-toe program. The output looks identical. We’ve moved the code that used to be in getBlankBoard() to the TTTBoard class’s __init__() method, because they perform the same task of preparing the board data structure. We converted the other functions into methods, with the self parameter replacing the old board parameter, because they also serve a similar purpose: they’re both blocks of code that operate on a tic-tac-toe board data structure.

When the code in these methods needs to change the dictionary stored in the _spaces attribute, the code uses self._spaces . When the code in these methods need to call other methods, the calls would also be preceded by self and a period. This is similar to how coinJars.values() in “Creating a Simple Class: WizCoin” had an object in the coinJars variable. In this example, the object that has the method to call is in a self variable.

Also, notice that the _spaces attribute begins with an underscore, mean- ing that only code inside the methods of TTTBoard should access or modify it. Code outside the class should only be able to modify _spaces indirectly by calling methods that modify it.

It can be helpful to compare the source code of the two tic-tac-toe pro- grams. You can compare the code in this book or view a side-by-side com- parison at https://autbor.com/compareoop/.

Tic-tac-toe is a small program, so it doesn’t take much effort to under- stand. But what if this program were tens of thousands of lines long with hundreds of different functions? A program with a few dozen classes would be easier to understand than a program with several hundred disparate func- tions. OOP breaks down a complicated program into easier-to-­understand chunks.