Ejemplo n.º 1
0
    def _get_corner_squares(self) -> Dict[str, Square]:
        """
        TODO
        :return:
        """
        corners: Dict[str, Square] = {}
        counter: int = 0
        top_left_corner: Square = self.squares[Pos2D(counter, counter)]
        while (top_left_corner.state != SquareState.CORNER):
            counter += 1
            top_left_corner = self.squares[Pos2D(counter, counter)]

        corners[Board._TOP_LEFT] = top_left_corner
        dist_between_corners: int = Board._NUM_COLS - 2 * counter - 1
        corners[Board._TOP_RIGHT] = \
            self.squares[top_left_corner.pos
                         + Pos2D(dist_between_corners, 0)]
        corners[Board._BOTTOM_LEFT] = \
            self.squares[top_left_corner.pos
                         + Pos2D(0, dist_between_corners)]
        corners[Board._BOTTOM_RIGHT] = \
            self.squares[top_left_corner.pos
                         + Pos2D(dist_between_corners, dist_between_corners)]

        return corners
Ejemplo n.º 2
0
    def _get_adjacent_squares(self, pos: Pos2D, direction: str = _OMNI) \
            -> List[Square]:
        """
        Returns a list of max 4 squares which are directly adjacent (up, down,
        left, right) of the given position. Can specify if only adjacent squares
        in a certain direction are wanted using the 'direction' parameter.
        :param pos: The position which we want adjacent squares for.
        :param direction: The desired direction to get the adjacent squares for.
        Can be horizontal, vertical, or omni i.e. both.
        :return: A list of adjacent squares.
        """
        adjacent_squares: List[Square] = []
        if (direction == Board._OMNI):
            # Recursively call this method to get the horizontally and
            # vertically adjacent squares.
            adjacent_squares.extend(
                self._get_adjacent_squares(pos, Board._HORIZONTAL))
            adjacent_squares.extend(
                self._get_adjacent_squares(pos, Board._VERTICAL))
        elif (direction == Board._HORIZONTAL):
            adjacent_squares = [
                self.squares.get(pos + Pos2D(1, 0)),
                self.squares.get(pos + Pos2D(-1, 0))
            ]
        elif (direction == Board._VERTICAL):
            adjacent_squares = [
                self.squares.get(pos + Pos2D(0, 1)),
                self.squares.get(pos + Pos2D(0, -1))
            ]

        return [square for square in adjacent_squares if square is not None]
Ejemplo n.º 3
0
    def create_from_string(round_num: int, game_phase: GamePhase) -> 'Board':
        """
        This method takes a string (that is a representation of the board in x-y
        format) and returns a board object. This also defines the round number
        and the game phase for the board.
        """
        new_board: Board = Board(None, round_num, game_phase)
        for row_i in range(Board._NUM_ROWS):
            row_string: List[str] = input().split(" ")
            for col_i, char in enumerate(row_string):
                pos: Pos2D = Pos2D(col_i, row_i)
                if (char == SquareState.CORNER.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, None, SquareState.CORNER)
                elif (char == SquareState.OPEN.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, None, SquareState.OPEN)
                elif (char == Player.WHITE.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, Piece(Player.WHITE), SquareState.OCCUPIED)
                elif (char == Player.BLACK.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, Piece(Player.BLACK), SquareState.OCCUPIED)

        return new_board
Ejemplo n.º 4
0
    def _select_squares(self, top_left_corner: Pos2D,
                        bottom_right_corner: Pos2D,
                        offset: Pos2D = Pos2D(0, 0)) -> List[Square]:
        """
        TODO: Make inclusive or exclusive? <- Kind of handled by offset, might be ugly though.
        :param top_left_corner:
        :param bottom_right_corner:
        :return:
        """
        squares: List[Square] = []

        # offset can make the selection inclusive.
        for row_i in range(top_left_corner.y, bottom_right_corner.y + offset.y):
            for col_i in range(top_left_corner.x, bottom_right_corner.x
                                                  + offset.x):
                squares.append(self.squares[Pos2D(col_i, row_i)])

        return squares
