Pentago Game in PyScript

Console output will appear here...
# 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()