def __init__(self, name=None, strategy=RandomStrategy()): """ :param str name: the name of the Santorini player. :param Strategy strategy: the strategy the player employs. :return: :rtype: None """ # TODO: Discuss / Document for code walk. # Should we have the player store a board object and call the set_board() method within Player, # or should we have a Player.set_board() function that simply reassigns self.board to a board object that's # already had set_board called on it? # Pranav's opinion: I think we should do the first option, because it ensures the clients of player don't have # to know about the Board.set_board() function, which is almost a helper, since it doesn't have core # functionality that's exposed to users. May as well abstract that using a Player function. If we do option 2, # we'll also have to write an extra contract to make sure a board that's been passed in has had set_board() # called on it, which will be overly complicated. if not name: self.name = "LocalPlayer{}".format(Player.COUNT) elif not isinstance(name, str): raise ContractViolation("Name must be a string!") else: self.name = name if not isinstance(strategy, BaseStrategy): raise ContractViolation("Strategy must implement BaseStrategy interface!") self.board = Board() self.strategy = strategy self.color = None self.registered = False # shadow state Player.COUNT += 1
def place(self, board, color): """ Returns the worker placements for the player. CONTRACT: - Can only be called after Player.register() has been called. - Must be called before Player.play(). - Cannot be called more than once. - `board` must be a valid initial board (a board where all cell's heights are 0, and no workers of `self.color` are present any cell. :param list board: an instance of Board (refer to documentation of Board class). :param str color: A color (as defined in the documentation of Referee). :return: `list` of [position1, position2] denoting the position of the player's 1st and 2nd worker respectively. See `position`, `worker` in documentation of Board.py. :rtype: list """ if not self.registered: raise ContractViolation("Function must be called after player.register()!") if self.color: raise ContractViolation("Cannot call Player.place() again until game ends!") if not RuleChecker.is_valid_color(color): raise ContractViolation("Invalid color provided: {}".format(color)) self.color = color if not RuleChecker.is_legal_initial_board(board, self.color): raise ContractViolation("Invalid initial board provided: {}".format(board)) self.board.set_board(board) # TODO: potential contract needed to ensure set_board is called at start of every turn for player return self.strategy.get_placements(self.board, self.color)
def play(self, board): if not self.color: raise ContractViolation( "Function must be called after player.place()!") if not RuleChecker.is_legal_board(board): raise ContractViolation("Invalid board provided: {}".format(board)) if not self._check_board(board): raise IllegalPlay("Player provided with a cheating board.") print("cheater checking...") # debug return super().play(board)
def is_winning_play(board, worker, directions): """ Takes in a play (as specified above) and checks if it's a winning play. This entails checking if the `worker` is moving up to height 3, or if the play blocks the opposition player from making any moves or builds. CONTRACT: - `[worker, directions]` must be a legal play. :param Board board: :param string worker: :param list directions: :return: :rtype: bool """ if not RuleChecker.is_valid_worker(worker): raise ContractViolation("Invalid (or no) worker provided.") if not all(map(RuleChecker.is_valid_direction, directions)): raise ContractViolation("Invalid (or no) directions provided.") if not RuleChecker.is_legal_play(board, worker, directions): raise ContractViolation( "Illegal play passed into is_winning_play: {}".format( [worker, directions])) color = worker[:-1] available_colors = list(RuleChecker.COLORS) available_colors.remove(color) opp_color = available_colors[0] if len( directions ) == 1: # must be winning, since one direction is only legal in the winning case return True else: opp_cannot_play = False move_dir, build_dir = directions # simulate play board.move(worker, move_dir) board.build(worker, build_dir) # commented out to avoid circular imports # opposition_player_legal_plays = Strategy.get_legal_plays(board, opp_color) # if not opposition_player_legal_plays: # opp_cannot_play = True # undo play board.undo_build(worker, build_dir) board.move(worker, board.get_opposite_direction(move_dir)) return opp_cannot_play
def is_legal_play(board, worker, directions): """ :param Board board: :param string worker: :param list directions: :return: `True` if play is legal, `False` otherwise :rtype: bool """ build_dir = None if len(directions) == 1: move_dir = directions[0] elif len(directions) == 2: move_dir, build_dir = directions else: raise ContractViolation("Too many/few directions provided.") if RuleChecker.is_valid_move(board, worker, move_dir): if RuleChecker.is_winning_move(board, worker, move_dir): if build_dir is None: # checking for win return True else: return False elif build_dir is None: return False board.move(worker, move_dir) if RuleChecker.is_valid_build(board, worker, build_dir): return_val = True else: return_val = False board.move(worker, board.get_opposite_direction(move_dir)) # undo the move return return_val else: return False
def play(self, board): """ Returns the strategized play a player wants to execute on a given turn. :param list board: :return: a play (as defined above) :rtype: list """ if not self.color: raise ContractViolation("Function must be called after player.place()!") if not RuleChecker.is_legal_board(board): raise ContractViolation("Invalid board provided: {}".format(board)) self.board.set_board(board) play = self.strategy.get_play(self.board, self.color) print("sending play", play) # debug return play
def move(self, worker, direction): """ Moves the specified worker from it's cell to the cell adjacent to the worker's existing position in the specified direction, if all of the following are true: - the cell being moved to exists - the cell being moved to is not occupied - the height of the cell being moved to is not 4 - the height of the cell being moved to - the height of the worker's cell <= 1 A move entails: - editing the specified worker's cell from `[height, worker]` to `height` - editing the adjacent cell from it's `height` to `[height, worker]`. :param string worker: a worker (as defined above). :param string direction: a direction (as defined above). :return: a board (as specified above) edited to reflect the build. Nothing if move is invalid. :rtype: list, void """ if not RuleChecker.is_valid_worker( worker) or not RuleChecker.is_valid_direction(direction): raise ContractViolation( "Invalid (or no) worker / direction provided.") worker_row, worker_col, worker_height = self.get_worker_position( worker) adj_cell_row, adj_cell_col = self._get_adj_cell( worker_row, worker_col, direction) adj_cell_height = self.board[adj_cell_row][adj_cell_col] self.board[adj_cell_row][adj_cell_col] = [adj_cell_height, worker] self.board[worker_row][worker_col] = worker_height self.worker_positions[worker] = (adj_cell_row, adj_cell_col, adj_cell_height) return self.board
def undo_build(self, worker, direction): """ Decreases the height of the cell adjacent to the worker's position in the specified direction by 1, if all of the following are true: - the cell being destroyed on exists - the cell being destroyed on is not occupied - the height of the cell being built on is not 0. Note: should not be used outside the `Strategy` component. :param string worker: a worker (as defined above). :param string direction: a direction (as defined above). :return: a board (as specified above) edited to reflect the undoing of the build. Nothing if move is invalid. :rtype: list, void """ if not RuleChecker.is_valid_worker( worker) or not RuleChecker.is_valid_direction(direction): raise ContractViolation( "Invalid (or no) worker / direction provided.") worker_row, worker_col, worker_height = self.get_worker_position( worker) adj_cell_row, adj_cell_col = self._get_adj_cell( worker_row, worker_col, direction) self.board[adj_cell_row][adj_cell_col] -= 1 return self.board
def _update_board_with_play(self, play): """ Updates the board state with the play and returns `True` if the given play resulted in a win. `False` otherwise. CONTRACT: - Can only be called after _update_board_with_placements has been called for every player. :param list play: a play (as defined above) :return: Boolean indicating whether the play resulted in the player winning on that turn :rtype: bool """ if not RuleChecker.is_valid_play(play): raise ContractViolation("Play not in correct format.") worker, directions = play if (worker[:-1] != RuleChecker.COLORS[self.turn] or not RuleChecker.is_legal_play(self.board, worker, directions)): raise IllegalPlay("Illegal play made by {player}: {play}".format( player=self.players[self.turn].get_name(), play=play)) if len(directions) == 1: return True move_dir, build_dir = directions self.board.move(worker, move_dir) self.board.build(worker, build_dir) return False
def _examine_for_error( response ): # TODO: this method makes assumptions about how the Player operates. refactor if response == "InvalidCommand": raise InvalidCommand() if response == "IllegalPlay": raise IllegalPlay() if response == "ContractViolation": raise ContractViolation()
def is_valid_build(board, worker, direction): if not RuleChecker.is_valid_worker( worker) or not RuleChecker.is_valid_direction(direction): raise ContractViolation( "Invalid (or no) worker / direction provided.") adj_cell_height = board.get_height(worker, direction) return (board.neighboring_cell_exists(worker, direction) and not board.is_occupied(worker, direction) and adj_cell_height != 4)
def get_worker_position(self, worker): """ Returns the position of the given worker and the height at that position as one tuple of the form `(row, col, height)` by iterating through the the cells in `self.board`. :param string worker: a worker (as defined above). :return: the position of the given worker (as specified above) and the height at that position as one tuple of the form `(row, col, height)`. :rtype: tuple of ints """ if not RuleChecker.is_valid_worker(worker): raise ContractViolation( "Invalid worker provided: {}".format(worker)) # Note: would use a dictionary for O(1) access if the board wasn't being reset with every command. if worker in self.worker_positions: return self.worker_positions[worker] else: raise ContractViolation( "Worker does not exist in worker_dictionary!")
def get_name(self): """ :return: :rtype: str """ if not self.name: raise ContractViolation( "ProxyPlayer.register() must be called before get_name()!") return self.name
def is_legal_initial_board(board, color): """ Checks the validity of an initial board. :param list board: A board (as defined in the documentation of Board). :param string color: A color (as defined in the documentation of Referee). :return: 'True' if the board is a valid initial board, else 'False'. :rtype: bool """ if not RuleChecker.is_valid_color(color): raise ContractViolation("Invalid color provided: {}".format(color)) unset_workers = [color + "1", color + "2"] return RuleChecker.is_legal_board(board, unset_workers, 0)
def get_dimensions(self): """ CONTRACT: - cannot be called before set_board() has been called. :return: The dimensions of the board member variable in format (num_rows, num_cols) :rtype: tuple """ if not self.board: raise ContractViolation( "Cannot get dimensions if Board.board member variable has not been set!" ) return len(self.board), len(self.board[0])
def register(self): """ Returns the name of the player. CONTRACT: - Must be the first function to be called after Player is instantiated. - Cannot be called more than once. :return: the name of the player :rtype: str """ if self.registered: raise ContractViolation("Cannot call Player.register() more than once!") self.registered = True return self.name
def place_worker(self, row, col, worker): """ Places a worker at the cell at position (row, col) :param int row: `row` in a position (as defined above). :param int col: `col` in a position (as defined above). :param string worker: a worker (as defined above). :rtype: void """ if not RuleChecker.is_valid_worker(worker): raise ContractViolation( "Invalid worker provided: {}".format(worker)) if self.has_worker(row, col): raise IllegalPlay("Cannot place worker in occupied cell!") height = self.board[row][col] self.board[row][col] = [height, worker] self.worker_positions[worker] = (row, col, height)
def notify(self, winner_name): """ Notifies the player with the winner of the Santorini game. CONTRACT: - can only be called once per game - must be the last function to be called by an object that implements PlayerInterface. :param str winner_name: Name of the winner of the Santorini game :return: An acknowledgement string of "OK" :rtype: str """ if not self.registered: raise ContractViolation("Player.notify() cannot be called before register!") # resetting interaction protocol contracts for future games self.board = Board() self.color = None print("{} has won the game!".format(winner_name)) # debug print("------------------------------------------------") # debug return "OK"
def neighboring_cell_exists(self, worker, direction): """ Checks if the cell adjacent to the worker's position in the specified direction exists. A cell exists if it's position is within the bounds of the board, i.e. it is a valid position as defined above. :param string worker: a worker (as defined above). :param string direction: a direction (as defined above). :return: `True` if cell adjacent in the specified direction exists, else `False`. :rtype: bool """ if not RuleChecker.is_valid_worker( worker) or not RuleChecker.is_valid_direction(direction): raise ContractViolation( "Invalid (or no) worker / direction provided.") worker_row, worker_col, worker_height = self.get_worker_position( worker) adj_cell_row, adj_cell_col = Board._get_adj_cell( worker_row, worker_col, direction) return 0 <= adj_cell_row < len(self.board) and 0 <= adj_cell_col < len( self.board[0])
def is_occupied(self, worker, direction): """ Checks if the cell adjacent to the worker's position in the specified direction is occupied. A cell is occupied if it is a `list` of [height, worker]. :param string worker: a worker (as defined above). :param string direction: a direction (as defined above). :return: `True` if the cell adjacent to the worker's position in the specified direction exists and is occupied, `False`, if cell is unoccupied. Behaviour unspecified if cell adjacent cell doesn't exist. :rtype: bool, void """ if not RuleChecker.is_valid_worker( worker) or not RuleChecker.is_valid_direction(direction): raise ContractViolation( "Invalid (or no) worker / direction provided.") if self.neighboring_cell_exists(worker, direction): worker_row, worker_col, cell_height = self.get_worker_position( worker) adj_cell_row, adj_cell_col = self._get_adj_cell( worker_row, worker_col, direction) return self.has_worker(adj_cell_row, adj_cell_col)
def _update_board_with_placements(self, placements): """ Updates the board with the given placements. CONTRACT: - Can only be called after _register_player has been called for every player. - Can only be called once per distinct player per game (two players). :param list placements: a list of placements (as defined above) :return: :rtype: void """ if not RuleChecker.is_valid_placement(placements): raise ContractViolation("Placements not in correct format.") for placement in placements: if not RuleChecker.is_legal_placement(self.board, placement): raise IllegalPlay( "Invalid placement position given: {}".format(placement)) for worker_num, placement in enumerate(placements, 1): row, col = placement worker = RuleChecker.COLORS[self.turn] + str(worker_num) self.board.place_worker(row, col, worker)
def get_legal_plays(board, color): """ Returns a list of all possible legal plays for players of the given color. :param Board board: an instance of Board (refer to documentation of Board class). :param str color: color (as defined above) :return: a `list` of legal plays (as defined above) :rtype: list """ if not RuleChecker.is_valid_color(color): raise ContractViolation("Invalid color given: {}".format(color)) legal_plays = [] players = [str(color + "1"), str(color + "2")] player_movable_directions = [[], []] # Valid Move directions for direc in RuleChecker.DIRECTIONS: for i, player in enumerate(players): if RuleChecker.is_valid_move(board, player, direc): player_movable_directions[i].append(direc) # Constructing all possible legal plays for i, player in enumerate(players): for move_dir in player_movable_directions[i]: if RuleChecker.is_winning_move(board, player, move_dir): legal_plays.append([player, [move_dir]]) else: board.move(player, move_dir) for build_dir in RuleChecker.DIRECTIONS: if RuleChecker.is_valid_build(board, player, build_dir): legal_plays.append([player, [move_dir, build_dir]]) opp_dir = board.get_opposite_direction(move_dir) board.move(player, opp_dir) # undoing the move return legal_plays
def get_height(self, worker, direction): """ Returns the height of the cell adjacent to the worker's position in the specified direction. :param string worker: a worker (as defined above). :param string direction: a direction (as defined above). :return: the height of the cell adjacent to the worker's position in the specified direction. :rtype: int """ if not RuleChecker.is_valid_worker( worker) or not RuleChecker.is_valid_direction(direction): raise ContractViolation( "Invalid (or no) worker / direction provided.") if self.neighboring_cell_exists(worker, direction): worker_row, worker_col, worker_height = self.get_worker_position( worker) adj_cell_row, adj_cell_col = Board._get_adj_cell( worker_row, worker_col, direction) cell = self.board[adj_cell_row][adj_cell_col] if isinstance(cell, list): return cell[0] else: return cell
def __init__(self, num_looks_ahead=1): if not isinstance(num_looks_ahead, int) or num_looks_ahead < 1: raise ContractViolation( "num_looks_ahead must be a positive integer! Given: {}".format( num_looks_ahead)) self.num_looks_ahead = num_looks_ahead
def get_plays(board, color, num_look_ahead): # TODO - make private method """ Returns a list of all possible legal plays that cannot not result in the opposing player winning within the next `num_look_ahead` moves. CONTRACT: - `num_look_ahead` must be >= 1 :param Board board: an instance of Board (refer to documentation of Board class). :param str color: color (as defined above) :param int num_look_ahead: number of moves to look ahead by :return: a `list` of legal plays (as defined above) :rtype: `list` """ if not RuleChecker.is_valid_color(color): raise ContractViolation("Invalid color given: {}".format(color)) available_colors = list(RuleChecker.COLORS) available_colors.remove(color) opp_color = available_colors[0] result_plays = [] for play in NLooksAheadStrategy.get_legal_plays(board, color): # avoid circular import # if RuleChecker.is_winning_play(board, *play): # result_plays.append(play) # print("checking", play) # debug if len(play[1]) == 1: result_plays.append(play) else: opposition_win = False worker = play[0] move_dir, build_dir = play[1] # player play board.move(worker, move_dir) board.build(worker, build_dir) opp_legal_plays = NLooksAheadStrategy.get_legal_plays( board, opp_color) if any(len(opp_play[1]) == 1 for opp_play in opp_legal_plays): # try and prune search opposition_win = True else: for opp_play in opp_legal_plays: # avoid circular import # if RuleChecker.is_winning_play(board, *opp_play): # opposition_win = True # break if len(opp_play[1]) == 1: opposition_win = True break elif num_look_ahead > 1: opp_worker = opp_play[0] opp_move_dir, opp_build_dir = opp_play[1] # opposition play board.move(opp_worker, opp_move_dir) board.build(opp_worker, opp_build_dir) opposition_win = NLooksAheadStrategy._loses_in_n_moves( board, color, num_look_ahead - 1) # undoing opposition play board.undo_build(opp_worker, opp_build_dir) board.move( opp_worker, board.get_opposite_direction(opp_move_dir)) if opposition_win: break # undoing player play board.undo_build(worker, build_dir) board.move(worker, board.get_opposite_direction(move_dir)) if not opposition_win: result_plays.append(play) return result_plays
def is_winning_move(board, worker, direction): if not RuleChecker.is_valid_worker( worker) or not RuleChecker.is_valid_direction(direction): raise ContractViolation( "Invalid (or no) worker / direction provided.") return board.get_height(worker, direction) == 3