Ejemplo n.º 5
0
    def __str__(self) -> str:
        """
        Returns a string representation of the calling instance.
        """
        output: str = ""

        for row_i in range(Board._NUM_ROWS):
            for col_i in range(Board._NUM_COLS):
                pos: Pos2D = Pos2D(col_i, row_i)
                output += ("{} ".format(self.squares[pos].get_representation()))
            # Finished row, add new line.
            output += "\n"

        return output
Ejemplo n.º 6
0
    def _get_death_zone_changes(self) -> Tuple[List[Square], List[Square]]:
        """
        TODO
        Does not take into account the round num. Simply returns a tuple of
        (eliminated squares : new corners).
        """

        eliminated_squares: List[Square] = []
        new_corners: List[Square] = []

        original_corners: Dict[str, Square] = self._get_corner_squares()
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._TOP_LEFT].pos,
                                 original_corners[Board._TOP_RIGHT].pos,
                                 offset = Pos2D(0, 1)))
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._TOP_RIGHT].pos,
                                 original_corners[Board._BOTTOM_RIGHT].pos,
                                 offset = Pos2D(1, 1)))
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._BOTTOM_LEFT].pos,
                                 original_corners[Board._BOTTOM_RIGHT].pos,
                                 offset = Pos2D(0, 1)))
        # TODO Consider that this means that top left will be in eliminated_squares TWICE due to the first argument
        # to _select_squares always being inclusive.
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._TOP_LEFT].pos,
                                 original_corners[Board._BOTTOM_LEFT].pos,
                                 offset=Pos2D(1, 0)))

        new_corners.append(
            self.squares[original_corners[Board._TOP_LEFT].pos
                         + Pos2D(1, 1)])
        new_corners.append(
            self.squares[original_corners[Board._BOTTOM_LEFT].pos
                         + Pos2D(1, -1)])
        new_corners.append(
            self.squares[original_corners[Board._BOTTOM_RIGHT].pos
                         + Pos2D(-1, -1)])
        new_corners.append(
            self.squares[original_corners[Board._TOP_RIGHT].pos
                         + Pos2D(-1, 1)])



        return (eliminated_squares, new_corners)
Ejemplo n.º 7
0
    def _init_squares() -> Dict[Pos2D, Square]:
        """
        This method initializes all square objects for a complete board as if it
        were the beginning of the game.
        """
        squares: Dict[Pos2D, Square] = {}
        for col_i in range(Board._NUM_COLS):
            for row_i in range(Board._NUM_ROWS):
                pos: Pos2D = Pos2D(col_i, row_i)

                if (pos.x == 0 and (pos.y == 0 or pos.y == Board._NUM_ROWS - 1)
                        or pos.x == Board._NUM_COLS - 1 and
                    (pos.y == 0 or pos.y == Board._NUM_ROWS - 1)):
                    squares[pos] = Square(pos, None, SquareState.CORNER)
                else:
                    squares[pos] = Square(pos, None, SquareState.OPEN)

        return squares
