From b9bc3baa8ebd496b8015f4de9eb5c8f8864c329e Mon Sep 17 00:00:00 2001 From: hosch-m <109269160+hosch-m@users.noreply.github.com> Date: Sun, 5 Jan 2025 04:53:20 +1100 Subject: [PATCH] EV Training (#591) * Add files via upload * Add files via upload * Add Wiki entry for EV train mode Add Wiki entry for EV train mode * Update WIKI Update WIKI * Updated to enable surfing --------- Co-authored-by: Tino --- modules/gui/ev_selection_window.py | 73 +++++++++ modules/modes/__init__.py | 6 +- modules/modes/ev_train.py | 252 +++++++++++++++++++++++++++++ wiki/Readme.md | 1 + wiki/images/EV_Train.png | Bin 0 -> 4610 bytes wiki/pages/Mode - EV Train.md | 80 +++++++++ 6 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 modules/gui/ev_selection_window.py create mode 100644 modules/modes/ev_train.py create mode 100644 wiki/images/EV_Train.png create mode 100644 wiki/pages/Mode - EV Train.md diff --git a/modules/gui/ev_selection_window.py b/modules/gui/ev_selection_window.py new file mode 100644 index 000000000..77d9dbdca --- /dev/null +++ b/modules/gui/ev_selection_window.py @@ -0,0 +1,73 @@ +import time +from tkinter import * + +from rich.prompt import IntPrompt + +from modules.context import context +from modules.pokemon import Pokemon +from modules.pokemon import StatsValues +from modules.pokemon_party import get_party + + +def ask_for_ev_targets(pokemon: "Pokemon") -> StatsValues: + if context.gui.is_headless: + return StatsValues( + hp=IntPrompt.ask("Choose target HP EVs", default=pokemon.evs.hp), + attack=IntPrompt.ask("Choose target Attack EVs", default=pokemon.evs.attack), + defence=IntPrompt.ask("Choose target Defence EVs", default=pokemon.evs.defence), + speed=IntPrompt.ask("Choose target Speed EVs", default=pokemon.evs.speed), + special_attack=IntPrompt.ask("Choose target Special Attack EVs", default=pokemon.evs.special_attack), + special_defence=IntPrompt.ask("Choose target Special Defence EVs", default=pokemon.evs.special_defence), + ) + + spinboxes: list[Spinbox] = [] + selected_ev_targets: StatsValues | None = None + + def remove_window(event=None): + nonlocal window + window.destroy() + window = None + + def return_selection(): + nonlocal spinboxes, selected_ev_targets + selected_ev_targets = StatsValues( + hp=int(spinboxes[0].get()), + attack=int(spinboxes[1].get()), + defence=int(spinboxes[2].get()), + speed=int(spinboxes[5].get()), + special_attack=int(spinboxes[3].get()), + special_defence=int(spinboxes[4].get()), + ) + window.after(50, remove_window) + + window = Toplevel(context.gui.window) + window.title("EV goals") + window.protocol("WM_DELETE_WINDOW", remove_window) + window.bind("", remove_window) + + Label(window, text=get_party()[0].name).grid(row=1, column=0) + + Label(window, text="HP").grid(row=0, column=1) + Label(window, text="Atk").grid(row=0, column=2) + Label(window, text="Def").grid(row=0, column=3) + Label(window, text="SpA").grid(row=0, column=4) + Label(window, text="SpD").grid(row=0, column=5) + Label(window, text="Spe").grid(row=0, column=6) + + for stat in ("hp", "attack", "defence", "special_attack", "special_defence", "speed"): + spinbox = Spinbox(window, from_=0, to=252, increment=4, wrap=True, width=8) + spinbox.delete(0, last=None) + spinbox.insert(0, str(pokemon.evs[stat])) + spinbox.grid(row=1, column=len(spinboxes) + 1, padx=10, pady=3) + spinboxes.append(spinbox) + + Button(window, text="EV Train", width=20, height=1, bg="lightblue", command=return_selection).grid( + row=7, column=3, columnspan=2, pady=15 + ) + + while window is not None: + window.update_idletasks() + window.update() + time.sleep(1 / 60) + + return selected_ev_targets diff --git a/modules/modes/__init__.py b/modules/modes/__init__.py index f4119ff32..8426679c5 100644 --- a/modules/modes/__init__.py +++ b/modules/modes/__init__.py @@ -18,6 +18,7 @@ def get_bot_modes() -> list[Type[BotMode]]: from .berry_blend import BerryBlendMode from .bunny_hop import BunnyHopMode from .daycare import DaycareMode + from .ev_train import EVTrainMode from .feebas import FeebasMode from .fishing import FishingMode from .game_corner import GameCornerMode @@ -27,6 +28,7 @@ def get_bot_modes() -> list[Type[BotMode]]: from .puzzle_solver import PuzzleSolverMode from .roamer_reset import RoamerResetMode from .rock_smash import RockSmashMode + from .safari import SafariMode from .spin import SpinMode from .starters import StartersMode from .sudowoodo import SudowoodoMode @@ -34,12 +36,12 @@ def get_bot_modes() -> list[Type[BotMode]]: from .static_gift_resets import StaticGiftResetsMode from .static_soft_resets import StaticSoftResetsMode from .sweet_scent import SweetScentMode - from .safari import SafariMode _bot_modes = [ BerryBlendMode, BunnyHopMode, DaycareMode, + EVTrainMode, FeebasMode, FishingMode, GameCornerMode, @@ -49,6 +51,7 @@ def get_bot_modes() -> list[Type[BotMode]]: PuzzleSolverMode, RoamerResetMode, RockSmashMode, + SafariMode, SpinMode, StartersMode, StaticRunAway, @@ -56,7 +59,6 @@ def get_bot_modes() -> list[Type[BotMode]]: StaticSoftResetsMode, SweetScentMode, SudowoodoMode, - SafariMode, ] for mode in plugin_get_additional_bot_modes(): diff --git a/modules/modes/ev_train.py b/modules/modes/ev_train.py new file mode 100644 index 000000000..8e144fdab --- /dev/null +++ b/modules/modes/ev_train.py @@ -0,0 +1,252 @@ +from typing import Generator + +from rich.table import Table + +from modules.context import context +from modules.map import get_map_data_for_current_position, get_map_data +from modules.map_data import MapFRLG, MapRSE, PokemonCenter, get_map_enum +from modules.map_path import calculate_path, PathFindingError +from modules.modes import BattleAction +from modules.player import get_player_avatar +from modules.pokemon import get_opponent, StatusCondition, StatsValues +from modules.pokemon_party import get_party +from ._interface import BotMode, BotModeError +from .util import navigate_to, heal_in_pokemon_center, spin +from ..battle_state import BattleOutcome +from ..battle_strategies import BattleStrategy, DefaultBattleStrategy +from ..console import console +from ..encounter import handle_encounter, EncounterInfo +from ..gui.ev_selection_window import ask_for_ev_targets + +closest_pokemon_centers: dict[MapFRLG | MapRSE, list[PokemonCenter]] = { + # Hoenn + MapRSE.ROUTE101: [PokemonCenter.OldaleTown], + MapRSE.ROUTE102: [PokemonCenter.OldaleTown, PokemonCenter.PetalburgCity], + MapRSE.ROUTE103: [PokemonCenter.OldaleTown], + MapRSE.ROUTE104: [PokemonCenter.PetalburgCity, PokemonCenter.RustboroCity], + MapRSE.ROUTE105: [PokemonCenter.PetalburgCity, PokemonCenter.DewfordTown], + MapRSE.ROUTE106: [PokemonCenter.DewfordTown], + MapRSE.ROUTE107: [PokemonCenter.DewfordTown], + MapRSE.ROUTE108: [PokemonCenter.DewfordTown], + MapRSE.ROUTE109: [PokemonCenter.SlateportCity], + MapRSE.ROUTE110: [PokemonCenter.SlateportCity, PokemonCenter.MauvilleCity], + MapRSE.ROUTE111: [PokemonCenter.MauvilleCity, PokemonCenter.MauvilleCity, PokemonCenter.FallarborTown], + MapRSE.ROUTE112: [PokemonCenter.LavaridgeTown, PokemonCenter.MauvilleCity, PokemonCenter.FallarborTown], + MapRSE.ROUTE113: [PokemonCenter.FallarborTown], + MapRSE.ROUTE114: [PokemonCenter.FallarborTown], + MapRSE.ROUTE115: [PokemonCenter.RustboroCity], + MapRSE.ROUTE116: [PokemonCenter.RustboroCity], + MapRSE.ROUTE117: [PokemonCenter.MauvilleCity, PokemonCenter.VerdanturfTown], + MapRSE.ROUTE118: [PokemonCenter.MauvilleCity], + MapRSE.ROUTE119: [PokemonCenter.FortreeCity, PokemonCenter.MauvilleCity], + MapRSE.ROUTE120: [PokemonCenter.FortreeCity], + MapRSE.ROUTE121: [PokemonCenter.LilycoveCity], + MapRSE.ROUTE122: [PokemonCenter.LilycoveCity], + MapRSE.ROUTE123: [PokemonCenter.LilycoveCity, PokemonCenter.MauvilleCity], + MapRSE.ROUTE124: [PokemonCenter.LilycoveCity, PokemonCenter.MossdeepCity], + MapRSE.ROUTE125: [PokemonCenter.MossdeepCity], + MapRSE.ROUTE126: [PokemonCenter.MossdeepCity], + MapRSE.ROUTE127: [PokemonCenter.MossdeepCity], + MapRSE.ROUTE128: [PokemonCenter.EvergrandeCity], + MapRSE.ROUTE129: [PokemonCenter.EvergrandeCity], + MapRSE.ROUTE130: [PokemonCenter.PacifidlogTown], + MapRSE.ROUTE131: [PokemonCenter.PacifidlogTown], + MapRSE.ROUTE132: [PokemonCenter.PacifidlogTown], + MapRSE.ROUTE133: [PokemonCenter.PacifidlogTown, PokemonCenter.SlateportCity], + MapRSE.ROUTE134: [PokemonCenter.SlateportCity], + MapRSE.PETALBURG_CITY: [PokemonCenter.PetalburgCity], + MapRSE.SLATEPORT_CITY: [PokemonCenter.SlateportCity], + MapRSE.MAUVILLE_CITY: [PokemonCenter.MauvilleCity], + MapRSE.RUSTBORO_CITY: [PokemonCenter.RustboroCity], + MapRSE.FORTREE_CITY: [PokemonCenter.FortreeCity], + MapRSE.LILYCOVE_CITY: [PokemonCenter.LilycoveCity], + MapRSE.MOSSDEEP_CITY: [PokemonCenter.MossdeepCity], + MapRSE.EVER_GRANDE_CITY: [PokemonCenter.EvergrandeCity], + MapRSE.OLDALE_TOWN: [PokemonCenter.OldaleTown], + MapRSE.DEWFORD_TOWN: [PokemonCenter.DewfordTown], + MapRSE.LAVARIDGE_TOWN: [PokemonCenter.LavaridgeTown], + MapRSE.FALLARBOR_TOWN: [PokemonCenter.FallarborTown], + MapRSE.VERDANTURF_TOWN: [PokemonCenter.VerdanturfTown], + MapRSE.PACIFIDLOG_TOWN: [PokemonCenter.PacifidlogTown], + # Kanto + MapFRLG.ROUTE1: [PokemonCenter.ViridianCity], + MapFRLG.ROUTE2: [PokemonCenter.ViridianCity, PokemonCenter.PewterCity], + MapFRLG.ROUTE3: [PokemonCenter.PewterCity, PokemonCenter.Route4], + MapFRLG.ROUTE4: [PokemonCenter.Route4, PokemonCenter.CeruleanCity], + MapFRLG.ROUTE6: [PokemonCenter.VermilionCity], + MapFRLG.ROUTE7: [PokemonCenter.CeladonCity], + MapFRLG.ROUTE9: [PokemonCenter.Route10], + MapFRLG.ROUTE10: [PokemonCenter.Route10], + MapFRLG.ROUTE11: [PokemonCenter.VermilionCity], + MapFRLG.ROUTE18: [PokemonCenter.FuchsiaCity], + MapFRLG.ROUTE19: [PokemonCenter.FuchsiaCity], + MapFRLG.ROUTE20: [PokemonCenter.CinnabarIsland, PokemonCenter.FuchsiaCity], + MapFRLG.ROUTE21_NORTH: [PokemonCenter.CinnabarIsland], + MapFRLG.ROUTE21_SOUTH: [PokemonCenter.CinnabarIsland], + MapFRLG.ROUTE22: [PokemonCenter.ViridianCity], + MapFRLG.ROUTE24: [PokemonCenter.CeruleanCity], + MapFRLG.VIRIDIAN_CITY: [PokemonCenter.ViridianCity], + MapFRLG.PEWTER_CITY: [PokemonCenter.PewterCity], + MapFRLG.CERULEAN_CITY: [PokemonCenter.CeruleanCity], + MapFRLG.LAVENDER_TOWN: [PokemonCenter.LavenderTown], + MapFRLG.VERMILION_CITY: [PokemonCenter.VermilionCity], + MapFRLG.CELADON_CITY: [PokemonCenter.CeladonCity], + MapFRLG.FUCHSIA_CITY: [PokemonCenter.FuchsiaCity], + MapFRLG.CINNABAR_ISLAND: [PokemonCenter.CinnabarIsland], + MapFRLG.SAFFRON_CITY: [PokemonCenter.SaffronCity], +} + +_list_of_stats = ("hp", "attack", "defence", "special_attack", "special_defence", "speed") + + +class NoRotateLeadDefaultBattleStrategy(DefaultBattleStrategy): + def choose_new_lead_after_battle(self) -> int | None: + return None + + +class EVTrainMode(BotMode): + @staticmethod + def name() -> str: + return "EV Train" + + @staticmethod + def is_selectable() -> bool: + current_location = get_map_data_for_current_position() + if current_location is None: + return False + + return current_location.has_encounters and current_location.map_group_and_number in closest_pokemon_centers + + def __init__(self): + super().__init__() + self._leave_pokemon_center = False + self._go_healing = True + self._level_balance = False + self._ev_targets: StatsValues | None = None + + def on_battle_started(self, encounter: EncounterInfo | None) -> BattleAction | BattleStrategy | None: + action = handle_encounter(encounter, enable_auto_battle=True) + lead_pokemon = get_party()[0] + # EV yield doubled with Macho Brace and Pokerus (this effect stacks) + ev_multiplier = 1 + if lead_pokemon.held_item is not None and lead_pokemon.held_item.name == "Macho Brace": + ev_multiplier *= 2 + if lead_pokemon.pokerus_status.days_remaining > 0: + ev_multiplier *= 2 + + # Checks if opponent evs are desired + good_yield = all( + get_opponent().species.ev_yield[stat] * ev_multiplier + lead_pokemon.evs[stat] <= self._ev_targets[stat] + for stat in _list_of_stats + ) + # Fights if evs are desired and oppenent is not shiny meets a custom catch filter + if good_yield and action is BattleAction.Fight: + return NoRotateLeadDefaultBattleStrategy() + elif action is BattleAction.Fight: + return BattleAction.RunAway + else: + return action + + def on_battle_ended(self, outcome: "BattleOutcome") -> None: + lead_pokemon = get_party()[0] + if ( + not DefaultBattleStrategy().pokemon_can_battle(lead_pokemon) + or lead_pokemon.status_condition is not StatusCondition.Healthy + ): + self._go_healing = True + + ev_multiplier = 1 + if lead_pokemon.held_item is not None and lead_pokemon.held_item.name == "Macho Brace": + ev_multiplier *= 2 + if lead_pokemon.pokerus_status.days_remaining > 0: + ev_multiplier *= 2 + + # Ugly table to keep track of progress + ev_table = Table(title=f"{lead_pokemon.species.name} EVs/Target") + ev_table.add_column("HP", justify="center") + ev_table.add_column("ATK", justify="center") + ev_table.add_column("DEF", justify="center") + ev_table.add_column("SPA", justify="center") + ev_table.add_column("SPD", justify="center") + ev_table.add_column("SPE", justify="center") + ev_table.add_column("Total", justify="right") + ev_table.add_row( + *[f"{str(lead_pokemon.evs[stat])}/{str(self._ev_targets[stat])}" for stat in _list_of_stats], + str(lead_pokemon.evs.sum()), + ) + console.print(ev_table) + + if outcome == BattleOutcome.RanAway: + context.message = "EVs not needed, skipping" + if outcome == BattleOutcome.Won: + context.message = ( + f"{'/'.join([str(get_opponent().species.ev_yield[stat]) for stat in _list_of_stats])} EVs gained" + ) + + def on_whiteout(self) -> bool: + self._leave_pokemon_center = True + return True + + def run(self) -> Generator: + training_spot = get_map_data_for_current_position() + if not training_spot.has_encounters: + raise BotModeError("There are no encounters on this tile.") + + training_spot_map = get_map_enum(training_spot) + training_spot_coordinates = training_spot.local_position + + # Find the closest Pokemon Center to the current location + pokemon_center = None + path_length_to_pokemon_center = None + if training_spot_map in closest_pokemon_centers: + for pokemon_center_candidate in closest_pokemon_centers[training_spot_map]: + try: + pokemon_center_location = get_map_data( + pokemon_center_candidate.value[0], pokemon_center_candidate.value[1] + ) + path_to = calculate_path(training_spot, pokemon_center_location) + path_from = [] + path_length = len(path_to) + len(path_from) + + if path_length_to_pokemon_center is None or path_length_to_pokemon_center > path_length: + pokemon_center = pokemon_center_candidate + path_length_to_pokemon_center = path_length + except PathFindingError: + pass + + if pokemon_center is None: + raise BotModeError("Could not find a suitable from here to a Pokemon Center nearby.") + + # Opens EV target selection GUI + target_pokemon = get_party()[0] + self._ev_targets = ask_for_ev_targets(target_pokemon) + if self._ev_targets is None: + # If the user just closed the window without answering + context.set_manual_mode() + return + + # Checks for EV target sensibility + if self._ev_targets.sum() > 510: + raise BotModeError("Total EVs must be 510 or below.") + + for stat in _list_of_stats: + if self._ev_targets[stat] < 0 or self._ev_targets[stat] > 255: + raise BotModeError( + f"Selected EV target for {stat} ('{self._ev_targets[stat]}') is invalid (must be between 0 and 255.)" + ) + if target_pokemon.evs[stat] > self._ev_targets[stat]: + raise BotModeError( + f"Selected EV target for {stat} ('{self._ev_targets[stat]}') must be equal to or larger than the current EV number ({target_pokemon.evs[stat]}.)" + ) + + 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) diff --git a/wiki/Readme.md b/wiki/Readme.md index 67efac184..9e07020df 100644 --- a/wiki/Readme.md +++ b/wiki/Readme.md @@ -28,6 +28,7 @@ For quick help and support, reach out in Discord [#pokebot-gen3-support❔](http - 🎰 [Game Corner](pages/Mode%20-%20Game%20Corner.md) - 🎨 [Kecleon](pages/Mode%20-%20Kecleon.md) - πŸ”„οΈ [Level Grind](pages/Mode%20-%20Level%20Grind.md) +- πŸ’Š [EV Train](pages/Mode%20-%20EV%20Train.md) - 🟑 [Nugget Bridge](pages/Mode%20-%20Nugget%20Bridge.md) - 🧩 [Puzzle Solver](pages/Mode%20-%20Puzzle%20Solver.md) - πŸƒ [Roamer Resets](pages/Mode%20-%20Roamer%20Resets.md) diff --git a/wiki/images/EV_Train.png b/wiki/images/EV_Train.png new file mode 100644 index 0000000000000000000000000000000000000000..78ee274d991659cdf42d405e4a21838dc079c4ca GIT binary patch literal 4610 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 a+Fq>2_xed!<58w9K