|
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) |
|
|
|
|
|
for action in self.legal_actions: |
|
if self.is_winning_move(action): |
|
return action |
|
|
|
|
|
for action in self.legal_actions: |
|
if self.is_blocking_move(action): |
|
return action |
|
|
|
|
|
self.remove_actions() |
|
|
|
|
|
if len(self.legal_actions) == 0: |
|
return np.random.choice(self.env.legal_actions) |
|
|
|
|
|
for action in self.legal_actions: |
|
if self.is_sequence_3_move(action): |
|
return action |
|
|
|
|
|
for action in self.legal_actions: |
|
if self.is_sequence_2_move(action): |
|
return action |
|
|
|
|
|
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] |
|
|
|
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) |
|
|
|
|
|
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. |
|
""" |
|
|
|
row = self.get_available_row(action) |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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. |
|
""" |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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. |
|
""" |
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|