Skip to content

Commit

Permalink
Check if fleeing/switching is possible before attempting these action…
Browse files Browse the repository at this point in the history
…s, don't use disabled moves
  • Loading branch information
hanzi committed Oct 3, 2024
1 parent 24131e6 commit a0a2193
Show file tree
Hide file tree
Showing 8 changed files with 335 additions and 207 deletions.
3 changes: 3 additions & 0 deletions modules/battle_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
140 changes: 136 additions & 4 deletions modules/battle_strategies/_util.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
):
Expand Down
17 changes: 3 additions & 14 deletions modules/battle_strategies/catch.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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:
Expand Down
40 changes: 18 additions & 22 deletions modules/battle_strategies/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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()
Expand Down Expand Up @@ -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
5 changes: 3 additions & 2 deletions modules/battle_strategies/level_balancing.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand All @@ -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)
Loading

0 comments on commit a0a2193

Please sign in to comment.