diff --git a/modules/battle_state.py b/modules/battle_state.py index 29bc0992..fee4bb1e 100644 --- a/modules/battle_state.py +++ b/modules/battle_state.py @@ -461,6 +461,9 @@ def is_fainted(self) -> bool: return True return False + def has_ability(self, ability: "Ability"): + return any(battler.ability.name == ability.name for battler in self.active_battlers) + class BattlePokemon: def __init__(self, data: bytes, status3: bytes, disable_struct: bytes, party_index: int): diff --git a/modules/battle_strategies/_util.py b/modules/battle_strategies/_util.py index 7b63795b..2a426134 100644 --- a/modules/battle_strategies/_util.py +++ b/modules/battle_strategies/_util.py @@ -1,10 +1,11 @@ from operator import truediv from typing import TYPE_CHECKING -from modules.battle_state import Weather, TemporaryStatus -from modules.items import ItemHoldEffect -from modules.memory import get_event_flag -from modules.pokemon import StatusCondition +from modules.battle_state import Weather, TemporaryStatus, BattleType +from modules.battle_strategies import TurnAction +from modules.items import ItemHoldEffect, get_item_bag, get_item_by_name +from modules.memory import get_event_flag, read_symbol +from modules.pokemon import StatusCondition, get_type_by_name, get_ability_by_name if TYPE_CHECKING: from modules.battle_state import BattlePokemon, BattleState @@ -39,6 +40,124 @@ class BattleStrategyUtil: def __init__(self, battle_state: "BattleState"): self._battle_state = battle_state + def get_escape_chance(self) -> float: + """ + Calculates the likelihood of an attempt to flee the battle will succeed. + + This only counts for the 'Run Away' battle option, not for other ways of escaping + (using a Poké Doll, using Teleport, ...) + + :return: A number between 0 and 1, with 0 meaning that escaping is impossible. + """ + + # Cannot run from trainer battles or R/S/E's first battle (Birch will complain) + if BattleType.Trainer in self._battle_state.type or BattleType.FirstBattle in self._battle_state.type: + return 0 + + # Smoke Ball check + battler = self._battle_state.own_side.active_battler + if battler.held_item is not None and battler.held_item.hold_effect is ItemHoldEffect.CanAlwaysRunAway: + return 1 + + if battler.ability.name == "Run Away": + return 1 + + if ( + TemporaryStatus.Rooted in battler.status_temporary + or TemporaryStatus.Wrapped in battler.status_temporary + or TemporaryStatus.EscapePrevention in battler.status_temporary + ): + return 0 + + opponent = self._battle_state.opponent.active_battler + if self._battle_state.opponent.has_ability(get_ability_by_name("Shadow Tag")): + return 0 + + if ( + self._battle_state.opponent.has_ability(get_ability_by_name("Arena Trap")) + and get_type_by_name("Flying") not in battler.types + and battler.ability.name != "Levitate" + ): + return 0 + + if opponent.ability.name == "Magnet Pull" and get_type_by_name("Steel") in battler.types: + return 0 + + if opponent.stats.speed < battler.stats.speed: + return 1 + + escape_attempts = read_symbol("gBattleStruct", 0x4C, 1)[0] + escape_chance = ((battler.stats.speed * 128) // opponent.stats.speed + (escape_attempts * 30)) % 256 + return escape_chance / 255 + + def get_best_escape_method(self) -> tuple[TurnAction, any] | None: + """ + :return: A turn action for escaping, or None if escaping is impossible. + """ + + escape_chance = self.get_escape_chance() + if escape_chance == 1: + return TurnAction.run_away() + + # Use Poké Doll or Fluffy Tail if available + if escape_chance < 0.9: + item_bag = get_item_bag() + if item_bag.quantity_of(get_item_by_name("Poké Doll")) > 0: + return TurnAction.use_item(get_item_by_name("Poké Doll")) + elif item_bag.quantity_of(get_item_by_name("Fluffy Tail")) > 0: + return TurnAction.use_item(get_item_by_name("Fluffy Tail")) + + # If escape odds are low enough, try escaping using a move + battler = self._battle_state.own_side.active_battler + opponent = self._battle_state.opponent.active_battler + if 0 < escape_chance < 0.5: + # Prefer Teleport as that might be quicker + for index in range(len(battler.moves)): + if battler.moves[index].move.name == "Teleport": + return TurnAction.use_move(index) + + # Whirlwind and Roar + if opponent.ability.name != "Suction Cups" and TemporaryStatus.Rooted not in opponent.status_temporary: + for index in range(len(battler.moves)): + if battler.moves[index].move.effect == "ROAR": + return TurnAction.use_move(index) + + # Only try to escape if it's not impossible + if escape_chance > 0: + return TurnAction.run_away() + + return None + + def can_switch(self) -> bool: + """ + :return: True if switching Pokémon is allowed at this point, False if it is impossible. + """ + battler = self._battle_state.own_side.active_battler + if ( + TemporaryStatus.Wrapped in battler.status_temporary + or TemporaryStatus.EscapePrevention in battler.status_temporary + or TemporaryStatus.Rooted in battler.status_temporary + ): + return False + + if self._battle_state.opponent.has_ability(get_ability_by_name("Shadow Tag")): + return False + + if ( + self._battle_state.opponent.has_ability(get_ability_by_name("Arena Trap")) + and get_type_by_name("Flying") not in battler.types + and battler.ability.name != "Levitate" + ): + return False + + if ( + self._battle_state.opponent.has_ability(get_ability_by_name("Magnet Pull")) + and get_type_by_name("Steel") in battler.types + ): + return False + + return True + def calculate_move_damage_range( self, move: "Move", attacker: "BattlePokemon", defender: "BattlePokemon", is_critical_hit: bool = False ) -> DamageRange: @@ -108,6 +227,19 @@ def calculate_move_damage_range( return DamageRange(max(1, _percentage(damage, 85)), damage) + def get_strongest_move_against(self, pokemon: "BattlePokemon", opponent: "BattlePokemon"): + move_strengths = [] + for learned_move in pokemon.moves: + move = learned_move.move + if learned_move.pp == 0 or pokemon.disabled_move is move: + move_strengths.append(-1) + else: + move_strengths.append(self.calculate_move_damage_range(move, pokemon, opponent).max) + + strongest_move = move_strengths.index(max(move_strengths)) + + return strongest_move + def _calculate_base_move_damage( self, move: "Move", attacker: "BattlePokemon", defender: "BattlePokemon", is_critical_hit: bool = False ): diff --git a/modules/battle_strategies/catch.py b/modules/battle_strategies/catch.py index 59ab85c8..bb62aed0 100644 --- a/modules/battle_strategies/catch.py +++ b/modules/battle_strategies/catch.py @@ -1,5 +1,5 @@ from modules.battle_state import BattleState -from modules.battle_strategies import BattleStrategy, SafariTurnAction +from modules.battle_strategies import SafariTurnAction, DefaultBattleStrategy from modules.battle_strategies import TurnAction from modules.context import context from modules.items import Item, get_item_bag @@ -8,24 +8,13 @@ from modules.pokemon import Pokemon, get_type_by_name, StatusCondition -class CatchStrategy(BattleStrategy): - def party_can_battle(self) -> bool: - return True - +class CatchStrategy(DefaultBattleStrategy): def pokemon_can_battle(self, pokemon: Pokemon) -> bool: - return True + return not pokemon.is_egg and pokemon.current_hp > 0 def should_flee_after_faint(self, battle_state: BattleState) -> bool: return False - def choose_new_lead_after_battle(self) -> int | None: - return None - - def choose_new_lead_after_faint(self, battle_state: BattleState) -> int: - context.message = "Player's Pokémon has fainted. Don't know what to do now, switching to Manual mode." - context.set_manual_mode() - return 0 - def decide_turn(self, battle_state: BattleState) -> tuple["TurnAction", any]: ball_to_throw = self._get_best_poke_ball(battle_state) if ball_to_throw is None: diff --git a/modules/battle_strategies/default.py b/modules/battle_strategies/default.py index 092dd9dd..7f302f06 100644 --- a/modules/battle_strategies/default.py +++ b/modules/battle_strategies/default.py @@ -138,23 +138,30 @@ def choose_new_lead_after_battle(self) -> int | None: return None def decide_turn(self, battle_state: BattleState) -> tuple["TurnAction", any]: + util = BattleStrategyUtil(battle_state) + if not self._pokemon_has_enough_hp(get_party()[battle_state.own_side.active_battler.party_index]): if context.config.battle.lead_cannot_battle_action == "flee" and not battle_state.is_trainer_battle: - return TurnAction.run_away() + # If it is impossible to escape, do not even attempt to do it but just keep battling. + best_escape_method = util.get_best_escape_method() + if best_escape_method is not None: + return best_escape_method elif ( context.config.battle.lead_cannot_battle_action == "rotate" and len(self._get_usable_party_indices(battle_state)) > 0 ): - return TurnAction.rotate_lead(self._select_rotation_target(battle_state)) + if util.can_switch(): + return TurnAction.rotate_lead(self._select_rotation_target(battle_state)) else: context.message = "Leading Pokémon's HP fell below the minimum threshold." return TurnAction.switch_to_manual() return TurnAction.use_move( - self._get_strongest_move_against(battle_state.own_side.active_battler, battle_state.opponent.active_battler) + util.get_strongest_move_against(battle_state.own_side.active_battler, battle_state.opponent.active_battler) ) def decide_turn_in_double_battle(self, battle_state: BattleState, battler_index: int) -> tuple["TurnAction", any]: + util = BattleStrategyUtil(battle_state) battler = battle_state.own_side.left_battler if battler_index == 0 else battle_state.own_side.right_battler partner = battle_state.own_side.right_battler if battler_index == 0 else battle_state.own_side.left_battler pokemon = get_party()[battler.party_index] @@ -163,22 +170,26 @@ def decide_turn_in_double_battle(self, battle_state: BattleState, battler_index: if not self._pokemon_has_enough_hp(pokemon): if partner_pokemon is None or not self._pokemon_has_enough_hp(partner_pokemon): if context.config.battle.lead_cannot_battle_action == "flee" and not battle_state.is_trainer_battle: - return TurnAction.run_away() + # If it is impossible to escape, do not even attempt to do it but just keep battling. + best_escape_method = util.get_best_escape_method() + if best_escape_method is not None: + return best_escape_method elif ( context.config.battle.lead_cannot_battle_action == "rotate" and len(self._get_usable_party_indices(battle_state)) > 0 ): - return TurnAction.rotate_lead(self._select_rotation_target(battle_state)) + if util.can_switch(): + return TurnAction.rotate_lead(self._select_rotation_target(battle_state)) else: context.message = "Both battling Pokémon's HP fell below the minimum threshold." return TurnAction.switch_to_manual() if battle_state.opponent.left_battler is not None: opponent = battle_state.opponent.left_battler - return TurnAction.use_move_against_left_side_opponent(self._get_strongest_move_against(battler, opponent)) + return TurnAction.use_move_against_left_side_opponent(util.get_strongest_move_against(battler, opponent)) else: opponent = battle_state.opponent.right_battler - return TurnAction.use_move_against_right_side_opponent(self._get_strongest_move_against(battler, opponent)) + return TurnAction.use_move_against_right_side_opponent(util.get_strongest_move_against(battler, opponent)) def decide_turn_in_safari_zone(self, battle_state: BattleState) -> tuple["SafariTurnAction", any]: return SafariTurnAction.switch_to_manual() @@ -243,18 +254,3 @@ def _move_is_usable(self, move: LearnedMove): and move.pp > 0 and move.move.name not in context.config.battle.banned_moves ) - - def _get_strongest_move_against(self, pokemon: BattlePokemon, opponent: BattlePokemon): - util = BattleStrategyUtil(get_battle_state()) - - move_strengths = [] - for learned_move in pokemon.moves: - move = learned_move.move - if learned_move.pp == 0: - move_strengths.append(-1) - else: - move_strengths.append(util.calculate_move_damage_range(move, pokemon, opponent).max) - - strongest_move = move_strengths.index(max(move_strengths)) - - return strongest_move diff --git a/modules/battle_strategies/level_balancing.py b/modules/battle_strategies/level_balancing.py index 655713d9..1dfdf7a8 100644 --- a/modules/battle_strategies/level_balancing.py +++ b/modules/battle_strategies/level_balancing.py @@ -1,5 +1,5 @@ from modules.battle_state import BattleState -from modules.battle_strategies import DefaultBattleStrategy, TurnAction +from modules.battle_strategies import DefaultBattleStrategy, TurnAction, BattleStrategyUtil from modules.context import context from modules.pokemon import get_party @@ -24,6 +24,7 @@ def choose_new_lead_after_battle(self) -> int | None: return lowest_level_index if lowest_level_index > 0 else None def decide_turn(self, battle_state: BattleState) -> tuple["TurnAction", any]: + util = BattleStrategyUtil(battle_state) battler = battle_state.own_side.active_battler # If the lead Pokémon (the one with the lowest level) is on low HP, try switching @@ -36,7 +37,7 @@ def decide_turn(self, battle_state: BattleState) -> tuple["TurnAction", any]: for index in range(len(party)): if self.pokemon_can_battle(party[index]) and party[index].level > strongest_pokemon[1]: strongest_pokemon = (index, party[index].level) - if strongest_pokemon[0] > 0: + if strongest_pokemon[0] > 0 and util.can_switch(): return TurnAction.rotate_lead(strongest_pokemon[0]) return super().decide_turn(battle_state) diff --git a/modules/battle_strategies/lose_on_purpose.py b/modules/battle_strategies/lose_on_purpose.py index 5df79090..b0a40a97 100644 --- a/modules/battle_strategies/lose_on_purpose.py +++ b/modules/battle_strategies/lose_on_purpose.py @@ -8,6 +8,146 @@ from modules.pokemon import Pokemon, Move +def _get_weakest_move_against(battle_state: "BattleState", pokemon: "BattlePokemon", opponent: "BattlePokemon"): + util = BattleStrategyUtil(battle_state) + + move_strengths = [] + for learned_move in pokemon.moves: + move = learned_move.move + + # Never use moves that ran out of PP, unless _all_ moves are out of PPs in + # which case we're using Struggle either way. + if learned_move.pp == 0 or pokemon.disabled_move is move: + move_strengths.append(99999) + continue + + # Calculate effective power of the move. + move_power = util.calculate_move_damage_range(move, pokemon, opponent).max + + # Doing nothing is always the best idea. + if move.effect == "SPLASH": + move_power = -1 + + # Moves that might end the battle. + if move.effect == "ROAR" and BattleType.Trainer not in battle_state.type: + move_power = 99998 + + # Moves that give an invulnerable turn (Fly, Dig, ...), as we WANT to be damaged. + if move.effect == "SEMI_INVULNERABLE": + move_power *= 2 + + # Moves that hit multiple times + if move.effect == "DOUBLE_HIT": + move_power *= 2 + if move.effect == "MULTI_HIT": + move_power *= 3 + if move.effect == "TRIPLE_KICK": + move_power *= 5 + + # Moves that would restore our HP. + if move.effect == "ABSORB": + move_power *= 1.5 + + if move.effect in ("SOFTBOILED", "MOONLIGHT", "MORNING_SUN", "SYNTHESIS", "RECOVER"): + move_power = 1500 + elif move.effect in "REST": + # Rest is the preferable healing move because it at least puts us to sleep. + move_power = 1490 + + # Moves that would remove damaging status effects. + if pokemon.status_permanent is not StatusCondition.Healthy and move.effect == "HEAL_BELL": + move_power = 1470 + elif ( + pokemon.status_permanent + in (StatusCondition.Poison, StatusCondition.BadPoison, StatusCondition.Burn, StatusCondition.Paralysis) + and move.effect == "REFRESH" + ): + move_power = 1470 + + # Moves that would decrease the opponent's accuracy or our own evasion. + if move.effect in ("ACCURACY_DOWN", "ACCURACY_DOWN_HIT", "EVASION_UP"): + move_power += 25 + + # Moves that would increase our own Defence. + if move.effect in ("DEFENSE_CURL", "DEFENSE_UP", "DEFENSE_UP_2", "DEFENSE_UP_HIT", "SPECIES_DEFENSE_UP_2"): + move_power += 15 + + # Moves that would decrease the opponent's attack power. + if move.effect in ("ATTACK_DOWN", "ATTACK_DOWN_2", "ATTACK_DOWN_HIT", "SPECIAL_ATTACK_DOWN_HIT"): + move_power += 15 + + # Add bonus for moves that would make US faint + if move.effect == "EXPLOSION": + move_power *= 0.85 + + # Add bonus for moves that inflict recoil damage + if move.effect == "DOUBLE_EDGE": + move_power *= 1 - 0.33 + if move.effect == "RECOIL": + move_power *= 1 - 0.25 + + # One-hit KO moves should not be risked, unless the opponent's level is higher than ours + # (in which case OHKO moves always fail) + if move.effect == "OHKO" and pokemon.level >= opponent.level: + move_power = 99997 + + # Moves might might inflict a status condition + if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( + "TRI_ATTACK", + "SECRET_POWER", + ): + move_power += 50 + + # Moves that might poison the target + if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( + "POISON", + "POISON_FANG", + "POISON_HIT", + "POISON_TAIL", + "TOXIC", + "TWINEEDLE", + ): + move_power += 50 + + # Moves that might burn the target + if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( + "BURN_HIT", + "BLAZE_KICK", + "THAW_HIT", + "TRI_ATTACK", + "WILL_O_WISP", + ): + move_power += 50 + + # Moves that might inflict paralysis + if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( + "PARALYZE", + "PARALYZE_HIT", + "THUNDER", + ): + move_power += 20 + + # Moves that might freeze the opponent + if opponent.status_permanent is StatusCondition.Healthy and move.effect == "FREEZE_HIT": + move_power += 45 + + # Moves that might make the opponent flinch + if move.effect in ("FLINCH_MINIMIZE_HIT", "FLINCH_HIT", "TWISTER"): + move_power += 5 + + # Bonus for moves that can only hit in certain conditions + if move.effect == "SNORE" and pokemon.status_permanent is not StatusCondition.Sleep: + move_power = 0 + + if move.effect == "FAKE_OUT" and battle_state.current_turn > 1: + move_power = 0 + + move_strengths.append(move_power) + + weakest_move = move_strengths.index(min(move_strengths)) + return weakest_move + + class LoseOnPurposeBattleStrategy(BattleStrategy): def party_can_battle(self) -> bool: return True @@ -35,7 +175,7 @@ def choose_new_lead_after_faint(self, battle_state: "BattleState") -> int: def decide_turn(self, battle_state: "BattleState") -> tuple["TurnAction", any]: return TurnAction.use_move( - self._get_weakest_move_against( + _get_weakest_move_against( battle_state, battle_state.own_side.active_battler, battle_state.opponent.active_battler ) ) @@ -46,151 +186,10 @@ def decide_turn_in_double_battle(self, battle_state: "BattleState", battler_inde if battle_state.opponent.left_battler is not None: opponent = battle_state.opponent.left_battler return TurnAction.use_move_against_left_side_opponent( - self._get_weakest_move_against(battle_state, battler, opponent) + _get_weakest_move_against(battle_state, battler, opponent) ) else: opponent = battle_state.opponent.right_battler return TurnAction.use_move_against_right_side_opponent( - self._get_weakest_move_against(battle_state, battler, opponent) + _get_weakest_move_against(battle_state, battler, opponent) ) - - def _get_weakest_move_against( - self, battle_state: "BattleState", pokemon: "BattlePokemon", opponent: "BattlePokemon" - ): - util = BattleStrategyUtil(battle_state) - - move_strengths = [] - for learned_move in pokemon.moves: - move = learned_move.move - - # Never use moves that ran out of PP, unless _all_ moves are out of PPs in - # which case we're using Struggle either way. - if learned_move.pp == 0: - move_strengths.append(99999) - continue - - # Calculate effective power of the move. - move_power = util.calculate_move_damage_range(move, pokemon, opponent).max - - # Doing nothing is always the best idea. - if move.effect == "SPLASH": - move_power = -1 - - # Moves that might end the battle. - if move.effect == "ROAR" and BattleType.Trainer not in battle_state.type: - move_power = 99998 - - # Moves that give an invulnerable turn (Fly, Dig, ...), as we WANT to be damaged. - if move.effect == "SEMI_INVULNERABLE": - move_power *= 2 - - # Moves that hit multiple times - if move.effect == "DOUBLE_HIT": - move_power *= 2 - if move.effect == "MULTI_HIT": - move_power *= 3 - if move.effect == "TRIPLE_KICK": - move_power *= 5 - - # Moves that would restore our HP. - if move.effect == "ABSORB": - move_power *= 1.5 - - if move.effect in ("SOFTBOILED", "MOONLIGHT", "MORNING_SUN", "SYNTHESIS", "RECOVER"): - move_power = 1500 - elif move.effect in "REST": - # Rest is the preferable healing move because it at least puts us to sleep. - move_power = 1490 - - # Moves that would remove damaging status effects. - if pokemon.status_permanent is not StatusCondition.Healthy and move.effect == "HEAL_BELL": - move_power = 1470 - elif ( - pokemon.status_permanent - in (StatusCondition.Poison, StatusCondition.BadPoison, StatusCondition.Burn, StatusCondition.Paralysis) - and move.effect == "REFRESH" - ): - move_power = 1470 - - # Moves that would decrease the opponent's accuracy or our own evasion. - if move.effect in ("ACCURACY_DOWN", "ACCURACY_DOWN_HIT", "EVASION_UP"): - move_power += 25 - - # Moves that would increase our own Defence. - if move.effect in ("DEFENSE_CURL", "DEFENSE_UP", "DEFENSE_UP_2", "DEFENSE_UP_HIT", "SPECIES_DEFENSE_UP_2"): - move_power += 15 - - # Moves that would decrease the opponent's attack power. - if move.effect in ("ATTACK_DOWN", "ATTACK_DOWN_2", "ATTACK_DOWN_HIT", "SPECIAL_ATTACK_DOWN_HIT"): - move_power += 15 - - # Add bonus for moves that would make US faint - if move.effect == "EXPLOSION": - move_power *= 0.85 - - # Add bonus for moves that inflict recoil damage - if move.effect == "DOUBLE_EDGE": - move_power *= 1 - 0.33 - if move.effect == "RECOIL": - move_power *= 1 - 0.25 - - # One-hit KO moves should not be risked, unless the opponent's level is higher than ours - # (in which case OHKO moves always fail) - if move.effect == "OHKO" and pokemon.level >= opponent.level: - move_power = 99997 - - # Moves might might inflict a status condition - if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( - "TRI_ATTACK", - "SECRET_POWER", - ): - move_power += 50 - - # Moves that might poison the target - if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( - "POISON", - "POISON_FANG", - "POISON_HIT", - "POISON_TAIL", - "TOXIC", - "TWINEEDLE", - ): - move_power += 50 - - # Moves that might burn the target - if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( - "BURN_HIT", - "BLAZE_KICK", - "THAW_HIT", - "TRI_ATTACK", - "WILL_O_WISP", - ): - move_power += 50 - - # Moves that might inflict paralysis - if opponent.status_permanent is StatusCondition.Healthy and move.effect in ( - "PARALYZE", - "PARALYZE_HIT", - "THUNDER", - ): - move_power += 20 - - # Moves that might freeze the opponent - if opponent.status_permanent is StatusCondition.Healthy and move.effect == "FREEZE_HIT": - move_power += 45 - - # Moves that might make the opponent flinch - if move.effect in ("FLINCH_MINIMIZE_HIT", "FLINCH_HIT", "TWISTER"): - move_power += 5 - - # Bonus for moves that can only hit in certain conditions - if move.effect == "SNORE" and pokemon.status_permanent is not StatusCondition.Sleep: - move_power = 0 - - if move.effect == "FAKE_OUT" and battle_state.current_turn > 1: - move_power = 0 - - move_strengths.append(move_power) - - weakest_move = move_strengths.index(min(move_strengths)) - return weakest_move diff --git a/modules/battle_strategies/run_away.py b/modules/battle_strategies/run_away.py index 7b9e0fcf..e237b5e3 100644 --- a/modules/battle_strategies/run_away.py +++ b/modules/battle_strategies/run_away.py @@ -1,35 +1,43 @@ from modules.battle_state import BattleState -from modules.battle_strategies import BattleStrategy, TurnAction, SafariTurnAction -from modules.pokemon import Pokemon, Move +from modules.battle_strategies import ( + TurnAction, + SafariTurnAction, + BattleStrategyUtil, + DefaultBattleStrategy, +) -class RunAwayStrategy(BattleStrategy): - def party_can_battle(self) -> bool: - return False - - def pokemon_can_battle(self, pokemon: Pokemon) -> bool: - return False - - def which_move_should_be_replaced(self, pokemon: Pokemon, new_move: Move) -> int: - return 4 - - def should_allow_evolution(self, pokemon: Pokemon, party_index: int) -> bool: - return False - +class RunAwayStrategy(DefaultBattleStrategy): def should_flee_after_faint(self, battle_state: BattleState) -> bool: return True def choose_new_lead_after_battle(self) -> int | None: return False - def choose_new_lead_after_faint(self, battle_state: BattleState) -> int: - return 0 - def decide_turn(self, battle_state: BattleState) -> tuple["TurnAction", any]: - return TurnAction.run_away() + util = BattleStrategyUtil(battle_state) + best_escape_method = util.get_best_escape_method() + if best_escape_method is not None: + return best_escape_method + else: + return TurnAction.use_move( + util.get_strongest_move_against( + battle_state.own_side.active_battler, battle_state.opponent.active_battler + ) + ) def decide_turn_in_double_battle(self, battle_state: BattleState, battler_index: int) -> tuple["TurnAction", any]: - return TurnAction.run_away() + util = BattleStrategyUtil(battle_state) + battler = battle_state.own_side.left_battler if battler_index == 0 else battle_state.own_side.right_battler + best_escape_method = util.get_best_escape_method() + if best_escape_method is not None: + return best_escape_method + elif battle_state.opponent.left_battler is not None: + opponent = battle_state.opponent.left_battler + return TurnAction.use_move_against_left_side_opponent(util.get_strongest_move_against(battler, opponent)) + else: + opponent = battle_state.opponent.right_battler + return TurnAction.use_move_against_right_side_opponent(util.get_strongest_move_against(battler, opponent)) def decide_turn_in_safari_zone(self, battle_state: BattleState) -> tuple["SafariTurnAction", any]: return SafariTurnAction.run_away() diff --git a/modules/items.py b/modules/items.py index 0f08b4d6..6672f4fc 100644 --- a/modules/items.py +++ b/modules/items.py @@ -124,7 +124,7 @@ class ItemHoldEffect(Enum): SoulDew = "soul_dew" DeepSeaTooth = "deep_sea_tooth" DeepSeaScale = "deep_sea_scale" - CanAlwaysRunAWay = "can_always_run_away" + CanAlwaysRunAway = "can_always_run_away" PreventEvolve = "prevent_evolve" FocusBand = "focus_band" LuckyEgg = "lucky_egg"