Skip to content

Commit

Permalink
Merge pull request #639 from hanzi/item-steal
Browse files Browse the repository at this point in the history
Add Item Steal mode
  • Loading branch information
hanzi authored Jan 25, 2025
2 parents a1c44bf + 895ceff commit d76dbe7
Show file tree
Hide file tree
Showing 10 changed files with 291 additions and 80 deletions.
12 changes: 12 additions & 0 deletions modules/battle_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,18 @@ def moves(self) -> list[LearnedMove | None]:
result.append(LearnedMove(move, total_pp, current_pp, total_pp - move.pp))
return result

def knows_move(self, move: str | Move, with_pp_remaining: bool = False):
if isinstance(move, Move):
move = move.name
for learned_move in self.moves:
if (
learned_move is not None
and learned_move.move.name == move
and (not with_pp_remaining or learned_move.pp > 0)
):
return True
return False

@property
def is_egg(self) -> bool:
return bool(self._data[0x17] & 1)
Expand Down
81 changes: 81 additions & 0 deletions modules/battle_strategies/item_stealing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
from typing import Optional

from modules.battle_state import BattleState, BattlePokemon, TemporaryStatus
from modules.battle_strategies import DefaultBattleStrategy, BattleStrategyUtil
from modules.battle_strategies import TurnAction
from modules.pokemon import Pokemon, get_type_by_name
from modules.pokemon_party import get_party


class ItemStealingBattleStrategy(DefaultBattleStrategy):
def __init__(self):
super().__init__()
self._victims = set()

def party_can_battle(self) -> bool:
return any([self._can_steal_item(pokemon, None) for pokemon in get_party().non_eggs])

def decide_turn(self, battle_state: BattleState) -> tuple["TurnAction", Optional[any]]:
battler = battle_state.own_side.active_battler
opponent = battle_state.opponent.active_battler

if len(opponent.species.held_items) and (
opponent.personality_value not in self._victims or opponent.held_item is not None
):
if self._can_steal_item(battler, opponent):
for index, learned_move in enumerate(battler.moves):
if learned_move.move.name in ("Covet", "Thief"):
self._victims.add(opponent.personality_value)
return TurnAction.use_move(index)
else:
best_thief_index = self._get_strongest_thief_index()
if best_thief_index is not None and best_thief_index != battler.party_index:
return TurnAction.rotate_lead(best_thief_index)
elif not battle_state.is_trainer_battle and (
escape_method := BattleStrategyUtil(battle_state).get_best_escape_method()
):
return escape_method

return super().decide_turn(battle_state)

def choose_new_lead_after_battle(self) -> int | None:
best_thief_index = self._get_strongest_thief_index()
return best_thief_index if best_thief_index is not None and best_thief_index > 0 else None

def choose_new_lead_after_faint(self, battle_state: BattleState) -> int:
best_thief_index = self._get_strongest_thief_index()
return best_thief_index if best_thief_index is not None else super().choose_new_lead_after_faint(battle_state)

def _can_steal_item(self, pokemon: BattlePokemon | Pokemon, opponent: BattlePokemon | None) -> bool:
possible_moves = ("Thief", "Covet")
if opponent is not None:
if opponent.ability.name == "Sticky Hold":
return False

if TemporaryStatus.Substitute in opponent.status_temporary:
return False

if get_type_by_name("Ghost") in opponent.types:
possible_moves = ("Thief",)

if pokemon.current_hp == 0:
return False

for learned_move in pokemon.moves:
if learned_move is not None and learned_move.move.name in possible_moves and learned_move.pp > 0:
if isinstance(pokemon, BattlePokemon) and pokemon.disabled_move is learned_move.move:
continue
else:
return True

return False

def _get_strongest_thief_index(self) -> int | None:
strongest_thief = None
strongest_thief_level = 0
for index, pokemon in enumerate(get_party()):
if not pokemon.is_egg and self._can_steal_item(pokemon, None) and pokemon.level > strongest_thief_level:
strongest_thief = index
strongest_thief_level = pokemon.level

