def _turn_start(self, player: Player): """ Called when the provided player's turn starts. :param player: The player whose turn it is. """ if not player.is_ai(): turn_complete = False while not turn_complete: pokemon = player.get_party().get_starting() move = prompt_multi( 'What will ' + player.get_name() + '\'s ' + pokemon.get_name() + ' do?', "Attack", "Bag", "Switch Pokemon", "Check Opponent's Pokemon", "Forfeit")[0] if move == 0: turn_complete = self._turn_attack(player) elif move == 1: turn_complete = self._turn_bag(player) elif move == 2: turn_complete = self._turn_switch_pokemon(player) elif move == 3: turn_complete = False self._turn_check_pokemon(player) else: clear_battle_screen() self._alert(player.get_name() + ' forfeits...', player) exit(0) else: # AI player should make move decision based on provided objects. self._turn_ai(player)
def take_turn(self, player: Player, other_player: Player, attack: Callable[[Move], None], use_item: Callable[[Item], None], switch_pokemon_at_idx: Callable[[int], None]) -> None: pokemon = player.get_party().get_starting() num_available_moves = sum([ int(move.is_available()) for move in pokemon.get_move_bank().get_as_list() ]) num_available_pokemon = sum([ int(not pokemon.is_fainted()) for pokemon in player.get_party().get_as_list() ]) - 1 if num_available_moves + num_available_pokemon > 0 and randint( 1, num_available_moves + num_available_pokemon) <= num_available_moves: # Perform a move move_list = deepcopy(pokemon.get_move_bank().get_as_list()) shuffle(move_list) for move in move_list: if move.is_available(): attack(move) break else: # Perform a switch idx = self.force_switch_pokemon(player.get_party()) switch_pokemon_at_idx(idx)
def outcome_func_v1(player: Player, opponent: Player) -> float: """ Calculates the outcome value on [0, 1] for use in Monte Carlo Tree Search. :param player: The current player. :param opponent: The opposing player. :return: An outcome value between 0 and 1. 1 denotes a strong win, 0 denotes a bad loss. """ # Calculate HP differences and fainted pokemon player_total_hp, opp_total_hp = 0, 0 hp_taken, hp_dealt = 0, 0 player_fainted_count, opp_fainted_count = 0, 0 for pokemon in player.get_party().get_as_list(): player_total_hp += pokemon.get_base_hp() hp_taken += pokemon.get_base_hp() - pokemon.get_hp() player_fainted_count += int(pokemon.get_hp() == 0) for pokemon in opponent.get_party().get_as_list(): opp_total_hp += pokemon.get_base_hp() hp_dealt += pokemon.get_base_hp() - pokemon.get_hp() opp_fainted_count += int(pokemon.get_hp() == 0) outcome = 0.2 if player_fainted_count == len( player.get_party().get_as_list()) else 0.8 # Outcome = %hp_dealt - %hp_taken + %pokemon_killed - (%pokemon_fainted)^2 hp_perc_diff = hp_dealt / opp_total_hp - hp_taken / player_total_hp pokemon_fainted_perc_diff = opp_fainted_count / len( opponent.get_party().get_as_list()) - ( player_fainted_count / len(player.get_party().get_as_list()))**2 scalar = (hp_perc_diff + pokemon_fainted_perc_diff) / 10 return outcome + scalar
def create_node(node_player: Player, node_other_player: Player, action_type: MonteCarloActionType, index: int) -> MonteCarloNode: """ Creates an attack or switch node. :param node_player: The current player object for the node. :param action_type: Either MonteCarloActionType.ATTACK or MonteCarloActionType.SWITCH. :param index: The index of the attack or Pokemon to switch to. """ # The currently battling pokemon pokemon = node_player.get_party().get_starting() # Create action descriptor if action_type == MonteCarloActionType.ATTACK: attack = pokemon.get_move_bank().get_move(index) action_descriptor = index description = "%s used %s." % (pokemon.get_name(), attack.get_name()) else: switch_pokemon = node_player.get_party().get_at_index(index) action_descriptor = switch_pokemon.get_id() description = "%s switched out with %s." % (pokemon.get_name(), switch_pokemon.get_name()) # Create the "turn" to be taken when this node is visited if action_type == MonteCarloActionType.ATTACK: def take_turn(_: Player, __: Player, do_move: Callable[[Move], None], ___: Callable[[Item], None], ____: Callable[[int], None]): do_move(attack) else: def take_turn(_: Player, __: Player, ___: Callable[[Move], None], ____: Callable[[Item], None], switch_pokemon_at_idx: Callable[[int], None]): switch_pokemon_at_idx(index) model = RandomModel() model.take_turn = take_turn # Return the move node return MonteCarloNode(node_player.copy(), node_other_player.copy(), node_player.get_id(), action_type, action_descriptor, model, 0, description)
def _turn_perform_attacks(self, player_a: Player, player_b: Player): """ Dequeues and performs the attacks in the attack queue. :param player_a: The first player to perform attacks for. :param player_b: The second player to perform attacks for. """ for player, pokemon, move in self.attack_queue: if player.get_id() is player_a.get_id(): if self._perform_attack(move, player_a, player_b, pokemon): break elif player.get_id() is player_b.get_id(): if self._perform_attack(move, player_b, player_a, pokemon): break self.attack_queue = []
def _make_input_vector(player: Player, other_player: Player) -> np.ndarray: """ Creates an input vector for the dense net. :param player: The focused player. :param other_player: The other player. :return: A 60-len numpy array with [HP ratio, Move 1 PP ratio, ... Move 4 PP ratio] at each row for max 12 Pokemon, flattened. """ # Store each player's Pokemon lists player_pokemon = player.get_party().get_sorted_list() other_player_pokemon = other_player.get_party().get_sorted_list() mat = [] def fill_rows(pokemon_list: List[Pokemon]): """ Fills a list of rows with Pokemon stats. :param pokemon_list: A list of Pokemon. """ for pkmn in pokemon_list: row = [] # Add HP component hp_comp = pkmn.get_hp() / pkmn.get_base_hp() row.append(hp_comp) # Add move components move_list = pkmn.get_move_bank().get_as_list() for move in move_list: move_comp = move.get_pp() / move.get_base_pp() row.append(move_comp) # Fill remaining moves with 0 for _ in range(POKEMON_MOVE_LIMIT - len(move_list)): row.append(EPSILON) # Add row to matrix mat.append(row) def fill_empty(num: int): for _ in range(num): mat.append([EPSILON, EPSILON, EPSILON, EPSILON, EPSILON]) # Fill rows fill_rows(player_pokemon) fill_empty(POKEMON_PARTY_LIMIT - len(player_pokemon)) fill_rows(other_player_pokemon) fill_empty(POKEMON_PARTY_LIMIT - len(other_player_pokemon)) return np.array(mat).flatten()
def _turn_bag(self, player: Player, ai_item: Item = None): """ Called when the player chooses to use a bag item. :param player: The player who is opening the bag. :param ai_item: The item the AI is using. :return: True if the player uses a bag item, False if the player chooses to go back. """ pokemon = player.get_party().get_starting() if not player.is_ai(): item = prompt_multi( 'Use which item?', 'None (Go back)', *[ i.get_name() + " (" + i.get_description() + ")" for i in player.get_bag().get_as_list() ])[0] elif ai_item is not None: item = ai_item else: item = 0 if item == 0: return False item_idx = item - 1 item = player.get_bag().get_as_list()[item_idx] # Use the item and remove it from the player's bag self._alert("%s used a %s." % (player.get_name(), item.get_name()), self.player1, self.player2) item.use(player, pokemon) player.get_bag().get_as_list().remove(item_idx) return True
def print_battle_screen(player: Player, other_player: Player, screen_width=None): """ Prints a battle screen. :param player: The current player who the battle screen is focused on. :param other_player: The other player. :param screen_width: The width of the terminal. """ screen_width = screen_width if screen_width is not None else get_terminal_dimensions( )[0] def get_statuses(pkmn: Pokemon): statuses = [] if pkmn.get_status() is not None: statuses.append(pkmn.get_status().name) if pkmn.get_other_status() is not None: statuses.append(pkmn.get_other_status().name) return ', '.join(statuses) txt_player_name = lambda plyr, aln: align(plyr.get_name().upper(), aln, screen_width) txt_pokemon_top = lambda pkmn: split_align( "%s (%s)" % (pkmn.get_name(), pkmn.get_type().name), 'Lv' + str( pkmn.get_level()), screen_width) txt_pokemon_bottom = lambda pkmn: split_align( str(pkmn.get_hp()) + '/' + str(pkmn.get_base_hp()) + ' HP', get_statuses(pkmn), screen_width) pokemon = player.get_party().get_starting() other_pokemon = other_player.get_party().get_starting() print("\n", end="\r", flush=True) print(repeat('=', screen_width)) print(txt_player_name(other_player, Align.RIGHT)) print(txt_pokemon_top(other_pokemon)) print(txt_pokemon_bottom(other_pokemon)) print(repeat('-', screen_width)) print(txt_player_name(player, Align.LEFT)) print(txt_pokemon_top(pokemon)) print(txt_pokemon_bottom(pokemon)) print(repeat('=', screen_width))
def take_turn(self, player: Player, other_player: Player, attack: Callable[[Move], None], use_item: Callable[[Item], None], switch_pokemon_at_idx: Callable[[int], None]) -> None: pokemon = player.get_party().get_starting() enemy = other_player.get_party().get_starting() damage_list = [] for move in pokemon.get_move_bank().get_as_list(): if move.is_available(): damage = calculate_damage_deterministic(move, pokemon, enemy)[0] damage_list.append((move, damage)) damage_list.sort(reverse=True, key=lambda x: x[1]) attack(damage_list[0][0])
def _turn_attack(self, player: Player, ai_move: Move = None): """ Called when the player chooses to attack. :param player: The player who is attacking. :param ai_move: The move the AI is playing. :return: True if the player selected an attack, False if the player chooses to go back. """ pokemon = player.get_party().get_starting() other_player = self.player2 if player.get_id( ) == self._PLAYER_1_ID else self.player1 on_pokemon = other_player.get_party().get_starting() move = None if not player.is_ai(): while move is None or not move.is_available(): move_idx = prompt_multi( 'Seslect a move.', 'None (Go back)', *[ m.get_name() + ': ' + PokemonType(m.get_type()).name.lower().capitalize() + (" (" + is_effective( m.get_type(), on_pokemon.get_type()).name.lower() + " damage) " if self.use_hints else "") + ', ' + str(m.get_pp()) + '/' + str(m.get_base_pp()) + ' PP' for m in pokemon.get_move_bank().get_as_list() ])[0] if move_idx == 0: return False move = pokemon.get_move_bank().get_move(move_idx - 1) if not move.is_available(): self._alert("There's no PP left for this move!", player) elif ai_move is not None: # ai_move contains the same move on another copy of the player's Pokemon, so we have to retrieve the # same move in the current in-game Pokemon ai_move_name = ai_move.get_name() for temp_move in player.get_party().get_starting().get_move_bank( ).get_as_list(): if temp_move.get_name() == ai_move_name: move = temp_move break else: return False self._enqueue_attack(player, move) return True
def take_turn(self, player: Player, other_player: Player, attack: Callable[[Move], None], use_item: Callable[[Item], None], switch_pokemon_at_idx: Callable[[int], None]) -> None: with ThreadPoolExecutor() as executor: self._predictor.predict_move(player, other_player) make_tree_thread = executor.submit(make_tree, player, other_player, NUM_SIMULATIONS, predictor=self._predictor, use_damage_model=self._use_damage_model, verbose=False) if self._verbose: print("%s is formulating a move..." % player.get_name()) tree = make_tree_thread.result() model = tree.get_next_action() if self._verbose: print("Done") model.take_turn(player, other_player, attack, use_item, switch_pokemon_at_idx)
def _turn_end(self, player: Player): """ Called when a turn is about to end to inflict damage from statuses and other things. :param player: The player to perform the calculations on. :return: True if the player wins, False otherwise. """ # Get the Pokemon that's currently out pokemon = player.get_party().get_starting() def self_inflict(base_damage: int): # Performs damage calculation for self-inflicted attacks damage = int(pokemon.get_stats().get_attack() / base_damage) defense = int(pokemon.get_stats().get_defense() / base_damage * 1 / 10) total_damage = min(max(1, (damage - defense)), pokemon.get_hp()) pokemon.take_damage(total_damage) if pokemon.is_fainted(): self._alert(pokemon.get_name() + ' fainted!', player) return not self._turn_switch_pokemon(player, False) return False if pokemon.get_other_status() in [ Status.POISON, Status.BAD_POISON, Status.BURN ]: self._alert( pokemon.get_name() + ' is ' + status_names[pokemon.get_other_status()] + '.', player) # Increment the number of turns with the other status pokemon.inc_other_status_turn() if pokemon.get_other_status() is Status.POISON: damage = int(pokemon.get_base_hp() / 16) self._alert( pokemon.get_name() + ' took ' + str(damage) + ' damage from poison.', player) return self_inflict(damage) if pokemon.get_other_status() is Status.BAD_POISON: damage = int(pokemon.get_base_hp() * pokemon.get_other_status_turns() / 16) self._alert( pokemon.get_name() + ' took ' + str(damage) + ' damage from poison.', player) return self_inflict(damage) elif pokemon.get_other_status() is Status.BURN: damage = int(pokemon.get_base_hp() / 8) self._alert( pokemon.get_name() + ' took ' + str(damage) + ' damage from its burn.', player) return self_inflict(damage) return False
def _turn_ai(self, player: Player): """ Let's the player's AI model perform the turn. :param player: The player the AI plays for. :return: True, always, as if the model knows what it's doing. """ assert player.get_model() is not None # Get the other player other_player = self.player2 if player.get_id( ) == self._PLAYER_1_ID else self.player1 # Create lambda functions to be used by the model attack_func = lambda move: self._turn_attack(player, move) use_item_func = lambda item: self._turn_bag(player, item) switch_pokemon_at_idx_func = lambda idx: self._turn_switch_pokemon( player, False, idx) # Take the turn using the model player.get_model().take_turn(player, other_player, attack_func, use_item_func, switch_pokemon_at_idx_func) return True
def _enqueue_attack(self, player: Player, move: Move): """ Enqueues an attack to be done by the player, depending on the player's Pokemon's speed. :param player: The player whose starter Pokemon will be attacking. :param move: The move to attack with. """ pokemon = player.get_party().get_starting() attack_triple = (player, pokemon, move) if len(self.attack_queue) == 0: self.attack_queue.append(attack_triple) else: op_speed = self.attack_queue[0][1].get_stats().get_speed() self_speed = pokemon.get_stats().get_speed() if op_speed > self_speed: self.attack_queue.append(attack_triple) elif op_speed < self_speed: self.attack_queue.insert(0, attack_triple) else: chance(0.5, lambda: self.attack_queue.append(attack_triple), lambda: self.attack_queue.insert(0, attack_triple))
def _battle_start(self, player1: Player, player2: Player): """ Called when the battle is about to begin. :param player1: The first player. :param player2: The second player. """ self.started = True # Assign faux IDs for keeping track player1._id = self._PLAYER_1_ID player2._id = self._PLAYER_2_ID # Reveal starting Pokemon player1.get_party().get_starting().reveal() player2.get_party().get_starting().reveal() if self.verbose >= 1: # Clear the terminal window #clear(get_terminal_dimensions()[1]) print("")
def _perform_attack(self, move: Move, player: Player, on_player: Player, pokemon: Pokemon): """ Performs a move by the starting Pokemon of player against the starting pokemon of on_player. :param move: The move player's Pokemon is performing. :param player: The player whose Pokemon is attacking. :param on_player: The player whose Pokemon is defending. :param pokemon: The attacking Pokemon. :return: Returns True if player wins, False otherwise. """ if pokemon.is_fainted(): return False on_pokemon = on_player.get_party().get_starting() def confusion(): base_damage = 40 damage = int(pokemon.get_stats().get_attack() / base_damage) defense = int(pokemon.get_stats().get_defense() / base_damage * 1 / 10) total_damage = max(1, damage - defense) pokemon.take_damage(total_damage) self._alert(pokemon.get_name() + ' hurt itself in its confusion.', player, on_player) if pokemon.is_fainted(): self._alert(pokemon.get_name() + ' fainted!', player) return not self._turn_switch_pokemon(player, False) return False def paralysis(): self._alert(pokemon.get_name() + ' is unable to move.', player, on_player) def infatuation(): self._alert( pokemon.get_name() + ' is infatuated and is unable to move.', player, on_player) def freeze(): self._alert(pokemon.get_name() + ' is frozen solid.', player, on_player) def sleep(): self._alert(pokemon.get_name() + ' is fast asleep.', player, on_player) def try_attack(): # Decrease the PP on the move move.dec_pp() move.reveal() # Checks to see if the attack hit change_of_hit = pokemon.get_stats().get_accuracy( ) / 100 * on_pokemon.get_stats().get_evasiveness() / 100 did_hit = chance(change_of_hit, True, False) if not did_hit: self._alert(pokemon.get_name() + '\'s attack missed.', player, on_player) return False self._alert( player.get_name() + "'s " + pokemon.get_name() + ' used ' + move.get_name() + '!', player, on_player) if move.is_damaging(): # Calculate damage damage, effectiveness, critical = calculate_damage( move, pokemon, on_pokemon) # Describe the effectiveness if critical == Criticality.CRITICAL and effectiveness != Effectiveness.NO_EFFECT: self._alert('A critical hit!', player, on_player) if effectiveness == Effectiveness.NO_EFFECT: self._alert( 'It has no effect on ' + on_pokemon.get_name() + '.', player, on_player) if effectiveness == Effectiveness.SUPER_EFFECTIVE: self._alert('It\'s super effective!', player, on_player) if effectiveness == Effectiveness.NOT_EFFECTIVE: self._alert('It\'s not very effective...', player, on_player) self._alert( on_pokemon.get_name() + ' took ' + str(damage) + ' damage.', player, on_player) # Lower the opposing Pokemon's HP on_pokemon._hp = max(0, on_pokemon.get_hp() - damage) # If the move inflicts a status, perform the status effect if move.get_status_inflict(): if move.get_status_inflict() in [ Status.POISON, Status.BAD_POISON, Status.BURN ]: # Unlike status turns, other status turns increase in length because they don't end on_pokemon.set_other_status(move.get_status_inflict()) else: on_pokemon.set_status(move.get_status_inflict()) self._alert( on_pokemon.get_name() + ' was ' + status_names[move.get_status_inflict()], player, on_player) # Heal the pokemon if move.get_base_heal() > 0: on_pokemon.heal(move.get_base_heal()) self._alert( pokemon.get_name() + ' gained ' + str(move.get_base_heal()) + ' HP.', player) # Check if the Pokemon fainted if on_pokemon.is_fainted(): self._alert(on_pokemon.get_name() + ' fainted!', player, on_player) return not self._turn_switch_pokemon(on_player, False) return False if pokemon.get_status() not in [ None, Status.POISON, Status.BAD_POISON, Status.BURN ]: # If the Pokemon is inflicted by a status effect that impacts the chance of landing the attack, # perform the calculations. status = pokemon.get_status() pokemon._status_turns = max(0, pokemon.get_status_turns() - 1) if pokemon.get_status_turns() == 0: pokemon._status = None self._alert( pokemon.get_name() + ' is ' + status_names[status] + '.', player, on_player) if status is Status.CONFUSION: chance(1 / 3, lambda: confusion(), try_attack) elif status is Status.PARALYSIS: chance(0.25, lambda: paralysis(), try_attack) elif status is Status.INFATUATION: chance(0.5, lambda: infatuation(), try_attack) elif status is Status.FREEZE: freeze() elif status is Status.SLEEP: sleep() elif pokemon.get_status() is None: # No status effect, attempt to attack return try_attack() return False
def predict_move( self, player: Player, other_player: Player ) -> Tuple[RandomModel, MonteCarloActionType, int, List[float], List[float]]: """ Predict the move the player should make. :param player: The player. :param other_player: The opposing player. :return: A tuple containing the <move model, move type, move index, move probabilities, switch-out probabilities>. """ if not self._is_trained: return RandomModel( ), MonteCarloActionType.ATTACK, 0, POKEMON_MOVE_LIMIT * [ round(1 / POKEMON_MOVE_LIMIT) ], POKEMON_PARTY_LIMIT * [round(1 / POKEMON_PARTY_LIMIT)] input_matrix = self._make_input_vector(player, other_player) output = self._model.predict([input_matrix])[0] # Get the index of the 4 current Pokemon moves from the output current_pokemon_id = player.get_party().get_starting().get_id() current_pokemon_idx = -1 for idx, pokemon in enumerate(player.get_party().get_sorted_list()): if pokemon.get_id() == current_pokemon_id: current_pokemon_idx = idx break # Create probabilities for picking moves and switches move_probs = [] if current_pokemon_idx >= 0: start_idx = POKEMON_PARTY_LIMIT + current_pokemon_idx * POKEMON_MOVE_LIMIT move_probs = output[start_idx:start_idx + POKEMON_MOVE_LIMIT] switch_probs = output[:POKEMON_PARTY_LIMIT] # Create the model model = RandomModel() # Get probability of attacking and switching all_moves = list(np.concatenate((move_probs, switch_probs), axis=0)) all_moves_probs = to_probs(all_moves) prob_attack = sum(all_moves_probs[:len(move_probs)]) move_type = chance(prob_attack, MonteCarloActionType.ATTACK, MonteCarloActionType.SWITCH) move_idx = 0 # Randomly select a move given the move weights if move_type == MonteCarloActionType.ATTACK: # Get a random move move_idx = chances( move_probs, list( range( len(player.get_party().get_starting().get_move_bank(). get_as_list())))) attack = player.get_party().get_starting().get_move_bank( ).get_move(move_idx) # Create a turn function def take_turn(_: Player, __: Player, do_move: Callable[[Move], None], ___: Callable[[Item], None], ____: Callable[[int], None]): do_move(attack) # Create the model model.take_turn = take_turn else: # Get a random switch index move_idx = chances(switch_probs, [i for i, _ in enumerate(switch_probs)]) # Create a turn function def take_turn(_: Player, __: Player, ___: Callable[[Move], None], ____: Callable[[Item], None], switch_pokemon: Callable[[int], None]): switch_pokemon(move_idx) # Create the model model.take_turn = take_turn return model, move_type, move_idx, move_probs, switch_probs
import sys from os.path import join, dirname sys.path.append(join(dirname(__file__), '../..')) from pokemon_ai.battle import Battle from pokemon_ai.classes import Bag, Player from pokemon_ai.data import get_party from pokemon_ai.ai.models import DamageModel party1 = get_party("charizard") party2 = get_party("bulbasaur") player1 = Player("Player 1", party1, Bag()) player2 = Player("Player 2", party2, Bag(), DamageModel()) battle = Battle(player1, player2) battle.play()
from pokemon_ai.battle import Battle from pokemon_ai.classes import Bag, Party, Player, Move, MoveBank, Pokemon, Stats, PokemonType from pokemon_ai.ai.models import SampleModel party1 = Party([ Pokemon(PokemonType.FIRE, "Charizard", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Flamethrower", 100, 0, PokemonType.FIRE, True)]), 300), Pokemon(PokemonType.WATER, "Piplup", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Water Squirt", 100, 3, PokemonType.WATER, True)]), 300) ]) party2 = Party([ Pokemon(PokemonType.FIRE, "Blaziken", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Flamethrower", 100, 3, PokemonType.FIRE, True)]), 300), Pokemon(PokemonType.WATER, "Squirtle", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Water Squirt", 100, 3, PokemonType.WATER, True)]), 300) ]) player1 = Player("Player 1", party1, Bag()) player2 = Player("Player 2", party2, Bag(), SampleModel()) battle = Battle(player1, player2) battle.play()
def _make_actual_output_list(player: Player, node: Any) -> np.ndarray: """ Creates a list of actual output values from a player object and a single node. :param player: A Player that owns the action in the node. :param node: A MonteCarloNode. :return: A list of output values of length OUTPUT_SIZE (31). """ # Get list of Pokemon player_pokemon = player.get_party().get_as_list() # Calculate switch probabilities switch_probs = [EPSILON] * len(player_pokemon) for child in node.children: if child.action_type == 0: # SWITCH pkmn_id = child.action_descriptor switch_idx = 0 for i, pkmn in enumerate(player_pokemon): if pkmn.get_id() == pkmn_id: switch_idx = i switch_probs[switch_idx] = child.outcome / node.outcome # Create a tuple of switch probability / Pokemon values pokemon_switch_tuple: List[Tuple[float, Pokemon]] = [] for i, switch_prob in enumerate(switch_probs): pokemon_switch_tuple.append((switch_prob, player_pokemon[i])) # Sort the tuple by Pokemon ID to retain order pokemon_switch_tuple.sort(key=lambda v: v[1].get_id()) # Remake switch_probs in order switch_probs = [tup[0] for tup in pokemon_switch_tuple] # Add extra probs in case of short party for _ in range(POKEMON_PARTY_LIMIT - len(player_pokemon)): switch_probs.append(EPSILON) # Add attack moves of every Pokemon pkmn_id_to_move_prob_map = {} for child in node.children: if child.action_type == 0: # ATTACK pkmn_id = child.detokenize_child() move_idx = child.action_descriptor if pkmn_id not in pkmn_id_to_move_prob_map: pkmn_id_to_move_prob_map[pkmn_id] = [EPSILON ] * POKEMON_MOVE_LIMIT pkmn_id_to_move_prob_map[pkmn_id][ move_idx] = child.outcome / node.outcome # Create a list of moves for all Pokemon and then traverse the sorted Pokemon list, adding the prob of each move # one by one. move_probs = [] for pkmn in player.get_party().get_sorted_list(): pkmn_id = pkmn.get_id() move_probs_for_pkmn = [] if pkmn_id in pkmn_id_to_move_prob_map: for move_prob in pkmn_id_to_move_prob_map[pkmn_id]: move_probs_for_pkmn.append(move_prob) while len(move_probs_for_pkmn) < 4: move_probs_for_pkmn.append(EPSILON) move_probs += move_probs_for_pkmn while len(move_probs) < POKEMON_PARTY_LIMIT * POKEMON_MOVE_LIMIT: move_probs += [EPSILON] * POKEMON_MOVE_LIMIT outcome_list = [node.outcome] mat = switch_probs + move_probs + outcome_list return np.array(mat)
def make_tree(player_real: Player, other_player_real: Player, num_plays=1, predictor: Predictor = None, learning_turns: int = 10, use_damage_model=False, verbose=False): """ Creates a MonteCarloTree of actions for the given battle. :param player_real: The player to find actions for. :param other_player_real: The opposing player. :param num_plays: The number of Monte Carlo simulations to perform. :param predictor: An optional neural network to weigh he training. :param learning_turns: Number of turns the model will learn before making decisions. :param use_damage_model: Use the DamageModel? :param verbose: Should the algorithm announce its current actions? :return: A MonteCarloTree. """ # Create tree tree = MonteCarloTree(player_real.copy(), other_player_real.copy()) root = tree.root root.player.set_model(RandomModel()) root.other_player.set_model(RandomModel() if not use_damage_model else DamageModel()) root.depth = 1 root.description = 'Battle Start' # Use workaround to pass this to children current_learning_turn = [0] # Play num_plays amount of times for current_num_plays in range(num_plays): def backprop(node: MonteCarloNode, outcome: float) -> None: """ Backpropogates and updates all nodes from the top node using the sums of the leaf nodes. Included logic to add wins for respective player :param node: The leaf node to start backpropgating from :param outcome: The calculated outcome """ if node.depth % 2 == 0 or node.depth == 1: node.outcome += outcome else: node.outcome += (1 - outcome) node.visit() if node.parent is not None: backprop(node.parent, outcome) def create_node(node_player: Player, node_other_player: Player, action_type: MonteCarloActionType, index: int) -> MonteCarloNode: """ Creates an attack or switch node. :param node_player: The current player object for the node. :param action_type: Either MonteCarloActionType.ATTACK or MonteCarloActionType.SWITCH. :param index: The index of the attack or Pokemon to switch to. """ # The currently battling pokemon pokemon = node_player.get_party().get_starting() # Create action descriptor if action_type == MonteCarloActionType.ATTACK: attack = pokemon.get_move_bank().get_move(index) action_descriptor = index description = "%s used %s." % (pokemon.get_name(), attack.get_name()) else: switch_pokemon = node_player.get_party().get_at_index(index) action_descriptor = switch_pokemon.get_id() description = "%s switched out with %s." % (pokemon.get_name(), switch_pokemon.get_name()) # Create the "turn" to be taken when this node is visited if action_type == MonteCarloActionType.ATTACK: def take_turn(_: Player, __: Player, do_move: Callable[[Move], None], ___: Callable[[Item], None], ____: Callable[[int], None]): do_move(attack) else: def take_turn(_: Player, __: Player, ___: Callable[[Move], None], ____: Callable[[Item], None], switch_pokemon_at_idx: Callable[[int], None]): switch_pokemon_at_idx(index) model = RandomModel() model.take_turn = take_turn # Return the move node return MonteCarloNode(node_player.copy(), node_other_player.copy(), node_player.get_id(), action_type, action_descriptor, model, 0, description) def insert_node(node: MonteCarloNode, parent: MonteCarloNode) -> MonteCarloNode: """ Adds an attack or switch move node to a tree. :param node: The current node. :param parent: The parent node.. """ pokemon = node.player.get_party().get_starting() child_exists = parent.has_child(pokemon, node.action_type, node.action_descriptor) if not child_exists: child = node parent.add_child(child, pokemon) else: child = parent.get_child(pokemon, node.action_type, node.action_descriptor) if node.depth % 2 == 1: # If on an odd depth, simulate a turn and update the node's state (players). node.player.set_model(node.model) node.other_player.set_model(parent.model) battle = Battle(node.player, node.other_player, 1 if verbose else 0) winner = battle.play_turn() # Get turn outcome if winner is not None and predictor is not None: # Train the predictor predictor.train_model(root, player, other_player) current_learning_turn[0] += 1 return child def traverse(node: MonteCarloNode) -> MonteCarloNode: """ If the node is not fully expanded, pick one of the unvisited children. Else, pick the child node with greatest UCT value. If this child node is also fully expanded, repeat process. """ def fully_expanded(node: MonteCarloNode): if node.depth == 1: c_player = node.player.copy() c_other_player = node.other_player.copy() else: c_player = node.other_player.copy() c_other_player = node.player.copy() pokemon = c_player.get_party().get_starting() # Creates all children for node if they do not already exist, and checks visit (0 is unvisited) attacks = list(filter(lambda m: m[0].is_available(), [(move, i) for i, move in enumerate(pokemon.get_move_bank().get_as_list())])) for _, attack_idx in attacks: child = create_node(c_player, c_other_player, MonteCarloActionType.ATTACK, attack_idx) insert_node(child, node) switches = list(filter(lambda p: not p[0].is_fainted(), [(pkmn, i) for i, pkmn in enumerate(node.player.get_party().get_as_list())]))[1:] for _, switch_idx in switches: child = create_node(c_player, c_other_player, MonteCarloActionType.SWITCH, switch_idx) insert_node(child, node) return all([child.visits > 0 for child in node.children]) def best_uct_node(node: MonteCarloNode) -> MonteCarloNode: uct_values = [] for child in node.children: uct_values.append(calculations.upper_confidence_bounds(child.outcome, child.visits, node.visits)) index_of_best_move = uct_values.index(max(uct_values)) return node.children[index_of_best_move] def pick_unvisited(node: MonteCarloNode) -> MonteCarloNode: for child in node.children: if child.visits == 0: return child return None # Adds the opponents moves as child nodes to player's moves (MCT will calculate best move for both sides) while fully_expanded(node): node = best_uct_node(node) return pick_unvisited(node) or node # ------------------------- # START OF ACTUAL ALGORITHM # ------------------------- # Traverse and find the leaf to recur from leaf = traverse(root) # If the leaf has an even depth, the opponent has yet to chose a move. Thus, set the opp's model to random and # take a turn. Then continue to randomly simulate the rest of the battle. if leaf.depth % 2 == 0: # Even depth also indicates the player is player 1. player = leaf.player.copy() other_player = leaf.other_player.copy() # Adding a move for opponent and taking a turn. player.set_model(leaf.model) other_player.set_model(RandomModel() if not use_damage_model else DamageModel()) battle = Battle(player, other_player, 1 if verbose else 0) winner = battle.play_turn() if winner is None: if predictor is not None and current_learning_turn[0] < learning_turns: player.set_model(RandomModel()) else: model, _, _, _, _ = predictor.predict_move(player, other_player) player.set_model(model) battle.play() else: # Odd depth indicates player is player 2. player = leaf.other_player.copy() other_player = leaf.player.copy() if predictor is not None and current_learning_turn[0] < learning_turns: player.set_model(RandomModel()) else: model, _, _, _, _ = predictor.predict_move(player, other_player) player.set_model(model) other_player.set_model(RandomModel() if not use_damage_model else DamageModel()) battle = Battle(player, other_player, 1 if verbose else 0) battle.play() outcome = calculations.outcome_func_v1(player, other_player) # On each run, calculate the outcomes via backpropagation backprop(leaf, outcome) return tree
import sys from os.path import join, dirname sys.path.append(join(dirname(__file__), '../..')) from pokemon_ai.battle import Battle from pokemon_ai.data import get_random_party from pokemon_ai.classes import Bag, Player from pokemon_ai.ai.models import RandomModel party1 = get_random_party() party2 = get_random_party() player1 = Player("Player 1", party1, Bag()) player2 = Player("Player 2", party2, Bag(), RandomModel()) battle = Battle(player1, player2) battle.play()
import sys from os.path import join, dirname sys.path.append(join(dirname(__file__), '../..')) from pokemon_ai.battle import Battle from pokemon_ai.classes import Bag, Party, Player, Move, MoveBank, Pokemon, Stats, PokemonType party1 = Party([ Pokemon(PokemonType.FIRE, "Charizard", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Flamethrower", 100, 0, PokemonType.FIRE, True)]), 300), Pokemon(PokemonType.WATER, "Piplup", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Water Squirt", 100, 3, PokemonType.WATER, True)]), 300) ]) party2 = Party([ Pokemon(PokemonType.FIRE, "Blaziken", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Flamethrower", 100, 3, PokemonType.FIRE, True)]), 300), Pokemon(PokemonType.WATER, "Squirtle", 100, Stats(300, 300, 300, 300, 300), MoveBank([Move("Water Squirt", 100, 3, PokemonType.WATER, True)]), 300) ]) player1 = Player("Player 1", party1, Bag()) player2 = Player("Player 2", party2, Bag()) battle = Battle(player1, player2, 2) battle.play()
import sys from os.path import join, dirname sys.path.append(join(dirname(__file__), '../..')) from pokemon_ai.battle import Battle from pokemon_ai.classes import Bag, Player from pokemon_ai.data import get_party from pokemon_ai.ai.models import PorygonModel, RandomModel win_count = {} for i in range(3): party1 = get_party("charizard", "venusaur") party2 = get_party("blastoise", "tentacruel") player1 = Player("MCTS", party1, Bag(), PorygonModel(), player_id=1) player2 = Player("GET HIT WIT ALLA DAT", party2, Bag(), RandomModel(), player_id=2) battle = Battle(player1, player2, 1) winner = battle.play() win_count[winner.get_name()] = int(0 if win_count.get(winner.get_name( )) is None else win_count.get(winner.get_name())) + 1 # for i in range(5): # party3 = get_party("bulbasaur", "venusaur", "ivysaur") # party4 = get_party("squirtle", "charizard", "wartortle")
def _turn_check_pokemon(self, player: Player): other_player = self.player2 if player.get_id( ) == self._PLAYER_1_ID else self.player1 clear_battle_screen() print(str(other_player.get_party()))
import sys from os.path import join, dirname sys.path.append(join(dirname(__file__), '../..')) from pokemon_ai.data import get_party from pokemon_ai.classes import Player from pokemon_ai.ai.models import RandomModel from pokemon_ai.ai.models.porygon_model.mcts import make_tree party1 = get_party("charizard", "venusaur") party2 = get_party("blastoise", "caterpie") print("%s vs. %s" % (', '.join([pkmn.get_name() for pkmn in party1.get_as_list()]), ', '.join([pkmn.get_name() for pkmn in party2.get_as_list()]))) player1 = Player("Player 1", party1, None, RandomModel(), player_id=1) player2 = Player("Player 2", party2, None, RandomModel(), player_id=2) tree = make_tree(player1, player2, 1000) tree.print() outcome_probs = tree.get_action_probabilities() for prob in outcome_probs: print("Outcome: %1.4f, Prob: %1.4f, Visits: %d, Move: %s" % prob)
def _turn_switch_pokemon(self, player: Player, none_option=True, ai_pokemon_idx: int = None): """ Called when the player must switch Pokemon. :param player: The player who is switching pokemon. :param none_option: Is the player allowed to go back? Aka, is there an option to choose "None"? :param ai_pokemon_idx: The index of the Pokemon the AI is switching out. :return: True if the player switches Pokemon, false otherwise. """ # Get the other player other_player = self.player2 if player.get_id( ) == self._PLAYER_1_ID else self.player1 current_pokemon = player.get_party().get_starting() can_switch_pokemon = not all([ pokemon.is_fainted() for pokemon in player.get_party().get_as_list() ]) if not can_switch_pokemon: return False if not player.is_ai(): while True: # Write the options out options = [ p.get_name() + " (" + str(p.get_hp()) + "/" + str(p.get_base_hp()) + " HP)" for p in player.get_party().get_as_list() ] if none_option: options.insert(0, 'None (Go back)') item = prompt_multi( 'Which Pokémon would you like to switch in?', *options)[0] # The player chooses "None" if none_option and item == 0: return False # Get the Pokemon to switch in idx = item - bool(none_option) switched_pokemon = player.get_party().get_at_index(idx) if switched_pokemon.is_fainted(): self._alert(switched_pokemon.get_name() + ' has fainted.', player) elif idx == 0: self._alert( switched_pokemon.get_name() + ' is currently in battle.', player) else: if current_pokemon.get_status() == Status.CONFUSION: current_pokemon.set_status(None) switched_pokemon.reveal() self._alert( 'Switched ' + current_pokemon.get_name() + ' with ' + switched_pokemon.get_name() + '.', player) self._alert( player.get_name() + ' switched ' + current_pokemon.get_name() + ' with ' + switched_pokemon.get_name() + '.', other_player) player.get_party().make_starting(idx) return True elif player.is_ai(): while ai_pokemon_idx is None or ai_pokemon_idx == 0 or ai_pokemon_idx >= len( player.get_party().get_as_list()): ai_pokemon_idx = player.get_model().force_switch_pokemon( player.get_party()) switched_pokemon = player.get_party().get_at_index(ai_pokemon_idx) player.get_party().make_starting(ai_pokemon_idx) if current_pokemon.get_status() == Status.CONFUSION: current_pokemon.set_status(None) switched_pokemon.reveal() self._alert( 'Switched ' + current_pokemon.get_name() + ' with ' + switched_pokemon.get_name() + '.', player) self._alert( player.get_name() + ' switched ' + current_pokemon.get_name() + ' with ' + switched_pokemon.get_name() + '.', other_player) return True else: return False
def take_turn(self, player: Player, other_player: Player, attack: Callable[[Move], None], use_item: Callable[[Item], None], switch_pokemon_at_idx: Callable[[int], None]) -> None: # I don't know what to do yet, so I'll just attack with my pokemon's first move. my_pokemon = player.get_party().get_starting() attack_move = my_pokemon.get_move_bank().get_move(0) attack(attack_move)
import sys from os.path import join, dirname sys.path.append(join(dirname(__file__), '../..')) from pokemon_ai.battle import Battle from pokemon_ai.data import get_party from pokemon_ai.classes import Bag, Player from pokemon_ai.ai.models import PorygonModel party1 = get_party("zapdos", "sandslash", "starmie", "charizard", "tauros", "chansey") # get_random_party() party2 = get_party("caterpie", "diglett", "rattata", "poliwag", "meowth", "vulpix") # get_random_party() player1 = Player("Player 1", party1, Bag()) player2 = Player("Player 2", party2, Bag(), PorygonModel()) battle = Battle(player1, player2, 2, use_hints=True) battle.play()