Skip to content

Commit

Permalink
Merge pull request #642 from hanzi/verify-space-for-pokemon
Browse files Browse the repository at this point in the history
Verify that there is space in party/boxes before running modes that catch Pokémon
  • Loading branch information
hanzi authored Jan 31, 2025
2 parents 20f5823 + 4e07fc5 commit 0815e9c
Show file tree
Hide file tree
Showing 14 changed files with 103 additions and 29 deletions.
41 changes: 36 additions & 5 deletions modules/modes/_asserts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from modules.safari_strategy import get_safari_balls_left
from modules.save_data import get_save_data
from ._interface import BotModeError
from ..pokemon_storage import get_pokemon_storage

_error_message_addendum_if_assert_only_failed_in_saved_game = (
" (This is only the case in the saved game. Perhaps you just need to save again?)"
Expand Down Expand Up @@ -166,17 +167,47 @@ def assert_empty_slot_in_party(error_message: str, check_in_saved_game: bool = F
raise BotModeError(error_message)


def assert_player_has_poke_balls() -> None:
def assert_boxes_or_party_can_fit_pokemon(error_message: str | None = None, check_in_saved_game: bool = False) -> None:
"""
Raises an exception if all boxes are full and there is no empty slot in the player's party,
i.e. if catching a Pokémon will fail due to lack of space.
:param error_message: Error message to display if the assertion fails.
:param check_in_saved_game: If True, this assertion will check the saved game instead of the
current party (which is the default.)
"""
pc_storage_capacity = 30 * 14

if error_message is None:
error_message = "Both the party and all the boxes are full. Cannot catch any more Pokémon."

if check_in_saved_game:
save_data = get_save_data()
if len(save_data.get_party()) >= 6 and save_data.get_pokemon_storage().pokemon_count >= pc_storage_capacity:
if len(get_party()) < 6 or get_pokemon_storage().pokemon_count < pc_storage_capacity:
error_message += _error_message_addendum_if_assert_only_failed_in_saved_game
raise BotModeError(error_message)
elif len(get_party()) >= 6 and get_pokemon_storage().pokemon_count >= pc_storage_capacity:
raise BotModeError(error_message)


def assert_player_has_poke_balls(check_in_saved_game: bool = False) -> None:
"""
Raises an exception if the player doesn't have any Pokeballs when starting a catching mode
or if safari ball threshold is reached.
"""
out_of_safari_balls_error = "You have less than 15 Safari balls left, switching to manual mode..."
out_of_poke_balls_error = "Out of Poké balls! Better grab more before the next shiny slips away..."

if is_safari_map():
if get_safari_balls_left() < 15:
raise BotModeError("You have less than 15 balls left, switching to manual mode...")
else:
if get_item_bag().number_of_balls_except_master_ball == 0:
raise BotModeError("Out of Pokéballs! Better grab more before the next shiny slips away...")
raise BotModeError(out_of_safari_balls_error)
elif check_in_saved_game and get_save_data().get_item_bag().number_of_balls_except_master_ball == 0:
if get_item_bag().number_of_balls_except_master_ball > 1:
raise BotModeError(out_of_poke_balls_error + _error_message_addendum_if_assert_only_failed_in_saved_game)
else:
raise BotModeError(out_of_poke_balls_error)
elif get_item_bag().number_of_balls_except_master_ball == 0:
raise BotModeError(out_of_poke_balls_error)


def pokemon_has_usable_damaging_move(pokemon: Pokemon) -> bool:
Expand Down
6 changes: 4 additions & 2 deletions modules/modes/bunny_hop.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from modules.items import get_item_by_name
from modules.player import AcroBikeState, TileTransitionState, get_player_avatar
from modules.battle_state import BattleOutcome
from ._asserts import assert_item_exists_in_bag, assert_player_has_poke_balls
from ._asserts import assert_item_exists_in_bag, assert_player_has_poke_balls, assert_boxes_or_party_can_fit_pokemon
from ._interface import BotMode
from .util import apply_white_flute_if_available, register_key_item

Expand All @@ -22,11 +22,13 @@ def is_selectable() -> bool:
return False

def on_battle_ended(self, outcome: "BattleOutcome") -> None:
if not outcome == BattleOutcome.Lost:
if outcome is not BattleOutcome.Lost:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

def run(self) -> Generator:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()
assert_item_exists_in_bag(("Acro Bike",), "You need to have the Acro Bike in order to use this mode.")
yield from register_key_item(get_item_by_name("Acro Bike"))

Expand Down
8 changes: 4 additions & 4 deletions modules/modes/feebas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@
from modules.player import get_player, get_player_avatar, AvatarFlags
from modules.pokemon_party import get_party
from . import BattleAction
from ._asserts import assert_player_has_poke_balls
from ._asserts import assert_player_has_poke_balls, assert_boxes_or_party_can_fit_pokemon
from ._interface import BotMode, BotModeError
from .util import (
ensure_facing_direction,
fish,
navigate_to,
register_key_item,
wait_for_task_to_start_and_finish,
walk_one_tile,
)
from ..clock import ClockTime, get_clock_time
from ..encounter import EncounterInfo
Expand Down Expand Up @@ -149,11 +147,13 @@ def on_battle_started(self, encounter: EncounterInfo | None) -> BattleAction | N
return None

def on_battle_ended(self, outcome: "BattleOutcome") -> None:
if not outcome == BattleOutcome.Lost:
if outcome is not BattleOutcome.Lost:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

def run(self) -> Generator:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

if not get_player_avatar().flags.Surfing:
raise BotModeError("Player is not surfing, only start this mode while surfing in any water at Route 119.")
Expand Down
9 changes: 5 additions & 4 deletions modules/modes/fishing.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from typing import Generator

from modules.context import context
from modules.battle_state import BattleOutcome
from modules.gui.multi_select_window import Selection, ask_for_choice
from modules.items import get_item_bag, get_item_by_name
from modules.player import get_player, get_player_avatar
from modules.runtime import get_sprites_path
from modules.battle_state import BattleOutcome
from ._asserts import assert_item_exists_in_bag, assert_player_has_poke_balls
from ._asserts import assert_item_exists_in_bag, assert_player_has_poke_balls, assert_boxes_or_party_can_fit_pokemon
from ._interface import BotMode
from .util import fish, register_key_item

Expand All @@ -23,11 +22,13 @@ def is_selectable() -> bool:
return targeted_tile is not None and targeted_tile.is_surfable

def on_battle_ended(self, outcome: "BattleOutcome") -> None:
if not outcome == BattleOutcome.Lost:
if outcome is not BattleOutcome.Lost:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

def run(self) -> Generator:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

# Ask player to register a rod if they have one
rod_names = ["Old Rod", "Good Rod", "Super Rod"]
Expand Down
7 changes: 6 additions & 1 deletion modules/modes/kecleon.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from modules.pokemon_party import get_party_size
from modules.save_data import get_last_heal_location
from . import BattleAction
from ._asserts import assert_has_pokemon_with_any_move, assert_player_has_poke_balls
from ._asserts import (
assert_has_pokemon_with_any_move,
assert_player_has_poke_balls,
assert_boxes_or_party_can_fit_pokemon,
)
from ._interface import BotMode, BotModeError
from .util import ensure_facing_direction, navigate_to
from ..battle_strategies import BattleStrategy
Expand Down Expand Up @@ -42,6 +46,7 @@ def on_whiteout(self) -> bool:

def run(self) -> Generator:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()
assert_has_pokemon_with_any_move(
["Selfdestruct", "Explosion"],
error_message="This mode requires a Pokémon with the move Selfdestruct.",
Expand Down
11 changes: 9 additions & 2 deletions modules/modes/roamer_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,13 @@
from modules.runtime import get_sprites_path
from modules.save_data import get_save_data
from modules.tasks import get_global_script_context
from ._asserts import SavedMapLocation, assert_save_game_exists, assert_saved_on_map, assert_player_has_poke_balls
from ._asserts import (
SavedMapLocation,
assert_save_game_exists,
assert_saved_on_map,
assert_player_has_poke_balls,
assert_boxes_or_party_can_fit_pokemon,
)
from ._interface import BattleAction, BotMode, BotModeError
from .util import (
RanOutOfRepels,
Expand Down Expand Up @@ -112,7 +118,8 @@ def run(self) -> Generator:
f"{highest_encounter_level + 1} in order for Repel to work."
)

assert_player_has_poke_balls()
assert_player_has_poke_balls(check_in_saved_game=True)
assert_boxes_or_party_can_fit_pokemon(check_in_saved_game=True)

if save_data.get_item_bag().number_of_repels == 0:
raise BotModeError("You do not have any repels in your item bag. Go and get some first!")
Expand Down
8 changes: 7 additions & 1 deletion modules/modes/rock_smash.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
assert_save_game_exists,
assert_saved_on_map,
assert_player_has_poke_balls,
assert_boxes_or_party_can_fit_pokemon,
assert_item_exists_in_bag,
)
from ._interface import BotMode, BotModeError
Expand Down Expand Up @@ -105,8 +106,9 @@ def on_battle_ended(self, outcome: "BattleOutcome") -> None:
)
context.set_manual_mode()

if not outcome == BattleOutcome.Lost:
if outcome is not BattleOutcome.Lost:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

def on_repel_effect_ended(self) -> None:
if self._using_repel:
Expand All @@ -124,6 +126,7 @@ def run(self) -> Generator:
"You do not have the Dynamo Badge, which is necessary to use Rock Smash outside of battle."
)

