Spaces:
Sleeping
Sleeping
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 | |