Skip to content

Commit

Permalink
EV Training (#591)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
hosch-m and hanzi authored Jan 4, 2025
1 parent a347f18 commit b9bc3ba
Show file tree
Hide file tree
Showing 6 changed files with 410 additions and 2 deletions.
73 changes: 73 additions & 0 deletions modules/gui/ev_selection_window.py
Original file line number Diff line number Diff line change
@@ -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("<Escape>", 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
6 changes: 4 additions & 2 deletions modules/modes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,19 +28,20 @@ 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
from .static_run_away import StaticRunAway
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,
Expand All @@ -49,14 +51,14 @@ def get_bot_modes() -> list[Type[BotMode]]:
PuzzleSolverMode,
RoamerResetMode,
RockSmashMode,
SafariMode,
SpinMode,
StartersMode,
StaticRunAway,
StaticGiftResetsMode,
StaticSoftResetsMode,
SweetScentMode,
SudowoodoMode,
SafariMode,
]

for mode in plugin_get_additional_bot_modes():
Expand Down
252 changes: 252 additions & 0 deletions modules/modes/ev_train.py
Original file line number Diff line number Diff line change
@@ -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)
1 change: 1 addition & 0 deletions wiki/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Binary file added wiki/images/EV_Train.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b9bc3ba

Please sign in to comment.