from copy import deepcopy import numpy as np from typing import Union class Connect4: def __init__(self, board:'Connect4'=None, row:int = 6, col:int =7): self.row = row self.col = col self.player_1 = 1 self.player_2 = -1 self.board = np.zeros((self.row, self.col)) self.winning_start = None self.winning_end = None if board is not None: self.__dict__ = deepcopy(board.__dict__) def drop_piece(self, action: int) -> 'Connect4': board = Connect4(board=self) # Find the row in that column which is valid to drop piece valid_row_idx = sum(board.board[:, action] == 0) - 1 board.board[valid_row_idx, action] = self.player_1 (board.player_1, board.player_2) = (self.player_2, self.player_1) return board, board.is_win() # Get the encoded state for the board def get_state(self) -> np.ndarray: # Create a layer to state the player turn turn = np.ones_like(self.board) if self.player_1 == 1 else np.zeros_like(self.board) enc_state = np.stack( (self.board == -1, self.board == 0, self.board == 1, turn) ).astype(np.int32) return enc_state # check if the board results in a draw state def is_draw(self): return (self.board != 0).all() def is_win(self) -> Union[None, int]: # Initially no one is winner winner = None # Check for columns if self.col_win(): winner = self.player_2 # Check for rows elif self.row_win(): winner = self.player_2 # Check for diagonals elif self.diag_win(): winner = self.player_2 return winner # Check for column win def col_win(self) -> bool: # Iterate over each column for c in range(self.col): # for 4 consequtive rows for r in range(self.row-3): # if the the all 4 element are of player who made move then its win if sum(self.board[r:r+4, c] == self.player_2) == 4: self.winning_start = (c, r) self.winning_end = (c, r+3) return True return False # check for win in row def row_win(self) -> bool: # Iterate over each row for r in range(self.row): # For 4 consequtive cols for c in range(self.col-3): # If all of 4 elements are of player who made move then its win if sum(self.board[r, c:c+4] == self.player_2) == 4: self.winning_start = (c, r) self.winning_end = (c+3, r) return True return False # check for win in diagonal def diag_win(self) -> bool: # For a window of 4x4 if the main diag or other diag has # same disc of player who made move then its a win for r in range(self.row-3): for c in range(self.col-3): # Get a window of size 4x4 window = self.board[r:r+4, c:c+4] # If all 4 element of main diag(/) is player who made move then its win if sum(np.diag(window) == self.player_2) == 4: self.winning_start = (c+3, r) self.winning_end = (c, r+3) # print("WinningMain Diag: ", self.winning_start, " - ", self.winning_end) return True # If all 4 element of other diag(\) is player who made move then its win if sum(np.diag(window[:, ::-1]) == self.player_2) == 4: self.winning_start = (c, r) self.winning_end = (c+3, r+3) # print("WinningMain Diag: ", self.winning_start, " - ", self.winning_end) return True return False # get a list of valid move that can be played by the current player def get_valid_moves(self) -> np.array: valid_cols = [False]*self.col for c in range(self.col): if self.board[0, c] == 0: valid_cols[c] = True return np.array(valid_cols, dtype=bool) def __str__(self) -> str: print_str = "" for r in range(self.row): for c in range(self.col): print_str += f"{self.board[r, c]:>3.0f}" print_str += "\n" return print_str