Connect-X / game_logic.py
alperugurcan's picture
Create game_logic.py
f5067fa verified
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