I have created a two-file tic-tac-toe game, with board.py
containing most of the internal workings of the playing board. I intended to have board.py
work as general infrastructure for playing the game and testing who won.
game.py
is the file to be run if you want to play the game, and it just handles the errors board.py
throws, in addition to fetching input from the user and informing about errors and results.
I tried to document it extensively.
One feature I am particularly proud of is the ability to play on e.g. a 7*3 board (sacrificing the chance of winning diagonally). I also tried to hide all errors from the user, the only error that will terminate the program is if they give invalid input in game.py
when creating the game board dimensions.
Here is the code:
board.py
:
"""
A playing board for tic tac toe. Contains the class Board.
"""
class Board:
"""
A playing board for tic tac toe.
Fields:
rows (int): The number of rows in the board.
columns (int): The number of columns in the board.
rowLists (list): A list of lists representing the cells of the board.
Methods:
__init__(self, rows: int, columns: int):
Initializes a new instance of the Board class.
Parameters:
rows (int): The number of rows in the board.
columns (int): The number of columns in the board.
__str__(self):
Returns a string representation of the board.
Returns:
str: The string representation of the board.
mark(self, coords: tuple, mark: str):
Set a mark on the board at the specified coordinates if there is no
mark there already.
Parameters:
coords (tuple): The coordinates of the cell where the mark will
be set.
mark (str): The mark to be set on the cell. Must be either 'X'
or 'O'.
Raises:
ValueError: If the mark is not 'X' or 'O'.
ValueError: If the cell is out of range
ValueError: If the cell has already been marked
Returns:
None
get_rows(self):
Returns the list of rows in the current object.
Returns:
list: A list of rows in the current object.
check_if_won(self):
Check if the player has won the game.
Returns:
bool|str: The player mark if a player has won, 'Tie' if all
cells are marked, and False if no player has won.
"""
def __init__(self, rows: int, columns: int):
self.row_lists = [[' ' for _ in range(columns)] for _ in range(rows)]
self.rows = rows
self.columns = columns
def __str__(self):
result = ' +' + '---+'*self.columns + '\n'
for i, row in enumerate(self.row_lists):
result += str(i) + ' |'
for column in row:
result += ' ' + column + ' |'
result += '\n' + ' +' + '---+'*self.columns + '\n'
result += ' '
for i in range(self.columns):
result += ' ' + str(i) + ' '
return result
def mark(self, coords: tuple, mark: str):
"""
Set a mark on the board at the specified coordinates if there is no
mark there already.
Parameters:
coords (tuple): The coordinates of the cell where the mark will be
set.
mark (str): The mark to be set on the cell. Must be either 'X' or
'O'.
Raises:
ValueError: If the mark is not 'X' or 'O'.
ValueError: If the cell is out of range.
ValueError: If the cell has already been marked.
Returns:
None
"""
if mark not in ('X', 'O'):
raise ValueError('Invalid mark')
if coords[0] >= self.rows or coords[1] >= self.columns:
raise ValueError('Invalid coordinates')
if self.row_lists[coords[0]][coords[1]] != ' ':
raise ValueError('Cell already marked')
row, column = coords
self.row_lists[row][column] = mark
def get_rows(self):
"""
Returns the list of rows in the current object.
Parameters:
None
Returns:
list: A list of rows in the current object.
"""
return self.row_lists
def check_if_won(self):
"""
Check if the player has won the game.
Checks if a player has won the game by checking the
rows, columns, and diagonals of the game board. It iterates through
ach row, column, and diagonal and calls the `check_line` function to
determine if all cells in the line are filled with the same value.
Parameters:
None
Returns:
bool|str: The player mark if a player has won, 'Tie' if all cells
are marked, and False if no player has won.
"""
winner = False
def check_line(line: list):
first = line[0]
for cell in line:
if cell == ' ' or cell != first:
return False
nonlocal winner
winner = line[0]
return True
def check_diagonals():
if self.rows != self.columns:
return False
diagonals = [[], []]
for i in range(self.rows):
diagonals[0].append(self.row_lists[i][i])
diagonals[1].append(self.row_lists[i][self.columns-1-i])
return check_line(diagonals[0]) or check_line(diagonals[1])
def check_rows():
for row in self.row_lists:
if check_line(row):
return True
return False
def check_columns():
for i in range(self.columns):
column = [row[i] for row in self.row_lists]
if check_line(column):
return True
return False
if check_rows() or check_columns() or check_diagonals():
return winner
for row in self.row_lists:
for cell in row:
if cell == ' ':
return False
return 'Tie'
if __name__ == '__main__':
# Testing that everything works as it should
b = Board(3, 3)
print(b)
b.mark((0, 0), 'X')
b.mark((1, 1), 'X')
b.mark((2, 2), 'X')
b.row_lists = [['X', 'Y', 'X'], ['Y', 'X', 'Y'], ['Y', 'X', 'Y']]
print(b)
print(b.check_if_won())
game.py
"""
NOT A MODULE
Used to play the tic-tac-toe game
"""
from ast import literal_eval # To not use the built-in eval function
from sys import exit as sys_exit
from board import Board
try:
rows = int(input('Enter number of rows: '))
cols = int(input('Enter number of columns: '))
except ValueError:
print('Invalid input')
sys_exit() # Because built-in exit() is bad practice
board = Board(rows, cols)
turn = 'X'
while not board.check_if_won():
print(board)
print(f'Player {turn}\'s turn')
try:
coords = literal_eval(input(
'Enter coordinates for your mark on the format "row, column": '
))
print('\n'*20)
board.mark(coords, turn)
except (ValueError, TypeError) as e:
# Catches the two exceptions board.mark() can throw (except 'Invalid
# mark', which cannot happen because of user input).
print('\n'*20)
if str(e) == 'Invalid coordinates':
print('Invalid coordinates!')
continue
if str(e) == 'Cell already marked':
print('Already marked!')
continue
print('An unknow error has occured. Please try again.')
continue
if turn == 'X':
turn = 'O'
else:
turn = 'X'
winner = board.check_if_won()
if winner in ['X', 'O']:
print(f'Player {board.check_if_won()} wins!')
else:
print('It\'s a tie!')
```