from enum import Enum import numpy as np class Player(Enum): undefined = 0 one = 1 two = 2 grid_full = 3 # Adjust the symbol or color for printing the grid SYMBOLS = { Player.one: "X", Player.two: "O", Player.undefined: " " } COLORS = { Player.one: "\033[32m", # green Player.two: "\033[94m", # blue Player.undefined: "" } END_COLOR = "\033[0m" def get_opponent(player:Player) -> Player: if player == Player.one: return Player.two else: return Player.one class TicTacToeError(Exception): """TicTacToe Error""" class TicTacToe(): def __init__(self) -> None: grid = [] for _ in range(3): grid.append([Player.undefined, Player.undefined, Player.undefined]) self.grid = np.array(grid) @staticmethod def _get_player_from_int(player:Player|int) -> Player: """ Internal helper methot to return a Player-Object from Int or Player """ if isinstance(player, int): try: return Player(player) except ValueError: raise TicTacToeError("The player number is invalid. Only '1' and '2' are accepted") return player def add(self, row:int, col:int, player:Player|int, grid:np.ndarray|None = None) -> bool: """ Change the value of a undefined field. The player can be passed as Player Object or plain Int. Returns True, if executed correctly. Returns False, if the field is already occupied or the player number is invalid. """ if grid is None: grid = self.grid player = self._get_player_from_int(player) if self.check_field(row, col, grid): grid[row][col] = player return True return False def check_field(self, row:int, col:int, grid:np.ndarray|None = None) -> bool: """ Checks, if a field is occupied """ if grid is None: grid = self.grid if self.grid[row][col] != Player.undefined: return False return True def print_grid(self, grid:np.ndarray|None=None) -> None: """ Prints the current state of a grid. If no grid is specified, the current game grid (self.grid) is printed """ if grid is None: grid = self.grid print("\t┌───┬───┬───┐") for i, row in enumerate(grid): print("\t│", end="") for field in row: print(f" {COLORS[field]}{SYMBOLS[field]}{END_COLOR} │", end="") if i < len(grid) - 1: print("\n\t├───┼───┼───┤") print("\n\t└───┴───┴───┘\n\n") def check_winner(self, grid:np.ndarray|None = None) -> Player: """ Checks, if the game is won. If the game is won by a player, returns this player. Else, returns Player.undefined """ if grid is None: grid = self.grid # Vertical line for row in grid: if (row[0] == row[1] == row[2] and row[0] != Player.undefined): return row[0] # Horizontal line for i in range(len(grid)): col = grid[:, i] if (col[0] == col[1] == col[2] and col[0] != Player.undefined): return col[0] # Diagonal line if (grid[0, 0] == grid[1, 1] == grid[2, 2] or grid[0, 2] == grid[1, 1] == grid[2, 0]): if grid[1, 1] != Player.undefined: return grid[1, 1] # No winner if Player.undefined in grid: return Player.undefined return Player.grid_full def _maximize(self, grid:np.ndarray) -> float: """ Helper Method for the maximizing Player """ best = -np.inf for i in range(3): for j in range(3): if grid[i, j] == Player.undefined: grid[i, j] = self.active_player score = self._evaluate_recursively(grid, self.opponent) grid[i, j] = Player.undefined best = max(best, score) return best def _minimize(self, grid:np.ndarray) -> float: """ Helper Method for the minimizing player """ best = np.inf for i in range(3): for j in range(3): if grid[i, j] == Player.undefined: grid[i, j] = self.opponent score = self._evaluate_recursively(grid, self.active_player) grid[i, j] = Player.undefined best = min(best, score) return best def _evaluate_recursively(self, grid:np.ndarray, current:Player) -> float: """ Helper Method for calculating the best move. Recursively evaluate the board state from the perspective of `current`. """ winner = self.check_winner(grid) if winner == self.active_player: return 1 elif winner == get_opponent(self.active_player): return -1 # Draw if not any(cell == Player.undefined for row in grid for cell in row): return 0 if current == self.active_player: return self._maximize(grid) else: return self._minimize(grid) def get_best_move(self, player:Player|int) -> tuple[int, int]: """ Returns the best move for the given Player as a tuple of integers. """ self.active_player = self._get_player_from_int(player) self.opponent = get_opponent(self.active_player) best_score = -np.inf move = (-1, -1) for i in range(3): for j in range(3): if self.grid[i, j] == Player.undefined: self.grid[i, j] = self.active_player score = self._evaluate_recursively(self.grid, self.opponent) self.grid[i, j] = Player.undefined if score > best_score: best_score = score move = (i, j) return move if __name__ == "__main__": ttt = TicTacToe() #ttt.add(1, 1, 1) #ttt.print_grid() #ttt.add(2, 1, 2) ttt.print_grid() current_player = Player.one while ttt.check_winner() == Player.undefined: move = ttt.get_best_move(current_player) ttt.add(move[0], move[1], current_player) if current_player == Player.one: current_player = Player.two else: current_player = Player.one ttt.print_grid()