Source code for colosseumrl.envs.blokus.board

''' 
Author: Caleb Pitts
Date: 3/15/19
 
Summary:
Handles state of hte board, piece placements, and valid move driver methods.

COLOR ENCODINGS
0 - Blank
1 - Red
2 - Blue
3 - Green
4 - Yellow
'''

from collections import defaultdict
from copy import deepcopy
import numpy as np
from . import computation as comp

# Stores structure of all playable pieces
# key: piece name:
# val: default offsets from index
PIECE_TYPES = {"monomino1": np.array([(0, 0)]),
               "domino1": np.array([(0, 0), (1, 0)]),
               "trominoe1": np.array([(0, 0), (1, 0), (1, 1)]),
               "trominoe2": np.array([(0, 0), (1, 0), (2, 0)]),
               "tetrominoes1": np.array([(0, 0), (1, 0), (0, 1), (1, 1)]),
               "tetrominoes2": np.array([(0, 0), (1, -1), (1, 0), (2, 0)]),
               "tetrominoes3": np.array([(0, 0), (1, 0), (2, 0), (3, 0)]),
               "tetrominoes4": np.array([(0, 0), (1, 0), (2, 0), (2, -1)]),
               "tetrominoes5": np.array([(0, 0), (1, 0), (1, -1), (2, -1)]),
               "pentominoe1": np.array([(0, 0), (0, -1), (1, 0), (2, 0), (3, 0)]),
               "pentominoe2": np.array([(0, 0), (0, -1), (0, 1), (1, 0), (2, 0)]),
               "pentominoe3": np.array([(0, 0), (0, -1), (0, -2), (1, -2), (2, -2)]),
               "pentominoe4": np.array([(0, 0), (1, 0), (1, -1), (2, -1), (3, -1)]),
               "pentominoe5": np.array([(0, 0), (0, 1), (1, 0), (2, 0), (2, -1)]),
               "pentominoe6": np.array([(0, 0), (1, 0), (2, 0), (3, 0), (4, 0)]),
               "pentominoe7": np.array([(0, 0), (1, 0), (2, 0), (1, -1), (2, -1)]),
               "pentominoe8": np.array([(0, 0), (0, 1), (1, 0), (1, -1), (2, -1)]),
               "pentominoe9": np.array([(0, 0), (1, 0), (0, 1), (0, 2), (1, 2)]),
               "pentominoe10": np.array([(0, 0), (1, 0), (1, -1), (1, 1), (2, -1)]),
               "pentominoe11": np.array([(0, 0), (-1, 0), (1, 0), (0, -1), (0, 1)]),
               "pentominoe12": np.array([(0, 0), (1, 0), (1, -1), (2, 0), (3, 0)])}

# All possible piece orientations, listed in clockwise order
ORIENTATIONS = ["north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest"]

# Default starting corners for each player (0 to 3)
PLAYER_DEFAULT_CORNERS = [(0, 0), (19, 0), (0, 19), (19, 19)]

BOARD_TO_PLAYER_OBSERVATION_ROTATION_MATRICES = np.array([
    [[1, 0],
     [0, 1]],
    [[0, -1],
     [1, 0]],
    [[-1, 0],
     [0, -1]],
    [[0, 1],
     [-1, 0]]
], dtype=np.int32)


PLAYER_OBSERVATION_TO_BOARD_ROTATION_MATRICES = np.array([
    [[1, 0],
     [0, 1]],
    [[0, 1],
     [-1, 0]],
    [[-1, 0],
     [0, -1]],
    [[0, -1],
     [1, 0]]
], dtype=np.int32)

