class Configuration: def __init__(self, config_dict): self.rows = config_dict["rows"] self.columns = config_dict["columns"] self.inarow = config_dict["inarow"] class Observation: def __init__(self, obs_dict): self.board = obs_dict["board"] self.mark = obs_dict["mark"] def my_agent(observation, configuration): """ ConnectX agent using Minimax algorithm with alpha-beta pruning Args: observation: Current game state configuration: Game configuration Returns: Column number (0-based) where to drop the piece """ import numpy as np # Constants EMPTY = 0 MAX_DEPTH = 6 # Search depth limit INFINITY = float('inf') def make_board(obs): """Convert observation to 2D numpy array""" return np.asarray(obs.board).reshape(configuration.rows, configuration.columns) def get_valid_moves(board): """Get list of valid moves (columns that aren't full)""" return [col for col in range(configuration.columns) if board[0][col] == EMPTY] def drop_piece(board, col, piece): """Drop piece in specified column and return row position""" row = np.where(board[:, col] == EMPTY)[0][-1] board[row, col] = piece return row def check_window(window, piece, inarow): """ Score a window of positions Higher scores for more pieces in a row and potential winning moves Negative scores for opponent's threatening positions """ score = 0 opp_piece = 1 if piece == 2 else 2 # Winning position if np.count_nonzero(window == piece) == inarow: score += 100 # One move away from winning elif np.count_nonzero(window == piece) == (inarow - 1) and np.count_nonzero(window == EMPTY) == 1: score += 10 # Two moves away from winning elif np.count_nonzero(window == piece) == (inarow - 2) and np.count_nonzero(window == EMPTY) == 2: score += 5 # Opponent one move away from winning - defensive move needed if np.count_nonzero(window == opp_piece) == (inarow - 1) and np.count_nonzero(window == EMPTY) == 1: score -= 80 return score def score_position(board, piece): """ Score entire board position Considers horizontal, vertical, and diagonal possibilities Extra weight for center column control """ score = 0 # Horizontal windows for row in range(configuration.rows): for col in range(configuration.columns - (configuration.inarow - 1)): window = board[row, col:col + configuration.inarow] score += check_window(window, piece, configuration.inarow) # Vertical windows for row in range(configuration.rows - (configuration.inarow - 1)): for col in range(configuration.columns): window = board[row:row + configuration.inarow, col] score += check_window(window, piece, configuration.inarow) # Positive diagonal windows for row in range(configuration.rows - (configuration.inarow - 1)): for col in range(configuration.columns - (configuration.inarow - 1)): window = [board[row + i][col + i] for i in range(configuration.inarow)] score += check_window(window, piece, configuration.inarow) # Negative diagonal windows for row in range(configuration.inarow - 1, configuration.rows): for col in range(configuration.columns - (configuration.inarow - 1)): window = [board[row - i][col + i] for i in range(configuration.inarow)] score += check_window(window, piece, configuration.inarow) # Center column control bonus center_array = board[:, configuration.columns//2] center_count = np.count_nonzero(center_array == piece) score += center_count * 6 return score def is_terminal_node(board): """Check if current position is terminal (game over)""" # Check horizontal wins for row in range(configuration.rows): for col in range(configuration.columns - (configuration.inarow - 1)): window = list(board[row, col:col + configuration.inarow]) if window.count(1) == configuration.inarow or window.count(2) == configuration.inarow: return True # Check vertical wins for row in range(configuration.rows - (configuration.inarow - 1)): for col in range(configuration.columns): window = list(board[row:row + configuration.inarow, col]) if window.count(1) == configuration.inarow or window.count(2) == configuration.inarow: return True # Check if board is full return len(get_valid_moves(board)) == 0 def minimax(board, depth, alpha, beta, maximizing_player): """ Minimax algorithm with alpha-beta pruning Returns best move and its score """ valid_moves = get_valid_moves(board) is_terminal = is_terminal_node(board) # Base cases: max depth reached or terminal position if depth == 0 or is_terminal: if is_terminal: return (None, -INFINITY if maximizing_player else INFINITY) else: return (None, score_position(board, observation.mark)) if maximizing_player: value = -INFINITY column = np.random.choice(valid_moves) for col in valid_moves: board_copy = board.copy() drop_piece(board_copy, col, observation.mark) new_score = minimax(board_copy, depth-1, alpha, beta, False)[1] if new_score > value: value = new_score column = col alpha = max(alpha, value) if alpha >= beta: break return column, value else: value = INFINITY column = np.random.choice(valid_moves) opponent_piece = 1 if observation.mark == 2 else 2 for col in valid_moves: board_copy = board.copy() drop_piece(board_copy, col, opponent_piece) new_score = minimax(board_copy, depth-1, alpha, beta, True)[1] if new_score < value: value = new_score column = col beta = min(beta, value) if alpha >= beta: break return column, value # Main game logic board = make_board(observation) valid_moves = get_valid_moves(board) # First move: take center column if len(np.where(board != 0)[0]) == 0: return configuration.columns // 2 # Check for immediate winning moves for col in valid_moves: board_copy = board.copy() drop_piece(board_copy, col, observation.mark) if is_terminal_node(board_copy): return col # Use minimax to find best move column, minimax_score = minimax(board, MAX_DEPTH, -INFINITY, INFINITY, True) return column