from __future__ import annotations from functools import cache import numpy as np WIN_LINES = [ (0, 1, 2), (3, 4, 5), (6, 7, 8), (0, 3, 6), (1, 4, 7), (2, 5, 8), (0, 4, 8), (2, 4, 6), ] def winner(board: np.ndarray) -> int: for left, middle, right in WIN_LINES: value = int(board[left]) if value and value == board[middle] == board[right]: return value return 0 def is_terminal(board: np.ndarray) -> bool: return winner(board) != 0 or not np.any(board == 0) def legal_actions(board: np.ndarray) -> np.ndarray: return np.flatnonzero(board == 0) def play(board: np.ndarray, action: int, player: int) -> np.ndarray: if board[action] != 0: raise ValueError(f"Cell {action} is occupied.") next_board = board.copy() next_board[action] = player return next_board def canonical(board: np.ndarray, player: int) -> np.ndarray: return board.astype(np.float32) * player def symmetries( state: np.ndarray, policy: np.ndarray, ) -> list[tuple[np.ndarray, np.ndarray]]: board = state.reshape(3, 3) probabilities = policy.reshape(3, 3) transformed = [] for rotations in range(4): rotated_board = np.rot90(board, rotations) rotated_policy = np.rot90(probabilities, rotations) transformed.append( (rotated_board.reshape(-1).copy(), rotated_policy.reshape(-1).copy()) ) transformed.append( ( np.fliplr(rotated_board).reshape(-1).copy(), np.fliplr(rotated_policy).reshape(-1).copy(), ) ) return transformed @cache def minimax_value(board_key: tuple[int, ...], player: int) -> int: board = np.asarray(board_key, dtype=np.int8) result = winner(board) if result: return 1 if result == player else -1 legal = legal_actions(board) if not len(legal): return 0 return max( -minimax_value(tuple(play(board, int(action), player)), -player) for action in legal ) def minimax_action(board: np.ndarray, player: int) -> int: legal = legal_actions(board) values = [ -minimax_value(tuple(play(board, int(action), player)), -player) for action in legal ] best = max(values) return int(legal[values.index(best)])