[docs]class Board: def __init__(self, copy_from_board=None): self.reset_board(copy_from_board)
[docs] def reset_board(self, copy_from_board=None): ''' Creates empty 2-dimensional 20 by 20 numpy zeros array that represents a clean board ''' if copy_from_board is not None: self.board_contents = deepcopy(copy_from_board.board_contents) else: self.board_contents = np.zeros((20, 20), dtype=int)
[docs] def update_board(self, player_color, piece_type, index, piece_orientation, round_count, ai_game): ''' Takes index point and places piece_type on board index[0] = x coord index[1] = y coord ''' self.player_color = player_color for offset in comp.shift_offsets(PIECE_TYPES[piece_type], int(piece_orientation[-1])): # NEW: Shifts offset orientation according to last character in piece orientation if offset[0] == 0 and offset[1] == 0: self.place_piece(index[0] + offset[0], index[1] + offset[1]) # Orientation doesn't matter since (0, 0) is the reference point else: new_x, new_y = comp.rotate_piece(index, offset[0], offset[1], piece_orientation) self.place_piece(new_x, new_y)
[docs] def place_piece(self, x, y): ''' Places piece on board by filling board_contents with the current player color ''' self.board_contents[y][x] = self.player_color
# def gather_empty_board_corners(self, corners_coords): # ''' Checks what corners are still available to play in the first round of the game # ''' # empty_corners = [] # for corner in corners_coords: # if self.board_contents[corner[1]][corner[0]] == 0: # empty_corners.append((corner[0], corner[1])) # return empty_corners
[docs] def gather_empty_corner_indexes(self, player_color): ''' Returns a list of tuples with the indexes of empty corner cells that connect to the player's color. The corner_index is not adjecent/touching any same color tiles on its sides beside its corners. ''' empty_corner_indexes = [] for row_num, row in enumerate(self.board_contents): for col_num, cell in enumerate(row): if cell == 0: # Check if cell is empty if self.check_valid_corner(self.board_contents, player_color, row_num, col_num): # If cell lines up with any adjacent piece that is the same color, its an invalid move, otherwise valid empty_corner_indexes.append((col_num, row_num)) return empty_corner_indexes
[docs] def check_valid_corner(self, board_contents, player_color, row_num, col_num): ''' Checks whether all adjacent pieces are a different color to the current player's color. Checks if any corner piece is the same color as the current player. ''' if not comp.is_valid_adjacents(board_contents, row_num, col_num, player_color): # Not a valid corner if adjacents are same color return False # Exclude top and right edge cases if row_num != 0 and col_num != 19: if board_contents[row_num - 1][col_num + 1] == player_color: return True # Exclude top and left cases if row_num != 0 and col_num != 0: if board_contents[row_num - 1][col_num - 1] == player_color: return True # Exclude bottom and right edge cases if row_num != 19 and col_num != 19: if board_contents[row_num + 1][col_num + 1] == player_color: return True # Exclude bottom and left cases if row_num != 19 and col_num != 0: if board_contents[row_num + 1][col_num - 1] == player_color: return True return False
[docs] def check_orientation_shifts(self, player_color, piece_type, index, orientation): ''' DESCRIPTION: Shifts piece N times where N is how large the piece is. Each piece shift is then checked to see whether it is a valid move. PARAMETERS: player_color: int indicating current player color piece_type: string mapping to set of default offsets (defined in global space) index: tuple representing index coords on board orientation: string specifying the current orientation being checked RETURNS: List of all offset lists where a shift at that index and orientation is possible ''' shifted_offsets = comp.get_all_shifted_offsets(PIECE_TYPES[piece_type], orientation) valid_shift_offsets = comp.check_shifted(self.board_contents, player_color, index, orientation, shifted_offsets) return valid_shift_offsets
[docs] def get_all_valid_moves(self, round_count, player_color, player_pieces): ''' Gathers all valid moves on the board that meet the following criteria: - Index of selected piece touches same-colored corner of a piece - Player piece does not fall outside of the board - Player piece does not overlap any of their pieces or other opponent pieces - May lay adjacent to another piece as long as its another color ''' if round_count == 0: # If still first round of game.. # empty_corner_indexes = self.gather_empty_board_corners([(0, 0), (19, 0), (0, 19), (19, 19)]) empty_corner_indexes = [PLAYER_DEFAULT_CORNERS[player_color-1]] else: empty_corner_indexes = self.gather_empty_corner_indexes(player_color) all_valid_moves = {} for piece_type in player_pieces: # Loop through all pieces the player currently has all_index_orientations = defaultdict(list) # Valid indexes with their valid orientations dict created for every piece for index in empty_corner_indexes: # Loop through all indexes where a piece can be placed for orientation in ORIENTATIONS: # Loop through all possible orientations at an unshifted index for shifted_id in self.check_orientation_shifts(player_color, piece_type, index, orientation): # NEW: Loop through all shifted offsets of a particular orientation, if none valid, nothing gets added all_index_orientations[index].append(orientation + str(shifted_id)) # NEW: shifted_id is the cell(s) in piece where a shift is possible if len(list(all_index_orientations.keys())) > 0: # If there are valid indexes for the piece type.. all_valid_moves[piece_type] = all_index_orientations return all_valid_moves
[docs] def decode_color(self, player_color): ''' Converts int representatio of player color to string representation. ''' player_color_codes = {1: "R", 2: "B", 3: "G", 4: "Y"} return player_color_codes[player_color]
[docs] def calculate_winner(self, players, round_count): ''' Returns the winner player color. ''' scores = [] max_score = 0 winner = "NONE" for current_player in players: scores.append((current_player.player_color, current_player.player_score)) if current_player.player_score > max_score: max_score = current_player.player_score # print("FINAL SCORES:") for player_color, score in sorted(scores, key=lambda x: x[1]): # print(self.decode_color(player_color), score) if score == max_score: # Prints all scores equal to the max score (accounts for ties) winner = self.decode_color(player_color) return winner