Ejemplo n.º 8
0
class Board():
    """
    A structure that represents the board at a given point.
    """

    MOVING_PHASE_ROUND_START = 24

    # TODO Move these into Utilities? Elsewhere?
    _TOP_LEFT: str = "top left"
    _TOP_RIGHT: str = "top right"
    _BOTTOM_LEFT: str = "bottom left"
    _BOTTOM_RIGHT: str = "bottom right"
    _HORIZONTAL: str = "horizontal"
    _VERTICAL: str = "vertical"
    _OMNI: str = "omnidirectional"

    _NUM_COLS: int = 8
    _NUM_ROWS: int = 8

    # TODO: Better way to specify placement zone?
    _WHITE_PLACEMENT_ZONE_CORNER_POSITIONS: List[Pos2D] = \
        [Pos2D(0, 0), Pos2D(_NUM_COLS, _NUM_ROWS - 2)]
    _BLACK_PLACEMENT_ZONE_CORNER_POSITIONS: List[Pos2D] = \
        [Pos2D(0, 2), Pos2D(_NUM_COLS, _NUM_ROWS)]


    _DEATH_ZONE_ROUNDS: List[int] = [151, 215]

    # The minimum number of pieces a player can have on the board before they
    # lose.
    _MIN_NUM_PIECES_BEFORE_LOSS = 2

    # Structure that contains the squares as (position : square) pairs.
    squares: Dict[Pos2D, Square]
    # round_num is not the turn number. e.g. after each player has had a turn,
    # then round_num == 3.
    round_num: int
    # An enum used to indicate the current phase of the game.
    phase: GamePhase
    # Equals None while the game isn't done. If phase == FINISHED and winner is
    # None, that the game was a tie.
    winner: PlayerColor

    def __init__(self, squares: Optional[Dict[Pos2D, Square]], round_num: int,
                 phase: GamePhase, winner: PlayerColor = None):
        if (squares is None):
            self.squares = self._init_squares()
        else:
            self.squares = squares

        self.round_num = round_num
        self.phase = phase
        self.winner = winner

    def get_num_moves(self, player: PlayerColor) -> int:
        """
        This method takes a player and returns the number of possible moves that
        they can take.
        """
        player_squares: List[Square] = self.get_player_squares(player)
        count: int = 0
        for player_square in player_squares:
            adj_squares: List[Square] = \
                self._get_adjacent_squares(player_square.pos)
            for adj_square in adj_squares:
                if (adj_square.state == SquareState.OPEN):
                    count += 1
                elif(adj_square.state == SquareState.OCCUPIED):
                    opposite_square: Square = \
                        self.squares.get(
                            self._get_opposite_pos(player_square.pos,
                                                   adj_square.pos))
                    if (opposite_square is not None
                            and opposite_square.state == SquareState.OPEN):
                        count += 1

        return count

    def get_possible_placements(self, player: PlayerColor) -> List[Delta]:
        """
        TODO
        :return:
        """

        player_zone_corner_positions: List[Pos2D]
        if (player == PlayerColor.WHITE):
            player_zone_corner_positions = \
                Board._WHITE_PLACEMENT_ZONE_CORNER_POSITIONS
        else:
            player_zone_corner_positions = \
                Board._BLACK_PLACEMENT_ZONE_CORNER_POSITIONS

        # Get a list of all valid squares in the zone that the given player is
        # allowed to place pieces.
        valid_squares: List[Square] = \
            [square for square in
             self._select_squares(player_zone_corner_positions[0],
                                  player_zone_corner_positions[1])
             if square.state == SquareState.OPEN]

        return [Delta(player, None, square,
                      self._get_killed_positions(Piece(player), square.pos), [],
                      []) for square in valid_squares]

    def is_suicide(self, delta: Delta) -> bool:
        """
        Checks if a move will result in death during placement phase.
        It does this by checking if there is a corner or enemy player
        next to the position and then checks the corresponding opposite square
        if it is not your own.
        :param delta:
        :return:
        """
        # Get our adjacent squares
        adj_squares: List[Square] = \
            self._get_adjacent_squares(delta.move_target.pos)
        for adj_square in adj_squares:
            # Don't place next to corners.
            if (adj_square.state == SquareState.CORNER):
                return True

            if adj_square.state == SquareState.OCCUPIED and \
                    adj_square.occupant.owner == delta.player.opposite():
                opposite_square: Square = \
                    self.squares.get(Board._get_opposite_pos(
                        delta.move_target.pos, adj_square.pos), None)

                # Enemy is on edge, we're away from edge. That's suicide.
                if (opposite_square is None):
                    return True

                # If the opposite piece is not our own then it's a suicide
                if (opposite_square.state == SquareState.OCCUPIED and
                    opposite_square.occupant.owner != delta.player) and \
                        (opposite_square.state != SquareState.CORNER):
                    return True

        return False

    def get_possible_moves(self, pos: Pos2D) -> List[Delta]:
        """
        Given a position on the board, returns a list of possible moves (or
        'deltas') from that position.
        """
        possible_deltas: List[Delta] = []

        potential_square_eliminations: List[Square] = []
        potential_new_corners: List[Square] = []
        if (self.round_num in Board._DEATH_ZONE_ROUNDS):
            (potential_square_eliminations, potential_new_corners) = \
                self._get_death_zone_changes()

        adjacent_squares: List[Square] = self._get_adjacent_squares(pos)
        # For each adjacent square, determine if it can be moved to or jumped
        # over and then create and store the corresponding delta if possible.
        for adjacent_square in adjacent_squares:
            move_origin: Square
            move_target: Square

            if (adjacent_square.state == SquareState.OPEN):
                move_origin = self.squares[pos]
                move_target = adjacent_square
            elif (adjacent_square.state == SquareState.OCCUPIED):
                opposite_square: Square = \
                    self.squares.get(
                        self._get_opposite_pos(pos, adjacent_square.pos))
                if (opposite_square is not None
                        and opposite_square.state == SquareState.OPEN):
                    move_origin = self.squares[pos]
                    move_target = opposite_square
                else:
                    continue
            else:
                continue

            # Calculate kills that occur due to the change in corners
            # (if applicable).
            potential_corner_kills: List[Square] = []
            if (self.round_num in Board._DEATH_ZONE_ROUNDS):
                assert(len(potential_new_corners) == 4)

                adj_to_corner_squares: List[Square]
                for corner in potential_new_corners:
                    adj_to_corner_squares = self._get_adjacent_squares(corner.pos)
                    adj_occupied_squares: List[Square] = \
                        [square for square in adj_to_corner_squares if
                         square.state == SquareState.OCCUPIED]

                    # Determine of the move_target would be an adjacent
                    # position as well (edge case).
                    if (move_target in adj_to_corner_squares):
                        move_target_copy: Square = copy(move_target)
                        move_target_copy.occupant = move_origin.occupant
                        move_target_copy.state = move_origin.state
                        adj_occupied_squares.append(move_target_copy)

                    for adj_square in adj_occupied_squares:
                        opposite_square: Square = \
                            self.squares.get(self._get_opposite_pos(corner.pos,
                                                                    adj_square.pos))

                        if (opposite_square is None):
                            continue

                        # Edge case handling. If the delta involves moving to
                        # the opposite square, pretend it's already there.
                        if (opposite_square.pos == move_target.pos):
                            opposite_square = copy(opposite_square)
                            opposite_square.state = move_target.state
                            opposite_square.occupant = move_target.occupant

                        # First statement is edge case handling. If the delta
                        # involves moving away from the opposite square, it
                        # cannot kill adj_square.
                        if (opposite_square.pos != move_origin.pos
                                and opposite_square.state == SquareState.OCCUPIED
                                and adj_square.occupant.owner
                                != opposite_square.occupant.owner
                                and opposite_square not in potential_corner_kills):
                            potential_corner_kills.append(adj_square)

            potential_kills: List[Pos2D] = \
                self._get_killed_positions(move_origin.occupant,
                                           move_target.pos) \
                + [square.pos for square in potential_corner_kills]
            # TODO Here, add function to assess kills from new corners.
            delta: Delta = Delta(self.squares[pos].occupant.owner, move_origin,
                                 move_target, potential_kills,
                                 potential_square_eliminations,
                                 potential_new_corners)
            possible_deltas.append(delta)

        return possible_deltas

    def get_all_possible_deltas(self, player: PlayerColor) -> List[Delta]:
        """
        Returns a list of all possible moves for a given player.
        """

        if (self.phase == GamePhase.PLACEMENT):
            return self.get_possible_placements(player)

        if (self.phase == GamePhase.MOVEMENT):
            valid_moves: List[Delta] = []

            # Get a list of all squares that are occupied by the given player.
            player_squares: List[Square] = self._get_player_squares(player)
            # Iterate over each of these squares and add their valid moves to
            # 'valid_moves'.
            [valid_moves.extend(self.get_possible_moves(square.pos)) for square
             in player_squares]

            return valid_moves

        assert (self.phase == GamePhase.FINISHED)

        print("BOARD:")
        print("Round num:", self.round_num)
        print(self)

        # TODO:
        # There are cases where this happens with the referee, at least
        # in GeneticAlgorithmDriver. Figure out why.

        # We shouldn't get this far. Operation from now on is unpredictable.
        print("WARNING: I think the game is finished but "
              "get_all_possible_deltas was called anyway! I'll try my best to "
              "continue.")

        # Assume we're still in movement phase. Call recursively again.
        self.phase = GamePhase.MOVEMENT
        return self.get_all_possible_deltas(player)

    def get_next_board(self, delta: Delta) -> 'Board':
        """
        This method takes a delta and uses it to create a new board from the
        calling instance. This can be seen as actually making the move, once the
        move has been determined.
        """

        # Sanity checks to ensure we're calling the method correctly.
        assert (self.squares[delta.move_target.pos].state == SquareState.OPEN)
        if (delta.move_origin is not None):
            assert (self.squares[delta.move_origin.pos].state
                    == SquareState.OCCUPIED)

        next_board: Board = self.__deepcopy__()

        # Make sure that both the original and target squares are changed to
        # reflect change in the given delta.
        if (delta.move_origin is None):
            # This delta is a placement.
            next_board.squares[delta.move_target.pos].occupant = \
                Piece(delta.player)
        else:
            # This delta is a movement.
            next_board.squares[delta.move_target.pos].occupant = \
                delta.move_origin.occupant
            next_board.squares[delta.move_origin.pos].occupant = None
            next_board.squares[delta.move_origin.pos].state = SquareState.OPEN

        next_board.squares[delta.move_target.pos].state = SquareState.OCCUPIED

        # Update all the squares that had a killed piece.
        for pos in delta.killed_square_positions:
            next_board.squares[pos].occupant = None
            next_board.squares[pos].state = SquareState.OPEN

        for square in delta.eliminated_squares: # TODO: Make eliminated squares and new_corners also lists of positions instead?
            next_board.squares[square.pos].occupant = None
            next_board.squares[square.pos].state = SquareState.ELIMINATED

        for square in delta.new_corners:
            next_board.squares[square.pos].occupant = None
            next_board.squares[square.pos].state = SquareState.CORNER

        # Update the game state.
        next_board.round_num += 1
        next_board._update_game_phase()

        return next_board

    def get_player_squares(self, player: PlayerColor) -> List[Square]:
        """
        Returns a list of all squares that have a piece on them that is
        controlled by the given player.
        """
        return [square for square in self.squares.values() if
                square.state == SquareState.OCCUPIED
                and square.occupant.owner == player]

    def _get_adjacent_squares(self, pos: Pos2D, direction: str = _OMNI) \
            -> List[Square]:
        """
        Returns a list of max 4 squares which are directly adjacent (up, down,
        left, right) of the given position. Can specify if only adjacent squares
        in a certain direction are wanted using the 'direction' parameter.
        :param pos: The position which we want adjacent squares for.
        :param direction: The desired direction to get the adjacent squares for.
        Can be horizontal, vertical, or omni i.e. both.
        :return: A list of adjacent squares.
        """
        adjacent_squares: List[Square] = []
        if (direction == Board._OMNI):
            # Recursively call this method to get the horizontally and
            # vertically adjacent squares.
            adjacent_squares.extend(
                self._get_adjacent_squares(pos, Board._HORIZONTAL))
            adjacent_squares.extend(
                self._get_adjacent_squares(pos, Board._VERTICAL))
        elif (direction == Board._HORIZONTAL):
            adjacent_squares = [self.squares.get(pos + Pos2D(1, 0)),
                                self.squares.get(pos + Pos2D(-1, 0))]
        elif (direction == Board._VERTICAL):
            adjacent_squares = [self.squares.get(pos + Pos2D(0, 1)),
                                self.squares.get(pos + Pos2D(0, -1))]

        return [square for square in adjacent_squares if square is not None]

    def _get_killed_positions(self, moving_piece: Piece,
                              moving_piece_target_pos: Pos2D) -> List[Pos2D]:
        """
        This method takes a piece that is moving and it's target position and
        calculates a list of positions that will be killed as a result of the
        move. This may include the moving piece itself.
        """
        killed_positions: List[Pos2D] = []

        # Short for 'horizontally adjacent squares'.
        x_adjacent: List[Square] = \
            self._get_adjacent_squares(moving_piece_target_pos,
                                       Board._HORIZONTAL)
        y_adjacent: List[Square] = \
            self._get_adjacent_squares(moving_piece_target_pos,
                                       Board._VERTICAL)

        # Add any adjacent squares that would be killed if the given piece moved
        # to the given location.
        for adjacent_square in [*x_adjacent, *y_adjacent]:
            if (adjacent_square.state == SquareState.OCCUPIED
                    and adjacent_square.occupant.owner != moving_piece.owner):
                opposite_square: Square = \
                    self.squares.get(self._get_opposite_pos(
                        moving_piece_target_pos, adjacent_square.pos))

                if opposite_square is None:
                    # The opposite square out of bounds, move onto the next one.
                    continue

                # Check if the opposite piece is owned by the same player who's
                # making the move or if it is a corner square. If so, add the
                # adjacent square to the list of killed positions.
                if ((opposite_square.state == SquareState.OCCUPIED
                     and opposite_square.occupant.owner == moving_piece.owner
                     and opposite_square.occupant != moving_piece)
                        or opposite_square.state == SquareState.CORNER):
                    killed_positions.append(adjacent_square.pos)

        # Now check if the piece that is moving gets killed. This would
        # (probably) be a stupid move, but it could happen.

        # Remove references to adjacent squares if they are going to be killed,
        # as they then cannot kill the moving piece.
        [x_adjacent.remove(self.squares[pos]) for pos in killed_positions if
         self.squares[pos] in x_adjacent]
        [y_adjacent.remove(self.squares[pos]) for pos in killed_positions if
         self.squares[pos] in y_adjacent]

        # Filter out squares from both lists that are not occupied by enemy
        # pieces or aren't corners.
        potential_x_killer_squares = \
            [square for square in x_adjacent
             if ((square.state == SquareState.OCCUPIED
                  and (square.occupant.owner != moving_piece.owner))
                 or (square.state == SquareState.CORNER))]
        potential_y_killer_squares = \
            [square for square in y_adjacent
             if ((square.state == SquareState.OCCUPIED
                  and (square.occupant.owner != moving_piece.owner))
                 or (square.state == SquareState.CORNER))]

        # If either list still has two squares in them, that means that the
        # moving piece is doomed.
        if (len(potential_x_killer_squares) == 2
                or len(potential_y_killer_squares) == 2):
            killed_positions.append(moving_piece_target_pos)

        return killed_positions

    def _get_player_squares(self, player: PlayerColor) -> List[Square]:
        """
        TODO
        :param player:
        :return:
        """
        return [square for square in self.squares.values() if
                square.state == SquareState.OCCUPIED
                and square.occupant.owner == player]

    def _get_death_zone_changes(self) -> Tuple[List[Square], List[Square]]:
        """
        TODO
        Does not take into account the round num. Simply returns a tuple of
        (eliminated squares : new corners).
        """

        eliminated_squares: List[Square] = []
        new_corners: List[Square] = []

        original_corners: Dict[str, Square] = self._get_corner_squares()
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._TOP_LEFT].pos,
                                 original_corners[Board._TOP_RIGHT].pos,
                                 offset = Pos2D(0, 1)))
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._TOP_RIGHT].pos,
                                 original_corners[Board._BOTTOM_RIGHT].pos,
                                 offset = Pos2D(1, 1)))
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._BOTTOM_LEFT].pos,
                                 original_corners[Board._BOTTOM_RIGHT].pos,
                                 offset = Pos2D(0, 1)))
        # TODO Consider that this means that top left will be in eliminated_squares TWICE due to the first argument
        # to _select_squares always being inclusive.
        eliminated_squares.extend(
            self._select_squares(original_corners[Board._TOP_LEFT].pos,
                                 original_corners[Board._BOTTOM_LEFT].pos,
                                 offset=Pos2D(1, 0)))

        new_corners.append(
            self.squares[original_corners[Board._TOP_LEFT].pos
                         + Pos2D(1, 1)])
        new_corners.append(
            self.squares[original_corners[Board._BOTTOM_LEFT].pos
                         + Pos2D(1, -1)])
        new_corners.append(
            self.squares[original_corners[Board._BOTTOM_RIGHT].pos
                         + Pos2D(-1, -1)])
        new_corners.append(
            self.squares[original_corners[Board._TOP_RIGHT].pos
                         + Pos2D(-1, 1)])



        return (eliminated_squares, new_corners)

    def _get_corner_squares(self) -> Dict[str, Square]:
        """
        TODO
        :return:
        """
        corners: Dict[str, Square] = {}
        counter: int = 0
        top_left_corner: Square = self.squares[Pos2D(counter, counter)]
        while (top_left_corner.state != SquareState.CORNER):
            counter += 1
            top_left_corner = self.squares[Pos2D(counter, counter)]

        corners[Board._TOP_LEFT] = top_left_corner
        dist_between_corners: int = Board._NUM_COLS - 2 * counter - 1
        corners[Board._TOP_RIGHT] = \
            self.squares[top_left_corner.pos
                         + Pos2D(dist_between_corners, 0)]
        corners[Board._BOTTOM_LEFT] = \
            self.squares[top_left_corner.pos
                         + Pos2D(0, dist_between_corners)]
        corners[Board._BOTTOM_RIGHT] = \
            self.squares[top_left_corner.pos
                         + Pos2D(dist_between_corners, dist_between_corners)]

        return corners

    def _select_squares(self, top_left_corner: Pos2D,
                        bottom_right_corner: Pos2D,
                        offset: Pos2D = Pos2D(0, 0)) -> List[Square]:
        """
        TODO: Make inclusive or exclusive? <- Kind of handled by offset, might be ugly though.
        :param top_left_corner:
        :param bottom_right_corner:
        :return:
        """
        squares: List[Square] = []

        # offset can make the selection inclusive.
        for row_i in range(top_left_corner.y, bottom_right_corner.y + offset.y):
            for col_i in range(top_left_corner.x, bottom_right_corner.x
                                                  + offset.x):
                squares.append(self.squares[Pos2D(col_i, row_i)])

        return squares

    def _update_game_phase(self):
        """
        Checks the current state of the game and the board to determine if the
        game phase should change (and then makes that change).
        """

        if (self.round_num == Board.MOVING_PHASE_ROUND_START):
            self.phase = GamePhase.MOVEMENT

        if (self.phase == GamePhase.PLACEMENT):
            # We're still in the placement phase, so neither player can lose due
            # to a lack of pieces on the board. End the method call, making no
            # changes to the phase.
            return

        player_square_counts: Dict[PlayerColor, int] = \
            {PlayerColor.WHITE: len(self._get_player_squares(PlayerColor.WHITE)),
             PlayerColor.BLACK: len(self._get_player_squares(PlayerColor.BLACK))}

        if (player_square_counts[PlayerColor.WHITE]
                < Board._MIN_NUM_PIECES_BEFORE_LOSS
                and player_square_counts[PlayerColor.BLACK]
                < Board._MIN_NUM_PIECES_BEFORE_LOSS):
            # Tie
            self.winner = None
            self.phase = GamePhase.FINISHED
        elif (player_square_counts[PlayerColor.BLACK]
              < Board._MIN_NUM_PIECES_BEFORE_LOSS):
            # White wins
            self.winner = PlayerColor.WHITE
            self.phase = GamePhase.FINISHED
        elif (player_square_counts[PlayerColor.WHITE]
              < Board._MIN_NUM_PIECES_BEFORE_LOSS):
            # Black wins
            self.winner = PlayerColor.BLACK
            self.phase = GamePhase.FINISHED

    def __str__(self) -> str:
        """
        Returns a string representation of the calling instance.
        """
        output: str = ""

        for row_i in range(Board._NUM_ROWS):
            for col_i in range(Board._NUM_COLS):
                pos: Pos2D = Pos2D(col_i, row_i)
                output += ("{} ".format(self.squares[pos].get_representation()))
            # Finished row, add new line.
            output += "\n"

        return output

    def __deepcopy__(self, memodict={}) -> 'Board':
        """
        Returns a deep copy of the calling instance. Necessary in order to
        ensure that different boards don't reference the same Square objects in
        memory.
        """
        squares: Dict[Pos2D, Square] = deepcopy(self.squares)
        round_num: int = self.round_num
        phase: GamePhase = self.phase
        winner: PlayerColor = self.winner

        return Board(squares, round_num, phase, winner)

    @staticmethod
    def _init_squares() -> Dict[Pos2D, Square]:
        """
        This method initializes all square objects for a complete board as if it
        were the beginning of the game.
        """
        squares: Dict[Pos2D, Square] = {}
        for col_i in range(Board._NUM_COLS):
            for row_i in range(Board._NUM_ROWS):
                pos: Pos2D = Pos2D(col_i, row_i)

                if (pos.x == 0 and (
                        pos.y == 0 or pos.y == Board._NUM_ROWS - 1) or
                        pos.x == Board._NUM_COLS - 1 and (
                                pos.y == 0 or pos.y == Board._NUM_ROWS - 1)):
                    squares[pos] = Square(pos, None, SquareState.CORNER)
                else:
                    squares[pos] = Square(pos, None, SquareState.OPEN)

        return squares

    @staticmethod
    def create_from_string(round_num: int, game_phase: GamePhase) -> 'Board':
        """
        This method takes a string (that is a representation of the board in x-y
        format) and returns a board object. This also defines the round number
        and the game phase for the board.
        """
        new_board: Board = Board(None, round_num, game_phase)
        for row_i in range(Board._NUM_ROWS):
            row_string: List[str] = input().split(" ")
            for col_i, char in enumerate(row_string):
                pos: Pos2D = Pos2D(col_i, row_i)
                if (char == SquareState.CORNER.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, None, SquareState.CORNER)
                elif (char == SquareState.OPEN.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, None, SquareState.OPEN)
                elif (char == PlayerColor.WHITE.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, Piece(PlayerColor.WHITE),
                               SquareState.OCCUPIED)
                elif (char == PlayerColor.BLACK.get_representation()):
                    new_board.squares[pos] = \
                        Square(pos, Piece(PlayerColor.BLACK),
                               SquareState.OCCUPIED)

        return new_board

    @staticmethod
    def _get_opposite_pos(first_pos: Pos2D, second_pos: Pos2D) -> Pos2D:
        """
        Given two sequential, adjacent positions, return the third position in
        that sequence. This method can be seen as getting the position that is
        opposite of 'first_pos' in regards to 'second_pos'. This method always
        returns a Pos2D, regardless of if it marks a position off of the board,
        an eliminated square, etc. In other words, it does no checking to see if
        the position is valid.
        """
        displacement: Pos2D = second_pos - first_pos

        # Ensure that the two given positions are adjacent.
        assert (abs(displacement.x) + abs(displacement.y)) == 1

        return second_pos + displacement
    def update(self, action: Tuple[Union[int, Tuple[int]]]):
        """
        This method is called by the referee to inform your player about the opponent’s
        most recent move, so that you can maintain your internal board configuration.
        The input parameter action is a representation of the opponent’s recent action
        based on the instructions below, in the ‘Representing actions’ section.
        This method should not return anything.
        Note: update() is only called to notify your player about the opponent’s actions.
        Your player will not be notified about its own actions.

        - To represent the action of placing a piece on square (x,y), use a tuple (x,y).
        - To represent the action of moving a piece from square (a,b) to square (c,d), use
        a nested tuple ((a,b),(c,d)).
        - To represent a forfeited turn, use the value None.
        """

        # TODO
        # Easiest way to generate a Delta from 'action' seems to be to use
        # board.get_valid_movements or board.get_valid_placements and then
        # "getting" the Delta being made by matching the Pos2Ds.

        if (action is None):
            # Opponent forfeited turn.
            self._board.round_num += 1
            self._board._update_game_phase()
            return

        positions: List[Pos2D]

        if (type(action[0]) == int):
            positions = [Pos2D(action[0], action[1])]
        else:
            positions = [Pos2D(x, y) for x, y in action]

        opponent_delta: Delta = None
        deltas: List[Delta]
        if (len(positions) == 1):
            # Placement
            assert (self._board.phase == GamePhase.PLACEMENT)

            deltas = self._board.get_possible_placements(
                self._color.opposite())

            for delta in deltas:
                if delta.move_target.pos == positions[0]:
                    opponent_delta = delta
                    break

        elif (len(positions) == 2):
            # Movement.
            try:
                assert (self._board.phase == GamePhase.MOVEMENT)
            except AssertionError:
                print(
                    "WARNING: 'assert(self._board.phase == GamePhase.MOVEMENT)' FAILED.'"
                )
                print("SETTING PHASE = GAMEPHASE.MOVEMENT.")
                self._board.phase = GamePhase.MOVEMENT

            deltas = self._board.get_possible_moves(positions[0])

            for delta in deltas:
                if delta.move_target.pos == positions[1]:
                    opponent_delta = delta
                    break

        assert (opponent_delta is not None)

        self._board = self._board.get_next_board(opponent_delta)