From 49e27e6f90dda5796c657966a773b339d09965dc Mon Sep 17 00:00:00 2001 From: Dalton-V Date: Thu, 25 Jun 2026 14:40:42 -0500 Subject: [PATCH] FRLG Optimal Safari Encounter Actions --- ...PokemonFRLG_BattleSelectionArrowDetector.h | 4 +- .../Source/PokemonFRLG/PokemonFRLG_Panels.cpp | 4 +- .../PokemonFRLG_BattleMenuNavigation.cpp | 2 - .../PokemonFRLG_SafariOptimalAction.cpp | 94 +++++++++++ .../PokemonFRLG_SafariOptimalAction.h | 45 ++++++ .../PokemonFRLG_SafariOptimalActionTest.cpp | 147 ++++++++++++++++++ .../PokemonFRLG_SafariOptimalActionTest.h | 40 +++++ SerialPrograms/cmake/SourceFiles.cmake | 4 + 8 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.cpp create mode 100644 SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h create mode 100644 SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.cpp create mode 100644 SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.h diff --git a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h index 4557facee7..a2987c9820 100644 --- a/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h +++ b/SerialPrograms/Source/PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h @@ -37,6 +37,8 @@ enum class BattleConfirmationOption{ NO, }; +const ImageFloatBox BATTLE_MENU_ARROW_BOX(0.525, 0.741, 0.273, 0.212); + class BattleSelectionArrowDetector : public StaticScreenDetector{ public: BattleSelectionArrowDetector( @@ -124,7 +126,7 @@ class BattleSelectionArrowWatcher : public DetectorToFinder PanelListFactory::make_panels() const{ ret.emplace_back(make_single_switch_program()); ret.emplace_back(make_single_switch_program()); ret.emplace_back(make_single_switch_program()); - ret.emplace_back(make_single_switch_program()); + ret.emplace_back(make_single_switch_program()); + ret.emplace_back(make_single_switch_program()); } return ret; diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp index 7e23883d63..fd0398d86a 100644 --- a/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp +++ b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.cpp @@ -21,8 +21,6 @@ namespace PokemonFRLG{ namespace{ -const ImageFloatBox BATTLE_MENU_ARROW_BOX(0.768, 0.868, 0.212, 0.159); - const int BATTLE_MENU_COLS = 2; bool move_cursor_2d_impl( diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.cpp new file mode 100644 index 0000000000..d074b9b49b --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.cpp @@ -0,0 +1,94 @@ +/* FRLG Safari Optimal Action + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Json/JsonValue.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "CommonFramework/Globals.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" +#include "PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +namespace{ + +SafariBattleMenuOption parse_safari_action(const JsonValue& json, const std::string& path){ + int64_t value = json.to_integer_throw(path); + switch (value){ + case 0: + return SafariBattleMenuOption::BALL; + case 1: + return SafariBattleMenuOption::BAIT; + case 2: + return SafariBattleMenuOption::ROCK; + case 3: + return SafariBattleMenuOption::RUN; + default: + throw JsonParseException(path, "Invalid SafariBattleMenuOption value: " + std::to_string(value)); + } +} + +} + +SafariOptimalAction::SafariOptimalAction(Language game_language){ + std::string language_folder = game_language == Language::Japanese ? "Japanese/" : "International/"; + std::string path = RESOURCE_PATH() + "PokemonFRLG/SafariOptimalAction/" + language_folder; + + for (const std::string& pokemon_name : SAFARI_ZONE_POKEMON_SUBSET){ + std::string json_path = path + pokemon_name + ".json"; + JsonValue json = load_json_file(json_path); + JsonArray& root = json.to_array_throw(json_path); + + std::vector>& action_table = m_action_table_by_pokemon[pokemon_name]; + action_table.reserve(root.size()); + + for (const JsonValue& row_value : root){ + const JsonArray& row = row_value.to_array_throw(json_path); + std::vector& actions = action_table.emplace_back(); + actions.reserve(row.size()); + + for (const JsonValue& action_value : row){ + actions.emplace_back(parse_safari_action(action_value, json_path)); + } + } + } +} + +std::optional>> SafariOptimalAction::get_optimal_actions( + ConsoleHandle& console, + const std::string& pokemon_name, + int balls_remaining +) const{ + if (balls_remaining < 1 || balls_remaining > 30){ + console.log("Invalid number of Safari Balls remaining: " + std::to_string(balls_remaining)); + return std::nullopt; + } + + auto iter = m_action_table_by_pokemon.find(pokemon_name); + if (iter == m_action_table_by_pokemon.end()){ + console.log("Unknown Pokemon: " + pokemon_name); + return std::nullopt; + } + + const std::vector>& action_table = iter->second; + if ((size_t)balls_remaining > action_table.size()){ + console.log("Invalid number of Safari Balls remaining: " + std::to_string(balls_remaining)); + return std::nullopt; + } + + const std::vector& actions = action_table[balls_remaining - 1]; + if (actions.empty()){ + console.log("No actions available for Pokemon: " + pokemon_name + " with " + std::to_string(balls_remaining) + " Safari Balls remaining."); + return std::nullopt; + } + + return std::cref(actions); +} + +} +} +} diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h new file mode 100644 index 0000000000..e6012c0b39 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h @@ -0,0 +1,45 @@ +/* FRLG Safari Optimal Action + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_SafariOptimalAction +#define PokemonAutomation_PokemonFRLG_SafariOptimalAction + +#include +#include +#include +#include +#include + +#include "CommonFramework/Language.h" +#include "PokemonFRLG/Inference/PokemonFRLG_BattleSelectionArrowDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ + class ConsoleHandle; +namespace PokemonFRLG{ + +static const std::set SAFARI_ZONE_POKEMON_SUBSET = { + "chansey", "doduo", "dragonair", "dratini", "exeggcute", "goldeen", "kangaskhan", "magikarp", "nidoran", "nidorina", "nidorino", "paras", "parasect", "pinsir", "poliwag", "psyduck", "rhyhorn", "scyther", "seaking", "slowpoke", "tauros", "venomoth", "venonat" +}; + +class SafariOptimalAction +{ +public: + SafariOptimalAction(Language game_language); + + // Returns the optimal action sequence for the given Pokemon and number of Safari Balls remaining. + // Returns std::nullopt if no actions are available. + std::optional>> get_optimal_actions(ConsoleHandle& console, const std::string& pokemon_name, int balls_remaining) const; + +private: + std::map>> m_action_table_by_pokemon; +}; + +} +} +} + +#endif \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.cpp b/SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.cpp new file mode 100644 index 0000000000..5754bd5212 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.cpp @@ -0,0 +1,147 @@ +/* Safari Optimal Action Test + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Pokemon_Strings.h" +#include "Pokemon/Inference/Pokemon_NameReader.h" +#include "PokemonFRLG/Inference/PokemonFRLG_WildEncounterReader.h" +#include "PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h" +#include "PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h" +#include "PokemonFRLG_SafariOptimalActionTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace std::chrono_literals; + +namespace{ + +std::string safari_action_to_string(SafariBattleMenuOption action){ + switch (action){ + case SafariBattleMenuOption::BALL: + return "BALL"; + case SafariBattleMenuOption::BAIT: + return "BAIT"; + case SafariBattleMenuOption::ROCK: + return "ROCK"; + case SafariBattleMenuOption::RUN: + return "RUN"; + default: + return "UNKNOWN"; + } +} + +std::string safari_actions_to_string(const std::vector& actions){ + std::string ret; + for (size_t c = 0; c < actions.size(); c++){ + if (!ret.empty()){ + ret += " -> "; + } + ret += safari_action_to_string(actions[c]); + } + return ret; +} + +} + +SafariOptimalActionTest_Descriptor::SafariOptimalActionTest_Descriptor() + : SingleSwitchProgramDescriptor( + "PokemonFRLG:SafariOptimalActionTest", + Pokemon::STRING_POKEMON + " FRLG", + "Safari Optimal Action Test", "", + "Start in a Safari Zone encounter with the Safari battle menu already visible. " + "The program reads the encounter and attempts to execute the optimal actions until the Safari battle menu is no longer detected.", + ProgramControllerClass::StandardController_NoRestrictions, + FeedbackType::REQUIRED, + AllowCommandsWhenRunning::DISABLE_COMMANDS + ){} + +SafariOptimalActionTest::SafariOptimalActionTest() + : LANGUAGE( + "Game Language:", + Pokemon::PokemonNameReader::instance().languages(), + LockMode::LOCK_WHILE_RUNNING, true + ) + , BALLS_REMAINING( + "Safari Balls Remaining:", + LockMode::LOCK_WHILE_RUNNING, + (uint8_t)30, (uint8_t)1, (uint8_t)30 + ) +{ + PA_ADD_OPTION(LANGUAGE); + PA_ADD_OPTION(BALLS_REMAINING); +} + +void SafariOptimalActionTest::program( + SingleSwitchProgramEnvironment& env, + ProControllerContext& context +){ + WildEncounterReader reader(COLOR_RED); + VideoOverlaySet overlays(env.console.overlay()); + reader.make_overlays(overlays); + + env.log("Reading encounter..."); + VideoSnapshot screen = env.console.video().snapshot(); + PokemonFRLG_WildEncounter encounter = reader.read_encounter(env.logger(), LANGUAGE, screen, SAFARI_ZONE_POKEMON_SUBSET); + env.log("Encounter: " + encounter.name); + + SafariOptimalAction safari_optimal_action(LANGUAGE); + auto actions = safari_optimal_action.get_optimal_actions( + env.console, + encounter.name, + BALLS_REMAINING + ); + + if (!actions.has_value()){ + env.log("No optimal action sequence found for this encounter.", COLOR_RED); + return; + } + + const std::vector& action_list = actions->get(); + + env.log("Optimal actions: " + safari_actions_to_string(action_list)); + + BattleSelectionArrowWatcher battle_arrow(COLOR_RED, &env.console.overlay()); + + for (size_t c = 0; c < action_list.size(); c++){ + int ret = wait_until( + env.console, context, + std::chrono::seconds(10), + { battle_arrow } + ); + + if (ret != 0){ + env.log("Safari battle menu not detected. Stopping.", COLOR_RED); + return; + } + + SafariBattleMenuOption action = action_list[c]; + env.log( + "Executing action " + std::to_string(c + 1) + "/" + std::to_string(action_list.size()) + ": " + safari_action_to_string(action) + ); + + if (!move_cursor_to_option(env.console, context, action)){ + env.log("Failed to move cursor to option.", COLOR_RED); + return; + } + + pbf_press_button(context, BUTTON_A, 200ms, 1000ms); + context.wait_for_all_requests(); + } + + env.log("Finished executing Safari optimal actions.", COLOR_BLUE); +} + +} +} +} diff --git a/SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.h b/SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.h new file mode 100644 index 0000000000..f8d2eff349 --- /dev/null +++ b/SerialPrograms/Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.h @@ -0,0 +1,40 @@ +/* Safari Optimal Action Test + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_SafariOptimalActionTest_H +#define PokemonAutomation_PokemonFRLG_SafariOptimalActionTest_H + +#include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "CommonTools/Options/LanguageOCROption.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +class SafariOptimalActionTest_Descriptor : public SingleSwitchProgramDescriptor{ +public: + SafariOptimalActionTest_Descriptor(); +}; + +class SafariOptimalActionTest : public SingleSwitchProgramInstance{ +public: + SafariOptimalActionTest(); + + virtual void start_program_border_check(VideoStream& stream, FeedbackType feedback_type) override{} + virtual void program(SingleSwitchProgramEnvironment& env, ProControllerContext& context) override; + +private: + OCR::LanguageOCROption LANGUAGE; + SimpleIntegerOption BALLS_REMAINING; +}; + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 95e9cc92d5..f1b37efede 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1531,6 +1531,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Programs/PokemonFRLG_BattleMenuNavigation.h Source/PokemonFRLG/Programs/Farming/PokemonFRLG_EvTrainer.cpp Source/PokemonFRLG/Programs/Farming/PokemonFRLG_EvTrainer.h + Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.cpp + Source/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.cpp Source/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h Source/PokemonFRLG/Programs/ShinyHunting/PokemonFRLG_GiftReset.cpp @@ -1579,6 +1581,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RoamingLegendaryRng.h Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_EggRng.cpp Source/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_EggRng.h + Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.cpp + Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SafariOptimalActionTest.h Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SoundListener.cpp Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_SoundListener.h Source/PokemonFRLG/Programs/TestPrograms/PokemonFRLG_ReadStats.cpp