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