File size: 13,597 Bytes
079c32c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
import copy
from typing import List, Dict, Any, Tuple, Union
import numpy as np


class Connect4RuleBot():
    """
    Overview:
        The rule-based bot for the Connect4 game. The bot follows a set of rules in a certain order until a valid move is found.\
        The rules are: winning move, blocking move, do not take a move which may lead to opponent win in 3 steps, \
        forming a sequence of 3, forming a sequence of 2, and a random move.
    """

    def __init__(self, env: Any, player: int) -> None:
        """
        Overview:
            Initializes the bot with the game environment and the player it represents.
        Arguments:
            - env: The game environment, which contains the game state and allows interactions with it.
            - player: The player that the bot represents in the game.
        """
        self.env = env
        self.current_player = player
        self.players = self.env.players

    def get_rule_bot_action(self, board: np.ndarray, player: int) -> int:
        """
        Overview:
            Determines the next action of the bot based on the current game board and player.
        Arguments:
            - board(:obj:`array`): The current game board.
            - player(:obj:`int`): The current player.
        Returns:
            - action(:obj:`int`): The next action of the bot.
        """
        self.legal_actions = self.env.legal_actions
        self.current_player = player
        self.next_player = self.players[0] if self.current_player == self.players[1] else self.players[1]
        self.board = np.array(copy.deepcopy(board)).reshape(6, 7)

        # Check if there is a winning move.
        for action in self.legal_actions:
            if self.is_winning_move(action):
                return action

        # Check if there is a move to block opponent's winning move.
        for action in self.legal_actions:
            if self.is_blocking_move(action):
                return action

        # Remove the actions which may lead to opponent to win.
        self.remove_actions()

        # If all the actions are removed, then randomly select an action.
        if len(self.legal_actions) == 0:
            return np.random.choice(self.env.legal_actions)

        # Check if there is a move to form a sequence of 3.
        for action in self.legal_actions:
            if self.is_sequence_3_move(action):
                return action

        # Check if there is a move to form a sequence of 2.
        for action in self.legal_actions:
            if self.is_sequence_2_move(action):
                return action

        # Randomly select a legal move.
        return np.random.choice(self.legal_actions)

    def is_winning_move(self, action: int) -> bool:
        """
        Overview:
            Checks if an action is a winning move.
        Arguments:
            - action(:obj:`int`): The action to be checked.
        Returns:
            - result(:obj:`bool`): True if the action is a winning move; False otherwise.
        """
        piece = self.current_player
        row = self.get_available_row(action)
        if row is None:
            return False
        temp_board = self.board.copy()
        temp_board[row][action] = piece
        return self.check_four_in_a_row(temp_board, piece)

    def is_winning_move_in_two_steps(self, action: int) -> bool:
        """
        Overview:
            Checks if an action can lead to win in 2 steps.
        Arguments:
            - action(:obj:`int`): The action to be checked.
        Returns:
            - result(:obj:`bool`): True if the action is a winning move; False otherwise.
         """
        piece = self.current_player
        row = self.get_available_row(action)
        if row is None:
            return False
        temp_board = self.board.copy()
        temp_board[row][action] = piece

        blocking_count = 0
        temp = [self.board.copy(), self.current_player]
        self.board = temp_board
        self.current_player = 3 - self.current_player
        legal_actions = [i for i in range(7) if self.board[0][i] == 0]
        for action in legal_actions:
            if self.is_winning_move(action):
                self.board, self.current_player = temp
                return False
            if self.is_blocking_move(action):
                blocking_count += 1
        self.board, self.current_player = temp
        if blocking_count >= 2:
            return True
        else:
            return False

    def is_blocking_move(self, action: int) -> bool:
        """
        Overview:
            Checks if an action can block the opponent's winning move.
        Arguments:
            - action(:obj:`int`): The action to be checked.
        Returns:
            - result(:obj:`bool`): True if the action can block the opponent's winning move; False otherwise.
        """
        piece = 2 if self.current_player == 1 else 1
        row = self.get_available_row(action)
        if row is None:
            return False
        temp_board = self.board.copy()
        temp_board[row][action] = piece
        return self.check_four_in_a_row(temp_board, piece)

    def remove_actions(self) -> None:
        """
        Overview:
            Remove the actions that may cause the opponent win from ``self.legal_actions``.
        """
        temp_list = self.legal_actions.copy()
        for action in temp_list:
            temp = [self.board.copy(), self.current_player]
            piece = self.current_player
            row = self.get_available_row(action)
            if row is None:
                break
            self.board[row][action] = piece
            self.current_player = self.next_player
            legal_actions = [i for i in range(7) if self.board[0][i] == 0]
            # print(f'if we take action {action}, then the legal actions for opponent are {legal_actions}')
            for a in legal_actions:
                if self.is_winning_move(a) or self.is_winning_move_in_two_steps(a):
                    self.legal_actions.remove(action)
                    # print(f"if take action {action}, then opponent take{a} may win")
                    # print(f"so we should take action from {self.legal_actions}")
                    break

            self.board, self.current_player = temp

    def is_sequence_3_move(self, action: int) -> bool:
        """
        Overview:
            Checks if an action can form a sequence of 3 pieces of the bot.
        Arguments:
            - action(:obj:`int`): The action to be checked.
        Returns:
            - result(:obj:`bool`): True if the action can form a sequence of 3 pieces of the bot; False otherwise.
        """
        piece = self.current_player
        row = self.get_available_row(action)
        if row is None:
            return False
        temp_board = self.board.copy()
        temp_board[row][action] = piece
        return self.check_sequence_in_neighbor_board(temp_board, piece, 3, action)

    def is_sequence_2_move(self, action: int) -> bool:
        """
        Overview:
            Checks if an action can form a sequence of 2 pieces of the bot.
        Arguments:
            - action(:obj:`int`): The action to be checked.
        Returns:
            - result(:obj:`bool`): True if the action can form a sequence of 2 pieces of the bot; False otherwise.
        """
        piece = self.current_player
        row = self.get_available_row(action)
        if row is None:
            return False
        temp_board = self.board.copy()
        temp_board[row][action] = piece
        return self.check_sequence_in_neighbor_board(temp_board, piece, 2, action)

    def get_available_row(self, col: int) -> bool:
        """
        Overview:
            Gets the available row for a given column.
        Arguments:
            - col(:obj:`int`): The column to be checked.
        Returns:
            - row(:obj:`int`): The available row in the given column; None if the column is full.
        """
        for row in range(5, -1, -1):
            if self.board[row][col] == 0:
                return row
        return None

    def check_sequence_in_neighbor_board(self, board: np.ndarray, piece: int, seq_len: int, action: int) -> bool:
        """
        Overview:
            Checks if a sequence of the bot's pieces of a given length can be formed in the neighborhood of a given action.
        Arguments:
            - board(:obj:`int`): The current game board.
            - piece(:obj:`int`): The piece of the bot.
            - seq_len(:obj:`int`) The length of the sequence.
            - action(:obj:`int`): The action to be checked.
        Returns:
            - result(:obj:`bool`): True if such a sequence can be formed; False otherwise.
        """
        # Determine the row index where the piece fell
        row = self.get_available_row(action)

        # Check horizontal locations
        for c in range(max(0, action - seq_len + 1), min(7 - seq_len + 1, action + 1)):
            window = list(board[row, c:c + seq_len])
            if window.count(piece) == seq_len:
                return True

        # Check vertical locations
        for r in range(max(0, row - seq_len + 1), min(6 - seq_len + 1, row + 1)):
            window = list(board[r:r + seq_len, action])
            if window.count(piece) == seq_len:
                return True

        # Check positively sloped diagonals
        for r in range(6):
            for c in range(7):
                if r - c == row - action:
                    window = [board[r - i][c - i] for i in range(seq_len) if 0 <= r - i < 6 and 0 <= c - i < 7]
                    if len(window) == seq_len and window.count(piece) == seq_len:
                        return True

        # Check negatively sloped diagonals
        for r in range(6):
            for c in range(7):
                if r + c == row + action:
                    window = [board[r - i][c + i] for i in range(seq_len) if 0 <= r - i < 6 and 0 <= c + i < 7]
                    if len(window) == seq_len and window.count(piece) == seq_len:
                        return True

        return False

    def check_four_in_a_row(self, board: np.ndarray, piece: int) -> bool:
        """
        Overview:
            Checks if there are four of the bot's pieces in a row on the current game board.
        Arguments:
            - board(:obj:`int`): The current game board.
            - piece(:obj:`int`): The piece of the bot.
        Returns:
            - Result(:obj:`bool`): True if there are four of the bot's pieces in a row; False otherwise.
        """
        # Check horizontal locations
        for col in range(4):
            for row in range(6):
                if board[row][col] == piece and board[row][col + 1] == piece and board[row][col + 2] == piece and \
                        board[row][col + 3] == piece:
                    return True

        # Check vertical locations
        for col in range(7):
            for row in range(3):
                if board[row][col] == piece and board[row + 1][col] == piece and board[row + 2][col] == piece and \
                        board[row + 3][col] == piece:
                    return True

        # Check positively sloped diagonals
        for row in range(3):
            for col in range(4):
                if board[row][col] == piece and board[row + 1][col + 1] == piece and board[row + 2][
                    col + 2] == piece and board[row + 3][col + 3] == piece:
                    return True

        # Check negatively sloped diagonals
        for row in range(3, 6):
            for col in range(4):
                if board[row][col] == piece and board[row - 1][col + 1] == piece and board[row - 2][
                    col + 2] == piece and board[row - 3][col + 3] == piece:
                    return True

        return False

    # not used now in this class
    def check_sequence_in_whole_board(self, board: np.ndarray, piece: int, seq_len: int) -> bool:
        """
        Overview:
            Checks if a sequence of the bot's pieces of a given length can be formed anywhere on the current game board.
        Arguments:
            - board(:obj:`int`): The current game board.
            - piece(:obj:`int`): The piece of the bot.
            - seq_len(:obj:`int`): The length of the sequence.
        Returns:
            - result(:obj:`bool`): True if such a sequence can be formed; False otherwise.
        """
        # Check horizontal locations
        for row in range(6):
            row_array = list(board[row, :])
            for c in range(8 - seq_len):
                window = row_array[c:c + seq_len]
                if window.count(piece) == seq_len:
                    return True

        # Check vertical locations
        for col in range(7):
            col_array = list(board[:, col])
            for r in range(7 - seq_len):
                window = col_array[r:r + seq_len]
                if window.count(piece) == seq_len:
                    return True

        # Check positively sloped diagonals
        for row in range(6 - seq_len):
            for col in range(7 - seq_len):
                window = [board[row + i][col + i] for i in range(seq_len)]
                if window.count(piece) == seq_len:
                    return True

        # Check negatively sloped diagonals
        for row in range(seq_len - 1, 6):
            for col in range(7 - seq_len):
                window = [board[row - i][col + i] for i in range(seq_len)]
                if window.count(piece) == seq_len:
                    return True