return strongest_thief
2 changes: 2 additions & 0 deletions modules/modes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def get_bot_modes() -> list[Type[BotMode]]:
from .feebas import FeebasMode
from .fishing import FishingMode
from .game_corner import GameCornerMode
from .item_steal import ItemStealMode
from .kecleon import KecleonMode
from .level_grind import LevelGrindMode
from .nugget_bridge import NuggetBridgeMode
Expand All @@ -45,6 +46,7 @@ def get_bot_modes() -> list[Type[BotMode]]:
FeebasMode,
FishingMode,
GameCornerMode,
ItemStealMode,
KecleonMode,
LevelGrindMode,
NuggetBridgeMode,
Expand Down
53 changes: 53 additions & 0 deletions modules/modes/item_steal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from typing import Generator

from modules.battle_state import BattleOutcome
from modules.battle_strategies import BattleStrategy
from modules.battle_strategies.item_stealing import ItemStealingBattleStrategy
from modules.encounter import EncounterInfo
from modules.map import get_map_data_for_current_position
from modules.modes import BotMode, BattleAction, BotModeError
from modules.modes.util import map_has_pokemon_center_nearby
from modules.modes.util.pokecenter_loop import PokecenterLoopController
from modules.pokemon_party import get_party


class ItemStealMode(BotMode):
@staticmethod
def name() -> str:
return "Item Steal"

@staticmethod
def is_selectable() -> bool:
current_location = get_map_data_for_current_position()
if current_location is None:
return False

party = get_party()
if not party.has_pokemon_with_move("Thief") and not party.has_pokemon_with_move("Covet"):
return False

return current_location.has_encounters and map_has_pokemon_center_nearby(current_location.map_group_and_number)

def __init__(self):
super().__init__()
self._controller = PokecenterLoopController()
self._controller.battle_strategy = ItemStealingBattleStrategy

def on_battle_started(self, encounter: EncounterInfo | None) -> BattleAction | BattleStrategy | None:
return self._controller.on_battle_started(encounter)

def on_battle_ended(self, outcome: BattleOutcome) -> None:
self._controller.on_battle_ended()

def on_whiteout(self) -> bool:
return self._controller.on_whiteout()

def run(self) -> Generator:
party = get_party()
if not party.has_pokemon_with_move("Thief") and not party.has_pokemon_with_move("Covet"):
raise BotModeError(
"You do not have a Pokémon that knows either Thief or Covet. One of these is needed to steal items."
)

self._controller.verify_on_start()
yield from self._controller.run()
97 changes: 18 additions & 79 deletions modules/modes/level_grind.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,18 @@

from modules.context import context
from modules.map import get_map_data_for_current_position
from modules.map_data import get_map_enum
from modules.modes import BattleAction
from modules.player import get_player_avatar
from modules.pokemon import StatusCondition
from modules.pokemon_party import get_party
from ._asserts import assert_party_has_damaging_move
from ._interface import BotMode, BotModeError
from .util import navigate_to, heal_in_pokemon_center, change_lead_party_pokemon, spin
from .util.map import map_has_pokemon_center_nearby, find_closest_pokemon_center
from .util import change_lead_party_pokemon
from .util.map import map_has_pokemon_center_nearby
from .util.pokecenter_loop import PokecenterLoopController
from ..battle_state import BattleOutcome
from ..battle_strategies import BattleStrategy, DefaultBattleStrategy
from ..battle_strategies import BattleStrategy
from ..battle_strategies.level_balancing import LevelBalancingBattleStrategy
from ..battle_strategies.level_up import LevelUpLeadBattleStrategy
from ..encounter import handle_encounter, EncounterInfo
from ..encounter import EncounterInfo
from ..gui.multi_select_window import ask_for_choice, Selection, ask_for_confirmation
from ..runtime import get_sprites_path
from ..sprites import get_sprite
Expand All @@ -36,59 +34,22 @@ def is_selectable() -> bool:

def __init__(self):
super().__init__()
self._leave_pokemon_center = False
self._go_healing = True
self._level_balance = False
self._controller = PokecenterLoopController()

def on_battle_started(self, encounter: EncounterInfo | None) -> BattleAction | BattleStrategy | None:
action = handle_encounter(encounter, enable_auto_battle=True)
if action is BattleAction.Fight:
if self._level_balance:
return LevelBalancingBattleStrategy()
else:
return LevelUpLeadBattleStrategy()
else:
return action
return self._controller.on_battle_started(encounter)

