def test_game_tree_complete_depth(self): # tests that the depth of the complete game tree is as expected board = Board(2, 5, {}, num_of_fish_per_tile=2) state = GameState(board, {1: "black", 2: "white"}, [2, 1], penguin_posns={1: [[0, 0], [0, 1], [0, 2], [1, 3]], 2: [[1, 0], [1, 1], [1, 2], [1, 4]]}) game = GameTree(state) game.next_layer() game.next_layer() assert game.next_layer() == "Game Done"
def test_execute_action_illegal(self): # tests the behavior of execute_action with regards to an illegal move board = Board(4, 5, {}, num_of_fish_per_tile=2) state = GameState(board, {1: "black", 2: "white"}, [2, 1], penguin_posns={1: [[0, 0], [0, 1], [0, 2], [1, 3]], 2: [[1, 0], [1, 1], [1, 2], [1, 4]]}) game = GameTree(state) game.next_layer() assert not game.execute_action(((-1,0), (3,0))) assert not game.execute_action(((1, 0), (4, 0)))
def test_execute_action_legal(self): # tests the behavior of execute_action with regards to a legal move board = Board(4, 5, {}, num_of_fish_per_tile=2) state = GameState(board, {1: "black", 2: "white"}, [2, 1], penguin_posns={1: [[0, 0], [0, 1], [0, 2], [1, 3]], 2: [[1, 0], [1, 1], [1, 2], [1, 4]]}) game = GameTree(state) game.next_layer() assert game.execute_action(((1, 0), (3, 0))) game.next_layer() game_tree_child_1 = game.get_map_action_to_child_nodes()[((1, 0), (3, 0))] assert game_tree_child_1.execute_action(((0,0), (2,0)))
def test_apply_to_all_children(self): # tests the apply to all children method board = Board(2, 5, {}, num_of_fish_per_tile=2) state = GameState(board, {1: "black", 2: "white"}, [2, 1], penguin_posns={1: [[0, 0], [0, 1], [0, 2], [1, 3]], 2: [[1, 0], [1, 1], [1, 2], [1, 4]]}) game = GameTree(state) game.next_layer() assert game.apply_to_all_children(game.score_at_state) == [2, 2] game.next_layer() game_tree_child_1 = game.get_map_action_to_child_nodes()[((1, 2), (0, 3))] assert game_tree_child_1.apply_to_all_children(game.score_at_state) == [2] game_tree_child_1 = game.get_map_action_to_child_nodes()[((1, 4), (0, 4))] assert game_tree_child_1.apply_to_all_children(game.score_at_state) == [2]
def test_game_tree_one_layer(self): # tests that the child nodes have the right turn and have added holes in the board, indicating a move being made board = Board(4, 5, {}, num_of_fish_per_tile=2) state = GameState(board, {1: "black", 2: "white"}, [2, 1], penguin_posns={1: [[0, 0], [0, 1], [0, 2], [1, 3]], 2: [[1, 0], [1, 1], [1, 2], [1, 4]]}) game = GameTree(state) game.next_layer() assert len(game.get_map_action_to_child_nodes()) == 19 for action, child_node in game.get_map_action_to_child_nodes().items(): assert child_node.get_map_action_to_child_nodes() == {} game.next_layer() game_tree_child_1 = game.get_map_action_to_child_nodes()[((1, 0), (2, 1))] assert game_tree_child_1.get_game_state().get_player_order() == [1, 2] assert len(game_tree_child_1.get_game_state().get_board().get_holes()) == 1 game_tree_child_2 = game.get_map_action_to_child_nodes()[((1, 0), (3, 1))] assert game_tree_child_2.get_game_state().get_player_order() == [1, 2] assert len(game_tree_child_2.get_game_state().get_board().get_holes()) == 1
class Strategy: def __init__(self, game_state, player_id): self.__game_state = game_state self.__game_tree = None self.__player_id = player_id """ Nothing -> Nothing Returns the next free spot available on the board, following a zig zag pattern that starts at the top left corner (when you reach the last column of a row, move to the row directly below). I interpret the top left corner as position [0,0] (row 0, column 0), the position directly to the right of it is [0,1]. The position directly below [0,0] is [1,0], etc. Please see board.py for more details on my coordinate system (which follows what I just described). There are no side effects because no game state is passed in; the referee will end up doing the placing based on what is returned by this function. Assumption: The board has enough free slots for which penguins can be placed. """ def zig_zag(self): board = self.__game_state.get_board() # Python ranges are exclusive at the end # i=0 i < len(board.get_tiles()) i++ for i in range(0, len(board.get_tiles())): # j=0 j < (board.get_tiles())[i] j++ for j in range(0, len(board.get_tiles()[i])): desired_posn = [i, j] if self.__game_state.is_unoccupied(desired_posn) and \ not self.__game_state.is_hole(desired_posn): return desired_posn """ int -> Action Picks the action that will realize the "best gain" (highest score/fish count) for the player whose turn it currently is, after looking ahead n > 0 turns in the tree. I return an Action because at the end of the day, the spec says that it is a player's CHOICE OF ACTION, NOT THAT THE PLAYER HAS TO PERFORM THAT ACTION. If the game terminates before n turns can be executed, then it just takes the player's score at the node where the game ends. If there is no Action to take, then it will return the empty Action that I described in the Action data def at the top of this file. Basically just an empty tuple (). """ def which_action_to_take(self, n): if n == 0: raise ValueError("N must be greater than 0") # instantiates the game tree because now we need it (next_layer must still be called to kick off the generator) self.__game_tree = GameTree(self.__game_state) self.__game_tree.next_layer() actions_to_scores = {} for action, node in self.__game_tree.get_map_action_to_child_nodes( ).items(): actions_to_scores[action] = (self.__minimax(node, n - 1)) # game over if actions_to_scores == {}: return () # this is the best gain optimal_score = max(actions_to_scores.values()) optimal_actions = [ action for action in actions_to_scores if actions_to_scores[action] == optimal_score ] return self.__get_optimal_action(optimal_actions) """ GameTree int -> int Finds the best gain (highest score) that this Player can obtain after n turns, assuming that all other Players want to minimize this Player's gain, via the minimax algorithm. This will terminate because you can see that I have a base case where I stop if the appropriate number of turns has been played or if the game is over at the given state. """ def __minimax(self, node, n): # base case, either the player has finished all their turns or there are no moves left after this move if n == 0 or node.get_game_state().is_game_over(): return node.get_game_state().get_player_score(self.__player_id) # it is the maximizing player's turn if node.get_game_state().get_player_order()[0] == self.__player_id: value = float( '-inf' ) # because we need the absolute lowest possible value to compare to node.next_layer() for action, child_node in node.get_map_action_to_child_nodes( ).items(): value = max(value, self.__minimax(child_node, n - 1)) return value # it is all opponents' turn else: value = float( '+inf' ) # because we need the absolute highest possible value to compare to node.next_layer() for action, child_node in node.get_map_action_to_child_nodes( ).items(): value = min(value, self.__minimax(child_node, n)) return value """ [int] [Action] -> Action Finds the Action, out of all the possible candidate Actions leading to the best gain (highest fish count), that the player should take in order to obtain the best gain. If there are multiple Actions that can lead to the same gain, tiebreaker functionality is executed. """ def __get_optimal_action(self, optimal_actions): # this means there are multiple optimal actions that can lead to the same best gain if len(optimal_actions) > 1: # tiebreaker for top most row of from position, top most col of from position optimal_actions = self.__tiebreaker(optimal_actions, 0) # tiebreaker top most row of to position, top most col of to position. It will terminate because there # cannot be two distinct Actions at the same time with the same from and to position: that would indicate duplicate Actions. if len(optimal_actions) > 1: optimal_actions = self.__tiebreaker(optimal_actions, 1) optimal_action = optimal_actions[0] else: optimal_action = optimal_actions[0] return optimal_action """ [Action] int -> [Action] The which_pos arg is one of the indexes in an Action for either a start position or destination position. The start position is the first tuple in the Action, so index would be 0, and the destination position is the second tuple in the Action, so index would be 1. Applies tiebreaker functionality to Actions. """ @staticmethod def __tiebreaker(lo_actions, which_pos): # the list of different positions, either from or to positions lo_pos = [] for action in lo_actions: lo_pos.append(action[which_pos]) # finding the lowest row/ lowest column final_pos = (sorted(lo_pos))[0] final_actions = [] for action in lo_actions: if action[which_pos] == final_pos: final_actions.append(action) return final_actions """ Nothing -> GameState Getter method for the game state class attribute. """ def get_game_state(self): return self.__game_state """ Nothing -> GameTree Getter method for the game tree class attribute. """ def get_game_tree(self): return self.__game_tree """ Nothing -> int Getter method for the player id class attribute. """ def get_player_id(self): return self.__player_id
def __is_illegal_action(self, action): game_tree = GameTree(self.__current_game_state) game_tree.next_layer() return action not in game_tree.get_map_action_to_child_nodes()