def test_stuck_state(self): """ Purpose: Show generation of children when the current player cannot move """ player_row1 = [(0, 0), (0, 2), (1, 1)] player_row2 = [(2, 0), (3, 1), (2, 2)] small_board = FishBoardModel.create_with_same_fish_amount(3, 3, 3) game_state = FishGameStateFactory.create_move_penguins_state( small_board, [(PlayerColor.RED, player_row1, 0), (PlayerColor.BLACK, player_row2, 0)], check_penguin_amount=False ) stuck_tree = FishGameTree(game_state) self.assertEqual(1, len(list(stuck_tree.generate_direct_children()))) self.assertEqual(PlayerColor.BLACK, list(stuck_tree.generate_direct_children())[0].get_turn_color())
def test_not_move_state(self): """ Purpose: Test trying to create a tree from a state that is not the player movement state """ with self.assertRaises(ValueError): small_board = FishBoardModel.create_with_same_fish_amount(4, 3, 3) game_state = FishGameStateFactory.create_place_penguins_state( small_board, [(PlayerColor.RED, []), (PlayerColor.BLACK, [])] ) FishGameTree(game_state)
def find_next_move(self, state: FishGameState) -> Action: """ Purpose: Find best move for player whose turn it is in the current state after going N turns ahead for that player in the current turn, or as many possible. This assumes that a player using this will have at least 1 possible move on their turn. This holds because the referee would never ask for a move for a player who has a turn skipped. N turns ahead means that we will look at the current players moves for that many turns ahead. We could, and in some cases should look at their opponents move for that final turn where turn=N, but for this particular maximin evaluation we only have to look to the depth where the player we are checking has made their Nth move and not look at their opponents moves also. This is because the evaluation function only checks the amount of fish that the maximizing player has, which means that the opponents moves in the Nth round cannot impact the amount of fish that the strategizing player has, and thus is wasted computation. If we had another maximin evaluation function that took into account other factors that could change turn-by-turn, we would also checks the opponents moves in the Nth round. Signature: FishGameState Int -> Action :param state: The state for which we are looking for a move. This state includes the current turn so we know which player to look for :return: The action representing a from and to position that is the player best move according to the maximin algorithm """ game_tree = FishGameTree(state=state) max_value = float("-inf") best_children = [] for child in game_tree.generate_direct_children(): # we have already gone one turn down, so # depth is amount of players there are * the number of turns - 1 depth = (self.look_ahead_turns - 1) * len(state.get_player_order()) minimax_value = FishBasicStrategy._find_maximin_value( child, False, depth, game_tree.get_turn_color()) if minimax_value > max_value: best_children = [child] max_value = minimax_value elif minimax_value == max_value: best_children.append(child) if len(best_children) == 1: best_action = best_children[0].parent_action else: best_action = FishBasicStrategy._break_ties( [tree.get_parent_action() for tree in best_children]) return best_action
def run_xtree(move_response_query): """ Purpose: Run the xtree test harness Signature: Move-Response-Query -> Void :param move_response_query: MoveResponseQuery object as specified by harness to print out test values :return: Prints out the Action that results from moving the first move and moving the second player to the neighbor of the first """ # Convert JSON into state and our coordinates state_json = move_response_query['state'] from_coord = move_response_query['from'] to_coord = move_response_query['to'] state = THHelper.create_state(state_json) # Save the to position of the action we are are gonna take and # find its neighbors to_posn_dh = THHelper.convert_to_double_height(to_coord[0], to_coord[1]) from_posn_dh = THHelper.convert_to_double_height(from_coord[0], from_coord[1]) to_posn_neighbors = THHelper.find_coord_neighbors(to_posn_dh) # Take the move on the state_json tree = FishGameTree(state) new_state = tree.validate_and_apply_action((from_posn_dh, to_posn_dh)) # Then we want to see if the new state_json which means # the next player(if it exists) has a move to a neighbor # of the to_pos new_tree = FishGameTree(new_state) final_actions = find_if_valid_move_from_state(new_tree, to_posn_neighbors) if final_actions: final_action = find_tiebreaker(final_actions) move_from_posn, move_to_posn = final_action move_from_org_posn = THHelper.convert_back_to_posn( move_from_posn[0], move_from_posn[1]) move_to_org_posn = THHelper.convert_back_to_posn( move_to_posn[0], move_to_posn[1]) final_action = [move_from_org_posn, move_to_org_posn] print(json.dumps(final_action)) else: print(json.dumps(False))
def create_small_tree(rows=4, columns=4): """ Purpose: Create a small tree to be used in tests Signature: Int Int -> FishGameTree :param rows: Amount of rows for the board :param columns: Amount of columns for the board """ small_board = FishBoardModel.create_with_same_fish_amount(rows, columns, 3) game_state = FishGameStateFactory.create_move_penguins_state( small_board, [(PlayerColor.RED, GameTreeTest.penguins_first_player, 5), (PlayerColor.BLACK, GameTreeTest.penguins_second_player, 0)] ) tree_node = FishGameTree(game_state) return tree_node
def _find_maximin_value(tree: FishGameTree, maximizing_player: bool, depth: int, maximizing_player_color: PlayerColor) -> int: """ Purpose: Find the maximin value for a certain game tree to a certain depth. This maximizes the amount of fish when a the player whose turn it is is playing and minimizes the amount of fish with its moves when all other players are playing, Signature: FishGameTree Bool Int PlayerColor -> Int :param tree: The tree which we are searching for the best moves on for a certain player :param maximizing_player: Whether it is the maximizing players turn :param depth: How many nodes down in the tree we have to go before returning the value :param maximizing_player_color: The color of the player who is trying to maximize their value :return: Returns an integer representing the maximum value that a player is guaranteed to get from that position in the tree """ # if we have gone N turns or hit an end game state if depth == 0 or tree.is_end_game_state(): return tree.get_state().get_fish_for_player( maximizing_player_color) if maximizing_player: compare_val = float("-inf") compare_func = max else: compare_val = float("inf") compare_func = min for child in tree.generate_direct_children(): maximizing_player = child.get_turn_color( ) == maximizing_player_color compare_val = compare_func( compare_val, FishBasicStrategy._find_maximin_value(child, maximizing_player, depth - 1, maximizing_player_color)) return compare_val
def test_run_movement_turn_skip(self): """ Test running a movement turn where someone is skipped """ # player that throws an exception when trying to place it ref = self.set_up_game_phases(3, 3, num_players=4, run_placement_phase=True) # Set player color to black, they can not go and will be skipped ref.current_game_state.set_turn(PlayerColor.BLACK) ref.current_game_tree = FishGameTree(copy.deepcopy(ref.current_game_state)) initial_pengs = ref.current_game_state.get_penguins_for_player(player_color=PlayerColor.BLACK) ref._run_movement_turn() # Player 4 can't go and is skipped so they have no penguins moved_pengs = ref.current_game_state.get_penguins_for_player(player_color=PlayerColor.BLACK) self.assertEqual(initial_pengs, moved_pengs) self.assertEqual(0, ref.current_game_state.get_fish_for_player(player_color=PlayerColor.BLACK))
def create_multiple_players(rows=5, columns=5): """ Purpose: Create a larger board with multiple players to be used for testing Signature: Int Int -> FishGameTree :param rows: Amount of rows for the board :param columns: Amount of columns for the board """ small_board = FishBoardModel.create_with_same_fish_amount(rows, columns, 3) game_state = FishGameStateFactory.create_move_penguins_state( small_board, [(PlayerColor.RED, GameTreeTest.penguins_four_player[1], 0), (PlayerColor.BLACK, GameTreeTest.penguins_four_player[0], 0), (PlayerColor.BROWN, GameTreeTest.penguins_four_player[2], 0), (PlayerColor.WHITE, GameTreeTest.penguins_four_player[3], 0), ] ) tree_node = FishGameTree(game_state) return tree_node
def test_find_if_valid_move(self): """ Purpose: Test finding valid move from a tree """ state = { "players": [{ "color": "red", "score": 10, "places": [[0, 0]] }, { "color": "black", "score": 1, "places": [[0, 2], [0, 1]] }], "board": [[4, 4, 4, 4], [4, 4, 4, 4], [4, 4, 4, 4], [3, 3, 3, 3]] } state = THHelper.create_state(state) state.move_penguin(PlayerColor.RED, (0, 0), (3, 3)) tree = FishGameTree(state) neighbors = THHelper.find_coord_neighbors([3, 3]) self.assertListEqual([((4, 0), (3, 1)), ((2, 0), (3, 1))], xtree.find_if_valid_move_from_state( tree, neighbors))