class GameTreeTests(unittest.TestCase): def __init__(self, *args, **kwargs): super(GameTreeTests, self).__init__(*args, **kwargs) # Initialize boards self.__board1 = Board.homogeneous(5, 5, 3) self.__board2 = Board.homogeneous(3, 5, 2) self.__board3 = Board({ Position(0, 0): Tile(5), Position(0, 1): Tile(3), Position(0, 2): Tile(2), Position(1, 0): Tile(2), Position(1, 1): Tile(3), Position(1, 2): Tile(2), Position(2, 0): Tile(3), Position(2, 1): Tile(4), Position(2, 2): Tile(1), Position(3, 0): Tile(1), Position(3, 1): Tile(1), Position(3, 2): Tile(5), Position(4, 0): Tile(2), Position(4, 1): Tile(3), Position(4, 2): Tile(4) }) # Initialize some players for testing self.__p1 = PlayerEntity("John", Color.RED) self.__p2 = PlayerEntity("George", Color.WHITE) self.__p3 = PlayerEntity("Gary", Color.BLACK) self.__p4 = PlayerEntity("Jeanine", Color.BROWN) self.__p5 = PlayerEntity("Obama", Color.RED) self.__p6 = PlayerEntity("Fred", Color.BROWN) self.__p7 = PlayerEntity("Stewart", Color.WHITE) self.__p8 = PlayerEntity("Bobby Mon", Color.BLACK) self.__p9 = PlayerEntity("Bob Ross", Color.WHITE) self.__p10 = PlayerEntity("Eric Khart", Color.BROWN) self.__p11 = PlayerEntity("Ionut", Color.RED) # ========================== STATE 1 ========================== # Initialize a premature state self.__state1 = State(self.__board1, [self.__p1, self.__p2, self.__p3, self.__p4]) # ========================== STATE 2 ========================== # Initialize a finalized state where at least two more rounds are possible self.__state2 = State(self.__board1, [self.__p1, self.__p2, self.__p3]) # Place all avatars # Player 1 place self.__state2.place_avatar(Color.RED, Position(4, 0)) # Player 2 place self.__state2.place_avatar(Color.WHITE, Position(0, 1)) # Player 3 place self.__state2.place_avatar(Color.BLACK, Position(2, 2)) # Player 1 place self.__state2.place_avatar(Color.RED, Position(1, 0)) # Player 2 place self.__state2.place_avatar(Color.WHITE, Position(2, 0)) # Player 3 place self.__state2.place_avatar(Color.BLACK, Position(3, 2)) # Player 1 place self.__state2.place_avatar(Color.RED, Position(1, 1)) # Player 2 place self.__state2.place_avatar(Color.WHITE, Position(4, 1)) # Player 3 place self.__state2.place_avatar(Color.BLACK, Position(3, 0)) # Make up tree for this state self.__tree2 = GameTree(self.__state2) # ========================== STATE 3 ========================== # Setup state that is one move away from game over self.__state3 = State( self.__board2, players=[self.__p5, self.__p6, self.__p7, self.__p8]) # Set up the board with placements s.t. only 2 moves can be made # Player 1 self.__state3.place_avatar(Color.RED, Position(3, 0)) # Player 2 self.__state3.place_avatar(Color.BROWN, Position(0, 0)) # Player 3 self.__state3.place_avatar(Color.WHITE, Position(1, 0)) # Player 4 self.__state3.place_avatar(Color.BLACK, Position(2, 0)) # Player 1 self.__state3.place_avatar(Color.RED, Position(3, 1)) # Player 2 self.__state3.place_avatar(Color.BROWN, Position(0, 1)) # Player 3 self.__state3.place_avatar(Color.WHITE, Position(1, 1)) # Player 4 self.__state3.place_avatar(Color.BLACK, Position(2, 1)) # Make move 1 for p1 self.__state3.move_avatar(Position(3, 1), Position(4, 1)) # Make up tree for this state self.__tree3 = GameTree(self.__state3) # ========================== STATE 4 ========================== # Setup state that is game over self.__state4 = copy.deepcopy(self.__state3) # Make final move self.__state4.move_avatar(Position(2, 0), Position(4, 0)) # Make up tree for this state self.__tree4 = GameTree(self.__state4) # ========================== STATE 5 ========================== # Setup state that includes heterogeneous board self.__state5 = State(self.__board3, players=[self.__p9, self.__p10, self.__p11]) # Player 1 self.__state5.place_avatar(Color.WHITE, Position(2, 0)) # Player 2 self.__state5.place_avatar(Color.BROWN, Position(0, 1)) # Player 3 self.__state5.place_avatar(Color.RED, Position(0, 2)) # Player 1 self.__state5.place_avatar(Color.WHITE, Position(1, 0)) # Player 2 self.__state5.place_avatar(Color.BROWN, Position(1, 2)) # Player 3 self.__state5.place_avatar(Color.RED, Position(0, 0)) # Player 1 self.__state5.place_avatar(Color.WHITE, Position(3, 1)) # Player 2 self.__state5.place_avatar(Color.BROWN, Position(2, 1)) # Player 3 self.__state5.place_avatar(Color.RED, Position(3, 2)) # Make up tree for this state self.__tree5 = GameTree(self.__state5) def test_init_fail1(self): # Tests failing constructor due to an invalid state with self.assertRaises(TypeError): GameTree("mickey mouse") @unittest.skip( "state does not check for the number of remaining avatars to place anymore" ) def test_init_fail2(self): # Tests failing constructor due to state not being 'started' (or due # to not everyone having placed their avatars). with self.assertRaises(GameNotRunningException): GameTree(self.__state1) def test_init_success(self): # Tests successful game tree constructor GameTree(self.__state2) def test_get_next1(self): # Tests the get_next() of a GameTree where at least two more rounds # are possible. # It's player 1's turn in the state corresponding to this tree self.assertEqual(self.__tree2.state.current_player, Color.RED) # Make sure it has generated all possible connecting trees self.assertSequenceEqual(self.__state2.get_possible_actions(), self.__tree2.all_possible_actions) # Cycle over child trees, check current player, and check move log to make sure # move has been made for the state. for action, tree in self.__tree2.get_next(): # Make sure it's player 2's turn in the state corresponding to this tree self.assertEqual(tree.state.current_player, Color.WHITE) # Make sure action was the last action that happened self.assertEqual(action, tree.state.move_log[-1]) def test_get_next2(self): # Tests the get_next() out of a GameTree where one more round is possible # It's player 4's turn in the state corresponding to this tree self.assertEqual(self.__tree3.state.current_player, Color.BLACK) # Make sure it has generated all possible connecting trees self.assertSequenceEqual(self.__tree3.all_possible_actions, [Action(Position(2, 0), Position(4, 0))]) # Cycle over child trees, check current player, and check move log to make sure # move has been made for the state. for action, tree in self.__tree3.get_next(): # Make sure it's player 11's turn in the state corresponding to this tree self.assertEqual(tree.state.current_player, Color.BLACK) # Make sure action was the last action that happened self.assertEqual(action, tree.state.move_log[-1]) # Make sure it's game over self.assertFalse(tree.state.can_anyone_move()) # Make sure no more child states are possible for it is game # over self.assertSequenceEqual(tree.all_possible_actions, []) def test_get_next3(self): # Tests the get_next() of a GameTree where no more rounds is possible (aka # game over state). for _, _ in self.__tree4.get_next(): self.assertTrue(False) # Make sure is has not generated any more trees (as there are no more # possible moves) self.assertSequenceEqual(self.__tree4.all_possible_actions, []) def test_get_next4(self): # Tests the get_next() out of a GameTree and that of its children # It's player 1's turn in the state corresponding to this tree self.assertEqual(self.__tree2.state.current_player, Color.RED) # Cycle over child trees, check current player, and check move log to make sure # move has been made for the state. for action1, tree1 in self.__tree2.get_next(): # Make sure it has generated all possible connecting edges self.assertSequenceEqual(tree1.state.get_possible_actions(), tree1.all_possible_actions) for action2, tree2 in self.__tree2.get_next(): # Make sure it has generated all possible connecting edges for current # child tree self.assertSequenceEqual(tree2.state.get_possible_actions(), tree2.all_possible_actions) # Make sure it's player 2's turn in the state corresponding to this tree self.assertEqual(tree2.state.current_player, Color.WHITE) # Make sure action was the last action that happened self.assertEqual(action2, tree2.state.move_log[-1]) def test_try_action_fail1(self): # Tests a failing try_action due to action being invalid (type-wise) with self.assertRaises(TypeError): self.__tree2.try_action(Position(0, 0), Position(1, 0)) def test_try_action_fail2(self): # Tests a failing try_action due to action being invalid (not accessible via a straight # line path) with self.assertRaises(InvalidActionException): GameTree.try_action(GameTree(self.__state2), Action(Position(3, 0), Position(0, 0))) def test_try_action_fail3(self): # Tests a failing try_action due to action being out of turn (it involves moving someone # else but the current player's avatar, despite otherwise being legal) with self.assertRaises(InvalidActionException): GameTree.try_action(GameTree(self.__state2), Action(Position(0, 1), Position(2, 1))) def test_try_action_fail4(self): # Tests a failing try_action due to action involves moving thru another character with self.assertRaises(InvalidActionException): GameTree.try_action(GameTree(self.__state2), Action(Position(4, 0), Position(2, 1))) def test_try_action_fail5(self): # Tests a failing try_action due to action involves moving to an already occupied tile with self.assertRaises(InvalidActionException): GameTree.try_action(GameTree(self.__state2), Action(Position(4, 0), Position(3, 0))) def test_try_action_success(self): # Tests a successful try_action where a valid action gets executed valid_action = Action(Position(1, 0), Position(4, 2)) new_state = self.__tree2.try_action(valid_action) # Make sure the valid action we gave it got executed and is at the top # of the move log self.assertEqual(valid_action, new_state.move_log[-1]) # Make sure it's second player's turn now self.assertEqual(new_state.current_player, Color.WHITE) def test_apply_to_child_states_fail1(self): # Tests apply_to_child_states failing due to invalid function (type-wise) with self.assertRaises(TypeError): self.__tree2.apply_to_child_states(lambda state, _: state) def test_apply_to_child_states_success1(self): # Tests successful apply_to_child_states where each child state maps to # that state's current player result = GameTree.apply_to_child_states( GameTree(self.__state2), lambda state: state.current_player) # Resulting array should consist of the next player's id the same number of times # as there are reachable states self.assertSequenceEqual(result, [Color.WHITE] * 7) def test_apply_to_child_states_success2(self): # Tests successful apply_to_child_states where each child state maps to # that a given player's score (homogeneous board) result = GameTree.apply_to_child_states( GameTree(self.__state2), lambda state: state.get_player_score(Color.RED)) # Score should be the same (5) all around since this is a homogeneous board with # 5 fish to each tile self.assertSequenceEqual(result, 7 * [5]) def test_apply_to_child_states_success3(self): # Tests successful apply_to_child_states where each child state maps to # that a given player's score (heterogeneous board) result = GameTree.apply_to_child_states( GameTree(self.__state5), lambda state: state.get_player_score(Color.WHITE)) self.assertSequenceEqual(result, [3, 3, 3, 2, 1, 1, 1, 1])
def __mini_max_search(node: GameTree, player_color_to_max: Color, depth: int, alpha: int = -VERY_LARGE_NUMBER, beta: int = VERY_LARGE_NUMBER): """ Implements min-max algorithm with alpha-beta pruning. It computes the best worst score of the current player in the provided GameTree node by picking the best moves the player can make during their turn and the "best" worst moves its opponents can make during their turns that most minimize the player's score. It utilizes alpha-beta pruning to trim away edges of the tree (or player moves) that are known to yield a worse score than previously computed same-level branches. Two branches are at on same level if they descend from a common node. If there are multiple best moves leading to the same score, then the one with the smallest source row, source column, destination row or destination column is picked (in that order). If the maximizing player becomes stuck at any point in the tree traversal, the search is aborted and its current score is returned. :param node: game tree node for which to run :param player_color_to_max: color of player whose score to maximize (maximizer) :param depth: the number of times maximizing player is evaluated :param alpha: the best score of the maximizer :param beta: the best worst score of the minimizer (one of the player's opponents) :return: tuple of integer best score and corresponding best Action object. """ # Validate params if not isinstance(node, GameTree): raise TypeError('Expected GameTree for node!') if not isinstance(player_color_to_max, Color): raise TypeError('Expected Color for player_color_to_max!') if not isinstance(depth, int) or depth < 0: raise TypeError('Expected integer >= 0 for depth!') if not isinstance(alpha, int): raise TypeError('Expected integer for alpha!') if not isinstance(beta, int): raise TypeError('Expected integer for beta!') # If we have reached our depth, maximizer is stuck or game is over, return player score if depth == 0 or (not node.state.can_anyone_move()) \ or player_color_to_max in node.state.stuck_players: return node.state.get_player_score(player_color_to_max), None # If current player is maximizer, maximize if node.state.current_player == player_color_to_max: if Strategy.DEBUG: print(f'==== PLAYER {node.state.current_player} ========') # Initialize best value to something very negative best_val = -VERY_LARGE_NUMBER # Initialize best_move to anything (won't be compared to assuming we can always achieve # a positive score) best_move: Action = Action( Position(VERY_LARGE_NUMBER, VERY_LARGE_NUMBER), Position(VERY_LARGE_NUMBER, VERY_LARGE_NUMBER)) # Cycle over all possibles moves and their associated states for move, child_node in node.get_next(): # Get best score of subsequent node score, _ = Strategy.__mini_max_search(child_node, player_color_to_max, depth - 1, alpha, beta) # If our best move leads to the same score, pick the move with # the lowest src x, dst y, dst x, dst y (in that order) if score == best_val: best_move = min(best_move, move) elif score > best_val: # If it leads to a better score, update best val and best move best_val = score best_move = move # Determine if this beats our alpha, and if so set our alpha alpha = max(alpha, best_val) # If player's best beats opponents best worst move, cut off if alpha >= beta: break # Return our best move return best_val, best_move else: if Strategy.DEBUG: print(f'==== PLAYER {node.state.current_player} ========') # Initialize opponent's best value to something very positive best_val = VERY_LARGE_NUMBER # Minimize, otherwise for move, child_node in node.get_next(): # Get best score of subsequent node score, _ = Strategy.__mini_max_search(child_node, player_color_to_max, depth, alpha, beta) # Minimize player_id_to_max's score best_val = min(score, best_val) # See if we have come up with a better "worst" move beta = min(beta, best_val) # If player's best beats opponents best worst move, cut off if alpha >= beta: break # Return opponent's best move return best_val, None