File size: 7,446 Bytes
f5067fa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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