def on_battle_ended(self, outcome: "BattleOutcome") -> None:
lead_pokemon = get_party()[0]

if self._level_balance:
if (
not DefaultBattleStrategy().pokemon_can_battle(lead_pokemon)
or lead_pokemon.status_condition is not StatusCondition.Healthy
):
self._go_healing = True
else:
if lead_pokemon.current_hp_percentage < context.config.battle.hp_threshold:
self._go_healing = True

lead_pokemon_has_damaging_moves = False
lead_pokemon_has_damaging_move_with_pp = False
for learned_move in lead_pokemon.moves:
if (
learned_move is not None
and learned_move.move.base_power > 1
and learned_move.move.name not in context.config.battle.banned_moves
):
lead_pokemon_has_damaging_moves = True
if learned_move.pp > 0:
lead_pokemon_has_damaging_move_with_pp = True
if lead_pokemon_has_damaging_moves and not lead_pokemon_has_damaging_move_with_pp:
self._go_healing = True

if not LevelUpLeadBattleStrategy().party_can_battle():
self._go_healing = True
return self._controller.on_battle_ended()

def on_whiteout(self) -> bool:
self._leave_pokemon_center = True
return True
return self._controller.on_whiteout()

def run(self) -> Generator:
# Check training spot first to see if it has encounters to not print multi choices windows for nothing
training_spot = self._get_training_spot()
self._controller.verify_on_start()

party_lead_pokemon, party_lead_index = self._get_party_lead()
party_lead_pokemon = get_party().non_eggs[0]
level_mode_choice = self._ask_for_leveling_mode(party_lead_pokemon)

if level_mode_choice is None:
Expand All @@ -97,8 +58,12 @@ def run(self) -> Generator:
return

if level_mode_choice.startswith("Level-balance"):
self._level_balance = True
self._controller.battle_strategy = LevelBalancingBattleStrategy
party_lead_index = LevelBalancingBattleStrategy().choose_new_lead_after_battle()
if party_lead_index != 0:
yield from change_lead_party_pokemon(party_lead_index)
else:
self._controller.battle_strategy = LevelUpLeadBattleStrategy
assert_party_has_damaging_move("No Pokémon in the party has a usable attacking move!")

if not LevelUpLeadBattleStrategy().pokemon_can_battle(party_lead_pokemon):
Expand All @@ -116,20 +81,10 @@ def run(self) -> Generator:
"Cannot level grind because your leader has no battle moves and lead_cannot_battle_action is set to flee!"
)

if user_confirmed:
self._level_balance = False
else:
if not user_confirmed:
context.set_manual_mode()

if self._level_balance:
party_lead_index = LevelBalancingBattleStrategy().choose_new_lead_after_battle()

if party_lead_index:
yield from change_lead_party_pokemon(party_lead_index)

pokemon_center = find_closest_pokemon_center(training_spot)

yield from self._leveling_loop(training_spot, pokemon_center)
yield from self._controller.run()

def _get_training_spot(self):
training_spot = get_map_data_for_current_position()
Expand All @@ -154,19 +109,3 @@ def _ask_for_leveling_mode(self, party_lead_pokemon):
],
"What to level?",
)

def _leveling_loop(self, training_spot, pokemon_center):
training_spot_map = get_map_enum(training_spot)
training_spot_coordinates = training_spot.local_position

while True:
if self._leave_pokemon_center:
yield from navigate_to(get_player_avatar().map_group_and_number, (7, 8))
elif self._go_healing:
yield from heal_in_pokemon_center(pokemon_center)

self._leave_pokemon_center = False
self._go_healing = False

yield from navigate_to(training_spot_map, training_spot_coordinates)
yield from spin(stop_condition=lambda: self._go_healing or self._leave_pokemon_center)
1 change: 1 addition & 0 deletions modules/modes/util/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from .event_flags_and_vars import *
from .higher_level_actions import *
from .items import *
from .map import *
from .sleep import *
from .soft_reset import *
from .tasks_scripts import *
Expand Down
Loading

0 comments on commit d76dbe7

Please sign in to comment.