def minimax(tree: GameStateTree, num_rounds: int, maximizing_player: Player) -> Tuple[int, Action]: """ Called recursively on various nodes of the GameStateTree to determine the maximum move for the maximizing player and the minimizing move for any player that is not the maximizing player. :param tree: The game state tree that will be examined to determine the optimal moves. :param num_rounds: The number of rounds each player will play within the decision algorithm :param maximizing_player: The player that is being maximized, all other players will attempt to minimize this player. :return: The Tuple(optimal_score, optimal_action) that will take place in this round of the minimax algorithm. """ if num_rounds == 0 and tree.state.current_player.color == maximizing_player.color: return tree.state.current_player.score, tree.previous_action elif tree.state.phase == GameStatePhase.OVER and tree.state.current_player.color == maximizing_player.color: return tree.state.current_player.score, tree.previous_action # if we reach the end and it is the maximizing players turn return the maximizing players score elif tree.state.phase == GameStatePhase.OVER and tree.state.current_player.color != maximizing_player.color: return -inf, None # if we reach the end and it is not the maximizing players turn return the -inf so # the next node up the chain will be greater than it elif tree.state.current_player.color == maximizing_player.color: # maximize this player children = [child for child in tree.get_children()] return GenericStrategyComponent.handle_maximum( children=children, num_rounds=num_rounds, maximizing_player=maximizing_player) elif tree.state.current_player.color != maximizing_player.color: # minimize the maximizing player for all other player children = [child for child in tree.get_children()] return GenericStrategyComponent.handle_minimum( children=children, num_rounds=num_rounds, maximizing_player=maximizing_player)
def test_create_choose_action_recursive_case_is_maximizing_twice(self): """ In a recursive case where 1 round is played and the all leave nodes are the maximizing players, the minimum of these leave nodes is return. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) expected = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) children = [child for child in production_tree.get_children()] for idx, child in enumerate(children): second_players_children = [ second_players_child for second_players_child in child.get_children() ] for jdx, second_players_child in enumerate( second_players_children): if jdx == 1 and idx == 1: score = 0 child.previous_action = expected else: score = 10 modified_player = Player.from_dict({ **second_players_child.state.current_player._asdict(), **{ 'score': score } }) second_players_child.state.current_player = modified_player second_players_child.get_children = MagicMock(return_value=[]) second_players_child.state.check_any_player_can_move = MagicMock( return_value=False) production_tree.get_children = MagicMock(return_value=children) actual = GenericStrategyComponent.choose_action(tree=production_tree, num_turns=2) self.assertEqual(expected, actual)
def test_create_choose_action_recursive_case_is_maximizing_once(self): """ In a scenario where all the children of the maximizing player are leaves - aka end-games - than a tie-breaker decides which action to take. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) expected = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) children = [child for child in production_tree.get_children()] for idx, child in enumerate(children): if idx == 1: child.previous_action = expected child.get_children = MagicMock(return_value=[]) production_tree.get_children = MagicMock(return_value=children) actual = GenericStrategyComponent.choose_action(tree=production_tree, num_turns=2) self.assertEqual(expected, actual)
def test_create_choose_action_base_case_0_turns(self): """ If the player has 0 turns and it is the currents player turn, then the previous action is return. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) production_tree.get_children = MagicMock(return_value=None) expected = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) production_tree.previous_action = expected actual = GenericStrategyComponent.choose_action(tree=production_tree, num_turns=0) self.assertEqual(expected, actual)
def test_create_choose_action_base_case_no_children_for_non_maximizing_player( self): """ If the player has turns > 0 but has no children - end-game - and this is not the maximizing player, then we return None. """ state, *_ = self.setup() production_tree = GameStateTree(state, previous_action=None) production_tree.get_children = MagicMock(return_value=[]) production_tree.previous_action = Action( player_color=production_tree.state.current_player.color, start=Coordinate(0, 0), end=Coordinate(1, 0)) expected = None _, actual = GenericStrategyComponent.minimax(tree=production_tree, num_rounds=2, maximizing_player=Player( "white", 0, 0, [])) self.assertEqual(expected, actual)