assert_boxes_or_party_can_fit_pokemon()
assert_has_pokemon_with_any_move(
["Rock Smash"], "None of your party Pokémon know the move Rock Smash. Please teach it to someone."
)
Expand All @@ -135,6 +138,7 @@ def run(self) -> Generator:
MapRSE.SAFARI_ZONE_SOUTHEAST,
):
assert_save_game_exists("There is no saved game. Cannot soft reset.")
assert_boxes_or_party_can_fit_pokemon(check_in_saved_game=True)
assert_saved_on_map(
SavedMapLocation(MapRSE.ROUTE121_SAFARI_ZONE_ENTRANCE),
"In order to rock smash for Shuckle you should save in the entrance building to the Safari Zone.",
Expand Down Expand Up @@ -166,6 +170,8 @@ def run(self) -> Generator:

if mode == "Use Repel":
assert_save_game_exists("There is no saved game. Cannot soft reset.")
assert_player_has_poke_balls(check_in_saved_game=True)
assert_boxes_or_party_can_fit_pokemon(check_in_saved_game=True)
assert_saved_on_map(
SavedMapLocation(MapRSE.GRANITE_CAVE_B2F),
"In order to use Repel, you need to save on this map.",
Expand Down
5 changes: 5 additions & 0 deletions modules/modes/safari.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
assert_item_exists_in_bag,
assert_save_game_exists,
assert_saved_on_map,
assert_boxes_or_party_can_fit_pokemon,
)
from .util import (
spin,
Expand Down Expand Up @@ -83,6 +84,7 @@ def on_battle_ended(self, outcome: "BattleOutcome") -> None:
if catched_pokemon == self._target_pokemon:
self._target_caught = True
self._atleast_one_pokemon_catched = True
assert_boxes_or_party_can_fit_pokemon()
if get_safari_balls_left() < 30:
current_cash = get_player().money
if (self._starting_cash - current_cash > self._money_spent_limit) or (current_cash < 500):
Expand All @@ -95,6 +97,9 @@ def run(self) -> Generator:

assert_save_game_exists("There is no saved game. Cannot start Safari mode. Please save your game.")

assert_boxes_or_party_can_fit_pokemon()
assert_boxes_or_party_can_fit_pokemon(check_in_saved_game=True)

assert_saved_on_map(
SavedMapLocation(self._safari_config["map"]),
self._safari_config["save_message"],
Expand Down
7 changes: 4 additions & 3 deletions modules/modes/spin.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from typing import Generator

from modules.context import context
from modules.player import get_player_avatar
from modules.battle_state import BattleOutcome
from ._interface import BotMode
from ._asserts import assert_player_has_poke_balls
from ._asserts import assert_player_has_poke_balls, assert_boxes_or_party_can_fit_pokemon
from .util import apply_white_flute_if_available, spin


Expand All @@ -18,10 +17,12 @@ def is_selectable() -> bool:
return get_player_avatar().map_location.has_encounters

def on_battle_ended(self, outcome: "BattleOutcome") -> None:
if not outcome == BattleOutcome.Lost:
if outcome is not BattleOutcome.Lost:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

def run(self) -> Generator:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()
yield from apply_white_flute_if_available()
yield from spin()
4 changes: 2 additions & 2 deletions modules/modes/static_run_away.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from modules.map_data import MapFRLG, MapRSE
from modules.memory import get_event_flag
from modules.player import get_player_avatar
from modules.pokemon import get_opponent
from ._asserts import assert_player_has_poke_balls
from ._asserts import assert_player_has_poke_balls, assert_boxes_or_party_can_fit_pokemon
from ._interface import BattleAction, BotMode, BotModeError
from .util import (
follow_path,
Expand Down Expand Up @@ -185,6 +184,7 @@ def path():
raise BotModeError(f"{pokemon_name} has already been caught.")

assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

while True:
yield from path()
Expand Down
11 changes: 9 additions & 2 deletions modules/modes/static_soft_resets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
from modules.map_data import MapFRLG, MapRSE
from modules.player import get_player_avatar
from modules.save_data import get_save_data
from ._asserts import SavedMapLocation, assert_save_game_exists, assert_saved_on_map, assert_player_has_poke_balls
from ._asserts import (
SavedMapLocation,
assert_save_game_exists,
assert_saved_on_map,
assert_player_has_poke_balls,
assert_boxes_or_party_can_fit_pokemon,
)
from ._interface import BattleAction, BotMode, BotModeError
from .util import (
soft_reset,
Expand Down Expand Up @@ -112,7 +118,8 @@ def run(self) -> Generator:
if encounter.condition is not None and not encounter.condition():
raise BotModeError(f"This {encounter.name} has already been encountered.")

assert_player_has_poke_balls()
assert_player_has_poke_balls(check_in_saved_game=True)
assert_boxes_or_party_can_fit_pokemon(check_in_saved_game=True)

while context.bot_mode != "Manual":
yield from soft_reset(mash_random_keys=True)
Expand Down
2 changes: 2 additions & 0 deletions modules/modes/sudowoodo.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
assert_save_game_exists,
assert_saved_on_map,
assert_player_has_poke_balls,
assert_boxes_or_party_can_fit_pokemon,
)
from ._interface import BattleAction, BotMode
from .util import soft_reset, wait_for_task_to_start_and_finish, wait_for_unique_rng_value, wait_until_task_is_active
Expand Down Expand Up @@ -47,6 +48,7 @@ def run(self) -> Generator:
)

assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon(check_in_saved_game=True)

while context.bot_mode != "Manual":
yield from soft_reset(mash_random_keys=True)
Expand Down
6 changes: 4 additions & 2 deletions modules/modes/sweet_scent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from modules.battle_state import BattleOutcome
from modules.menuing import use_field_move
from modules.player import get_player_avatar
from ._asserts import assert_player_has_poke_balls
from ._asserts import assert_player_has_poke_balls, assert_boxes_or_party_can_fit_pokemon
from ._interface import BotMode


Expand All @@ -17,9 +17,11 @@ def is_selectable() -> bool:
return get_player_avatar().map_location.has_encounters

def on_battle_ended(self, outcome: "BattleOutcome") -> None:
if not outcome == BattleOutcome.Lost:
if outcome is not BattleOutcome.Lost:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()

def run(self) -> Generator:
assert_player_has_poke_balls()
assert_boxes_or_party_can_fit_pokemon()
yield from use_field_move("Sweet Scent")
7 changes: 6 additions & 1 deletion modules/save_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from modules.memory import get_save_block, unpack_uint16, unpack_uint32
from modules.player import Player
from modules.pokemon_party import Party, PartyPokemon
from modules.pokemon_storage import PokemonStorage


def get_last_heal_location() -> tuple[int, int]:
Expand Down Expand Up @@ -57,7 +58,7 @@ def get_map_local_coordinates(self) -> tuple[int, int]:

@cached_property
def _save_block_1(self) -> bytes:
return self.sections[1] + self.sections[2] + self.sections[3] + self.sections[4]
return b"".join([self.sections[1], self.sections[2], self.sections[3], self.sections[4]])

def get_save_block(self, num: int = 1, offset: int = 0, size: int = 1) -> bytes:
if num == 2:
Expand Down Expand Up @@ -97,6 +98,10 @@ def get_party(self) -> Party:
party.append(PartyPokemon(self.sections[1][offset : offset + 100], index))
return Party(party)

def get_pokemon_storage(self) -> PokemonStorage:
data = b"".join(self.sections[5:])
return PokemonStorage(0, data)

def get_item_bag(self) -> ItemBag:
if context.rom.is_frlg:
items_count = 42
Expand Down

0 comments on commit 0815e9c

Please sign in to comment.