# Author: David Foster
# GitHub username: Fostdavi
# Date: 08/06/2024
# Description: This script defines the Pentago class. This class can be used to play the game pentago.
# self.make_move allows a player to make their move
# self.print_board pretty prints the current board state
# self.get_game_state returns the current state i.e. WHITE_WON, BLACK_WON, UNFINISHED, DRAW
# other methods are helpers and not used in the playing of the game.
class Pentago:
"""This class allows two users to play the game of Pentago through the terminal."""
def __init__(self):
"""creates each 3x3 board as an array of the unicode "□" charactor, initializes the "_valid_colors" data member
that will hold a list of the valid color strings, and a _last_player datamember that will hold the last player
to make a move"""
self._blank_char = "□"
self._b_char = "○"
self._w_char = "●"
self._last_player = "white"
self._valid_colors = ['white', 'black']
self._game_is_finished = False
self._row = [self._blank_char, self._blank_char, self._blank_char]
self._board_1 = [self._row.copy(), self._row.copy(), self._row.copy()]
self._board_2 = [self._row.copy(), self._row.copy(), self._row.copy()]
self._board_3 = [self._row.copy(), self._row.copy(), self._row.copy()]
self._board_4 = [self._row.copy(), self._row.copy(), self._row.copy()]
self._boards = [self._board_1, self._board_2, self._board_3, self._board_4]
def make_move(self, color, pos, sub_board_rotate, rotation):
"""takes a color, position, sub-board to rotate, and direction of rotation
validates user color selection
checks if this is the first move being made in this game(if it is sets _last_player datamember to current color)
checks if the correct player is making a move
checks if the space selected is empty (using get_value_at_location)
sets the _last_player datamember to the color given
sets the charactor at the position given to the charactor that matches the color given
checks if there is a winner or draw
rotates the given board the given direction
checks if there is a winner or draw
"""
if self._game_is_finished:
return "game is finished"
if color not in self._valid_colors:
return "INVALID COLOR SELECTION"
# make sure correct player is making a move
if self._last_player is None:
self._last_player = color.lower()
elif self._last_player == color.lower():
return "not this player's turn"
# checks if given position is empty or already taken
if self.get_value_at_location(pos) != self._blank_char:
return "position is not empty"
# set the last_player data member to current color for next check
self._last_player = color.lower()
# change the symbol at the location given to the symbol of the color given
sub_board, sub_board_row_index, sub_board_col_index = self.get_sub_board_indexes(pos)
if color.lower() == 'white':
sub_board[sub_board_col_index][sub_board_row_index] = self._w_char
elif color.lower() == 'black':
sub_board[sub_board_col_index][sub_board_row_index] = self._b_char
# check if there is a winner
if self.get_game_state() in ['WHITE_WON', 'BLACK_WON', 'DRAW']:
self._game_is_finished = True
return True
# rotates the board
self.rotate_board(rotation, sub_board_rotate)
# checks if there is a winner
if self.get_game_state() in ['WHITE_WON', 'BLACK_WON', 'DRAW']:
self._game_is_finished = True
return True
return True
def get_game_state(self):
"""uses check_sequences to get the characters of the winners decodes these and returns. DRAW, WHITE_WON,
BLACK_WON, UNFINISHED depending on the result of check_sequences"""
win_result = self.check_sequences()
if len(win_result) == 2:
return 'DRAW'
elif self._w_char in win_result:
return 'WHITE_WON'
elif self._b_char in win_result:
return 'BLACK_WON'
else:
return 'UNFINISHED'
def is_board_full(self):
"""checks if the board is full and returns True if the board is full False otherwise"""
combined_board = self.combine_boards()
for row in combined_board:
if self._blank_char in row:
return False
return True
def print_board(self):
"""prints the whole board using hard coded column and index headers
adds the left boards (1 and 3) adds the right boards (2 and 4) then uses a for loop to itterate through
each row combining left_board[i]+"|"+right_board[i] and printing a board separator when we reach the middle of
the for loop."""
# print index header
print(" 0 1 2 3 4 5")
# create row index list
row_index = ["A| ", "B| ", "C| ", "D| ", "E| ", "F| "]
left_board = self._board_1 + self._board_3
right_board = self._board_2 + self._board_4
# get the number of rows
row_count = len(left_board)
for i in range(row_count):
row_left = left_board[i]
row_right = right_board[i]
print(row_index[i] + ' '.join(row_left) + " | " + ' '.join(row_right))
if i == 2:
print(' ------|------')
def transpose(self, board):
"""will transpose the board given to it"""
transposed = []
for i in range(len(board[0])):
trans_row = []
for j in range(len(board[i])):
trans_row.append(board[j][i])
transposed.append(trans_row)
return transposed
def reverse_rows(self, board):
"""will reverse the rows of the board given to it"""
rows_reversed = []
for row in board:
rev_row = row[::-1]
rows_reversed.append(rev_row)
return rows_reversed
def rotate_board(self, direction, board_num):
"""Rotates the board either clockwise (direction = c) or anticlockwise (direction = a)
uses the transpose and reverse_rows methods to accomplish this rotation
clockwise: transpose then reverse rows
anticlockwise: reverse rows then transpose"""
board = self._boards[board_num - 1]
direction = direction.lower()
if direction == 'c':
# clockwise rotation: transpose then reverse
transposed_board = self.transpose(board)
rotated_board = self.reverse_rows(transposed_board)
board[:] = rotated_board
elif direction == 'a':
# anticlockwise rotation: reverse then transpose
rotated_board = self.reverse_rows(board)
transposed_board = self.transpose(rotated_board)
board[:] = transposed_board
self._boards[board_num - 1] = board
def decode_coordinate(self, coordinate):
"""takes coordinates in the form "a0" and converts it into an array index
returns: y_index, x_index"""
letter_to_row = {'a': 0, 'b': 1, 'c': 2, 'd': 3, 'e': 4, 'f': 5}
letter = coordinate[0]
y_index = letter_to_row[letter]
x_index = int(coordinate[1])
return y_index, x_index
def get_sub_board_indexes(self, pos):
"""takes coordinates in the form of "a0" and uses the decode_coordinate to convert to array index then
determines the correct sub-board and adjusts the index for that board."""
y_index, x_index = self.decode_coordinate(pos)
if y_index < 3:
if x_index < 3:
sub_board = self._board_1
else:
sub_board = self._board_2
else:
if x_index < 3:
sub_board = self._board_3
else:
sub_board = self._board_4
# use modulous to get the sub board adjusted indexes
sub_board_col_index = x_index % 3
sub_board_row_index = y_index % 3
return sub_board, sub_board_col_index, sub_board_row_index
def combine_boards(self):
"""combine all 4 boards into one array"""
combined_board = []
for i in range(3):
combined_board.append(self._board_1[i] + self._board_2[i])
for i in range(3):
combined_board.append(self._board_3[i] + self._board_4[i])
return combined_board
def get_value_at_location(self, pos):
"""returns the value at a specific location on the board given in the format 'a0' """
sub_board, sub_board_row_index, sub_board_col_index = self.get_sub_board_indexes(pos)
return sub_board[sub_board_col_index][sub_board_row_index]
def check_sequences(self):
""" Checks if there has been a winner or a draw
combines all sub-boards into one array
Horizontal check: converts each row of the combined array to string then checks if a substring length 5 for each
player exists, if it does adds that players charactor to the "winner" set
Vertical check: transposes the combined board and runs the same code as horizontal check
Diagonal check: gets all values from each diagonal of length >= 5 as list, converts lists to strings then checks
if substring of players characters exist, if they do adds that charactor to the winners set
returns: the winners set with the characters of the players with a row of five"""
# get combined board, set our chars list for checking both players, initiate our winner set
combined_board = self.combine_boards()
chars = [self._w_char, self._b_char]
winner = set()
# check horizontal
for character in chars:
check_str = character * 5
for row in combined_board:
row_string = ''.join(row)
if check_str in row_string:
winner.add(character)
# check vertical (we can just transpose the board and check horizontal)
combined_board = self.transpose(combined_board)
for character in chars:
check_str = character * 5
for row in combined_board:
row_string = ''.join(row)
if check_str in row_string:
winner.add(character)
# get diagonals
main_diagonal_locs = [['b0', 'c1', 'd2', 'e3', 'f4'], ['a0', 'b1', 'c2', 'd3', 'e4', 'f5'],
['a1', 'b2', 'c3', 'd4', 'e5']]
anit_diagonal_locs = [['e0', 'd1', 'c2', 'b3', 'a4'], ['f0', 'e1', 'd2', 'c3', 'b4', 'a5'],
['f1', 'e2', 'd3', 'c4', 'b5']]
diagonals = []
# get digonal values as lists
for diagonal in main_diagonal_locs:
diag = []
for loc in diagonal:
diag.append(self.get_value_at_location(loc))
diagonals.append(diag)
# get anti diagonal values as lists
for diagonal in anit_diagonal_locs:
diag = []
for loc in diagonal:
diag.append(self.get_value_at_location(loc))
diagonals.append(diag)
# check if diagonals (anti and main) have a sequence of 5 user chars
for character in chars:
check_str = character * 5
for row in diagonals:
row_string = ''.join(row)
if check_str in row_string:
winner.add(character)
# adds both player characters to winner if board is full
if self.is_board_full():
winner.add(chars[0])
winner.add(chars[1])
return winner
# Initialize the game object globally
game = Pentago()