From c3c16c89a34d2b519dfd37306c4e4b591b92157a Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 12:08:46 -0800 Subject: [PATCH 01/39] BDSP navigation: the walker, with battles passed in Walks the planned routes on the BDSP story maps: Walk holds a direction and stops on what the screen shows (a dialogue, a transition, a wall), RouteWalk walks a plan's legs, tracks the tiles the player may be on and plans again after a trainer, and leaves buildings by indoors() and the door's fade. The walker sees wild battles and trainers start but does not fight them: a WildBattleEscape runs from a wild battle and a RouteTrainerFight fights a trainer, both passed in by the program. Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters, kept nearly as written: the AutoStory OverworldDetector and TransitionWatchers, which the walker uses to see the overworld and an encounter's white flash. Tests: indoors() on synthetic screens and screenshots, the door's fade, and the location banner reads. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_TransitionWatchers.cpp | 36 + .../PokemonBDSP_TransitionWatchers.h | 56 ++ .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + ...okemonBDSP_AutoStory_OverworldDetector.cpp | 135 +++ .../PokemonBDSP_AutoStory_OverworldDetector.h | 98 ++ .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 225 +++++ .../PokemonBDSP_AutoStory_Walk_Tests.h | 27 + .../Utils/PokemonBDSP_AutoStory_Indoors.cpp | 60 ++ .../Utils/PokemonBDSP_AutoStory_Indoors.h | 33 + .../Utils/PokemonBDSP_AutoStory_Location.cpp | 135 +++ .../Utils/PokemonBDSP_AutoStory_Location.h | 65 ++ .../PokemonBDSP_AutoStory_OverlayLog.cpp | 42 + .../Utils/PokemonBDSP_AutoStory_OverlayLog.h | 27 + .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 926 ++++++++++++++++++ .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 203 ++++ .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 469 +++++++++ .../Utils/PokemonBDSP_AutoStory_Walk.h | 172 ++++ SerialPrograms/cmake/SourceFiles.cmake | 16 + 18 files changed, 2727 insertions(+) create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp new file mode 100644 index 0000000000..3655b5244d --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp @@ -0,0 +1,36 @@ +#include "PokemonBDSP_TransitionWatchers.h" +namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ +WhiteScreenOverWatcher::WhiteScreenOverWatcher( + Color color, const ImageFloatBox& box, + double min_rgb_sum, + double max_stddev_sum, + std::chrono::milliseconds hold_duration, + std::chrono::milliseconds release_duration +) + : VisualInferenceCallback("WhiteScreenOverWatcher") + , m_on(color, box, min_rgb_sum, max_stddev_sum, WhiteScreenWatcher::FinderType::PRESENT, hold_duration) + , m_off(color, box, min_rgb_sum, max_stddev_sum, WhiteScreenWatcher::FinderType::GONE, release_duration) +{} +void WhiteScreenOverWatcher::make_overlays(VideoOverlaySet& items) const{ + m_on.make_overlays(items); +} +bool WhiteScreenOverWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp){ + if (m_white_is_over.load(std::memory_order_acquire)){ + return true; + } + if (!m_has_been_white){ + m_has_been_white = m_on.process_frame(frame, timestamp); + return false; + } + bool is_over = m_off.process_frame(frame, timestamp); + if (!is_over){ + return false; + } + m_white_is_over.store(true, std::memory_order_release); + return true; +} +bool WhiteScreenOverWatcher::white_is_over(const ImageViewRGB32& frame){ + return m_white_is_over.load(std::memory_order_acquire); +} + +}}} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h new file mode 100644 index 0000000000..ce5a34d2cb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h @@ -0,0 +1,56 @@ +#pragma once +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ +// AutoStory-specific transition handling; shared watchers retain upstream behavior. +class WhiteScreenWatcher : public DetectorToFinder{ +public: + WhiteScreenWatcher( + Color color = COLOR_RED, + const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, + double min_rgb_sum = 500, + double max_stddev_sum = 10, + FinderType finder_type = FinderType::PRESENT, + std::chrono::milliseconds duration = std::chrono::milliseconds(100) + ) + : DetectorToFinder("WhiteScreenWatcher", finder_type, duration, color, box, min_rgb_sum, max_stddev_sum) + {} +}; + +class BlackScreenFlashWatcher : public BlackScreenOverWatcher{ +public: + BlackScreenFlashWatcher( + Color color = COLOR_RED, + const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, + double max_rgb_sum = 100, + double max_stddev_sum = 10, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(20), + std::chrono::milliseconds release_duration = std::chrono::milliseconds(100) + ) + : BlackScreenOverWatcher(color, box, max_rgb_sum, max_stddev_sum, hold_duration, release_duration) + {} +}; + +class WhiteScreenOverWatcher : public VisualInferenceCallback{ +public: + WhiteScreenOverWatcher( + Color color = COLOR_RED, + const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, + double min_rgb_sum = 500, + double max_stddev_sum = 10, + std::chrono::milliseconds hold_duration = std::chrono::milliseconds(100), + std::chrono::milliseconds release_duration = std::chrono::milliseconds(100) + ); + + bool white_is_over(const ImageViewRGB32& frame); + + virtual void make_overlays(VideoOverlaySet& items) const override; + + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; + +private: + WhiteScreenWatcher m_on; + WhiteScreenWatcher m_off; + bool m_has_been_white = false; + std::atomic m_white_is_over = false; +}; +}}} diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 608126f0db..56338bad77 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -7,6 +7,7 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h" #include "PokemonBDSP_Tests.h" namespace PokemonAutomation{ @@ -18,6 +19,7 @@ namespace PokemonBDSP{ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_MapGrids(database); add_tests_AutoStory_Segment00(database); + add_tests_AutoStory_Walk(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp new file mode 100644 index 0000000000..3e4e0f41ca --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp @@ -0,0 +1,135 @@ +/* BDSP AutoStory - Overworld Detector + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/Images/SolidColorTest.h" + +#include "PokemonBDSP_AutoStory_OverworldDetector.h" + +using namespace std::chrono_literals; + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// --------------------------------------------------------------------------- +// OverworldDetector +// --------------------------------------------------------------------------- + +OverworldDetector::OverworldDetector(Color color) + : m_color(color) + , m_battle_menu(BattleType::STANDARD, color){ +} + +void OverworldDetector::make_overlays(VideoOverlaySet& items) const { + m_dialog.make_overlays(items); + m_battle_menu.make_overlays(items); + m_menu.make_overlays(items); +} + +bool OverworldDetector::detect(const ImageViewRGB32& screen){ + // We are in the overworld when none of the overlay screens are present. + if (m_dialog.detect(screen)){ + return false; + } + if (m_battle_menu.detect(screen)){ + return false; + } + if (m_menu.detect(screen)){ + return false; + } + return true; +} + + +// --------------------------------------------------------------------------- +// OverworldWatcher +// --------------------------------------------------------------------------- + +OverworldWatcher::OverworldWatcher(Color color) + : OverworldDetector(color) + , VisualInferenceCallback("OverworldWatcher"){ +} + +void OverworldWatcher::make_overlays(VideoOverlaySet& items) const { + OverworldDetector::make_overlays(items); +} + +bool OverworldWatcher::process_frame(const ImageViewRGB32& frame, WallClock /*timestamp*/){ + return detect(frame); +} + + +// --------------------------------------------------------------------------- +// PoketchButtonDetector +// --------------------------------------------------------------------------- + +PoketchButtonDetector::PoketchButtonDetector(Color color) + : m_color(color) + , m_button(0.968, 0.170, 0.022, 0.120){ +} + +void PoketchButtonDetector::make_overlays(VideoOverlaySet& items) const { + items.add(m_color, m_button); +} + +bool PoketchButtonDetector::detect(const ImageViewRGB32& screen){ + ImageStats stats = image_stats(extract_box_reference(screen, m_button)); + // The button is a glossy red with a highlight, so allow a wide stddev. + return is_solid(stats, {0.74, 0.10, 0.16}, 0.12, 80); +} + +PoketchButtonWatcher::PoketchButtonWatcher(Color color) + : PoketchButtonDetector(color) + , VisualInferenceCallback("PoketchButtonWatcher"){ +} + +void PoketchButtonWatcher::make_overlays(VideoOverlaySet& items) const { + PoketchButtonDetector::make_overlays(items); +} + +bool PoketchButtonWatcher::process_frame(const ImageViewRGB32& frame, WallClock){ + return detect(frame); +} + + +static bool look_for_poketch_button(VideoStream& stream, ProControllerContext& context){ + context.wait_for_all_requests(); + PoketchButtonWatcher watcher; + return wait_until(stream, context, 2s, {{watcher}}) == 0; +} + +bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context){ + for (size_t attempt = 0; attempt < 3; attempt++){ + if (look_for_poketch_button(stream, context)){ + return true; + } + stream.log("[AutoStory] Poketch button not found. Attempt " + std::to_string(attempt + 1) + " of 3.", + COLOR_ORANGE); + + if (attempt == 0){ + // Most likely hidden: one tap shows it. + pbf_press_button(context, BUTTON_R, 205ms, 0ms); + }else{ + // Long press hides it, then a tap shows it at the normal size. + pbf_press_button(context, BUTTON_R, 1548ms, 0ms); + pbf_wait(context, 1418ms); + pbf_press_button(context, BUTTON_R, 143ms, 0ms); + } + } + + // Check the result of the last press sequence. + return look_for_poketch_button(stream, context); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h new file mode 100644 index 0000000000..4741325711 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h @@ -0,0 +1,98 @@ +/* BDSP AutoStory - Overworld Detector + * + * From: https://github.com/PokemonAutomation/ + * + * Detects that the player is in the free-roaming overworld state: + * no dialog box, no battle menu, and no in-game menu open. + * + * BDSP does not ship a standalone OverworldDetector in PokemonBDSP/Inference/, + * so this composite detector is defined here for AutoStory use. + * + * Reuses: + * - ShortDialogDetector (PokemonBDSP_DialogDetector.h) — absence required + * - BattleMenuDetector (PokemonBDSP_BattleMenuDetector.h) — absence required + * - MenuDetector (PokemonBDSP_MenuDetector.h) — absence required + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H +#define PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H + +#include "Common/Cpp/Color.h" + +#include "CommonFramework/Tools/VideoStream.h" + +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "CommonTools/VisualDetector.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" + +namespace PokemonAutomation { +namespace NintendoSwitch { +namespace PokemonBDSP { + + +// Static (single-frame) version — use when you already have a frame. +class OverworldDetector : public StaticScreenDetector { +public: + OverworldDetector(Color color = COLOR_CYAN); + + virtual void make_overlays(VideoOverlaySet& items) const override; + + // Returns true when none of: short dialog, battle menu, or in-game menu + // are present on the given frame. + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ShortDialogDetector m_dialog; + BattleMenuDetector m_battle_menu; + MenuDetector m_menu; +}; + + +// Watcher (callback) version — use with run_until<> or wait_until(). +class OverworldWatcher : public OverworldDetector, public VisualInferenceCallback { +public: + OverworldWatcher(Color color = COLOR_CYAN); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; +}; + + +// Detects the big red button on the right edge of the Poketch overlay. +// Present only while the Poketch is shown at its normal size. +class PoketchButtonDetector : public StaticScreenDetector { +public: + PoketchButtonDetector(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ImageFloatBox m_button; +}; + +class PoketchButtonWatcher : public PoketchButtonDetector, public VisualInferenceCallback { +public: + PoketchButtonWatcher(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; +}; + +// Tries up to 3 times to get the Poketch button on screen using R presses. +// Returns true if the button was seen. +bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp new file mode 100644 index 0000000000..cb6ab3bb54 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -0,0 +1,225 @@ +/* BDSP AutoStory Walk Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Walk_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// indoors() decides when leave_door_south() is out: r257 walked on south +// past Sandgem's road into a house after leaving the Center, because the +// evening tint made the outside as warm as a room. Synthetic screens: the +// evening outside (warm all over), a room (warm, black along the bottom), +// a fade (black all over), and warm with a thin dark edge (a shadow). +struct IndoorsCase{ + std::string name; + uint32_t fill; + double dark_bottom; // how much of the screen's height, from the bottom, is black + bool indoors; +}; + +class Test_IndoorsAnyLight : public UnitTest{ +public: + Test_IndoorsAnyLight() + : UnitTest("PokemonBDSP::AutoStory - indoors() under the evening tint") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "evening outside, orange all over", .fill = 0xffe0a040, .dark_bottom = 0, .indoors = false}, + {.name = "a room: orange, black below its floor", .fill = 0xffe0a040, .dark_bottom = 0.10, .indoors = true}, + {.name = "a fade: black all over", .fill = 0xff000000, .dark_bottom = 0, .indoors = false}, + {.name = "orange with a thin shadow at the bottom", .fill = 0xffe0a040, .dark_bottom = 0.01, .indoors = false}, + {.name = "daytime grass", .fill = 0xff60b040, .dark_bottom = 0.10, .indoors = false}, + }; + for (const IndoorsCase& item : cases){ + ImageRGB32 screen(1920, 1080); + const size_t dark_from = (size_t)(1080 * (1 - item.dark_bottom)); + for (size_t y = 0; y < 1080; y++){ + for (size_t x = 0; x < 1920; x++){ + screen.pixel(x, y) = y >= dark_from ? 0xff000000 : item.fill; + } + } + if (indoors(screen) != item.indoors){ + return item.name + (item.indoors ? ": read as outside." : ": read as inside."); + } + } + return true; + } +}; + +// door_fade_look(): out of a door is a dark look, then a bright one that is +// not a room. r259 turned round in Sandgem's lit Center, read "not a room" +// once at full brightness, and walked east inside it as if outside. +struct DoorLook{ + double brightness; + bool room; +}; +struct DoorCase{ + std::string name; + std::vector looks; + // The look (1-based) that reads out, or 0 for none. + size_t out_at; +}; + +class Test_DoorFade : public UnitTest{ +public: + Test_DoorFade() + : UnitTest("PokemonBDSP::AutoStory - out of a door needs its fade") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "r259: a bright look in the Center that misses the room", .looks = {{458, true}, {440, false}, {457, true}}, .out_at = 0}, + {.name = "the fade, then the town", .looks = {{458, true}, {300, true}, {60, false}, {310, false}}, .out_at = 4}, + {.name = "the fade, then the room again", .looks = {{458, true}, {60, false}, {420, true}}, .out_at = 0}, + {.name = "the fade, still dim", .looks = {{60, false}, {150, false}, {320, false}}, .out_at = 3}, + }; + for (const DoorCase& item : cases){ + DoorFadeWatch watch; + size_t out_at = 0; + for (size_t i = 0; i < item.looks.size() && out_at == 0; i++){ + if (door_fade_look(watch, item.looks[i].brightness, item.looks[i].room)){ + out_at = i + 1; + } + } + if (out_at != item.out_at){ + return item.name + ": out at look " + std::to_string(out_at) + ", expected " + std::to_string(item.out_at) + "."; + } + } + return true; + } +}; + + +class Test_LocationBanner : public UnitTest{ +public: + Test_LocationBanner() + : UnitTest("PokemonBDSP::AutoStory - location banner match") + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + struct Case{ + const char* read; + const char* expected; + bool match; + }; + const Case cases[] = { + {"sandgemtown", "Sandgem Town", true}, + {"andgemltown", "Sandgem Town", true}, // r109's OCR + {"Route 202", "Route 202", true}, + {"route2o2", "Route 202", false}, // a digit read as a letter is not trusted + {"route201", "Route 202", false}, + {"jubilifecity", "Sandgem Town", false}, + {"twinleaftown", "Sandgem Town", false}, + {"", "Sandgem Town", false}, + }; + for (const Case& c : cases){ + if (location_banner_matches(c.read, c.expected) != c.match){ + return std::string("Banner \"") + c.read + "\" for " + c.expected + + (c.match ? " should match." : " should not match."); + } + } + return true; + } +}; + + +class Test_Indoors : public UnitTest{ +public: + Test_Indoors() + : UnitTest("PokemonBDSP::AutoStory - indoors") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* file; bool inside; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", true}, + {"PokemonBDSP/AutoStory/SandgemCenterInside-20261030-053555.png", true}, + {"PokemonBDSP/AutoStory/JubilifeSchoolInside-20261030.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", false}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", false}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep2-20261030.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, + {"PokemonBDSP/AutoStory/SandgemCenterBelowSign-20261030-053527.png", false}, + {"PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", false}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const bool inside = indoors(ImageRGB32(path)); + logger.log(std::string(c.file) + ": " + (inside ? "indoors" : "outside")); + if (inside != c.inside){ + return std::string(c.file) + ": expected " + (c.inside ? "indoors" : "outside") + "."; + } + } + return true; + } +}; + + +class Test_LocationBannerShowing : public UnitTest{ +public: + Test_LocationBannerShowing() + : UnitTest("PokemonBDSP::AutoStory - location banner showing") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* file; bool banner; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", true}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const bool banner = location_banner_showing(ImageRGB32(path)); + logger.log(std::string(c.file) + ": banner " + (banner ? "shows" : "none")); + if (banner != c.banner){ + return std::string(c.file) + ": expected " + (c.banner ? "a banner" : "no banner") + "."; + } + } + return true; + } +}; + + +} + + +void add_tests_AutoStory_Walk(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h new file mode 100644 index 0000000000..ac120c6244 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Walk Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walk_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Walk_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// The walker's screen reads: indoors() on synthetic screens and +// screenshots, the door's fade, and the location banner. +void add_tests_AutoStory_Walk(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp new file mode 100644 index 0000000000..8819372066 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp @@ -0,0 +1,60 @@ +/* BDSP AutoStory - Indoors + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Color.h" +#include "PokemonBDSP_AutoStory_Indoors.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// Near-black pixels in one edge strip of the screen, out of all its pixels. +struct EdgeDark{ + size_t dark = 0; + size_t count = 0; +}; +void add_pixel(EdgeDark& edge, bool dark){ + edge.dark += dark; + edge.count++; +} +bool mostly_dark(const EdgeDark& edge){ + return edge.count > 0 && edge.dark * 100 >= edge.count * 15; +} + +} + +bool indoors(const ImageViewRGB32& screen){ + const size_t width = screen.width(), height = screen.height(); + const size_t strip_h = height * 12 / 100, strip_w = width * 12 / 100; + size_t warm = 0; + size_t count = 0; + EdgeDark top, bottom, left, right; + for (size_t y = 0; y < height; y += 8){ + for (size_t x = 0; x < width; x += 8){ + const Color pixel(screen.pixel(x, y)); + count++; + warm += pixel.red() > 150 && pixel.red() > pixel.blue() + 70 && pixel.green() > pixel.blue() + 20; + const bool dark = pixel.red() + pixel.green() + pixel.blue() < 60; + if (y < strip_h){ add_pixel(top, dark); } + if (y >= height - strip_h){ add_pixel(bottom, dark); } + if (x < strip_w){ add_pixel(left, dark); } + if (x >= width - strip_w){ add_pixel(right, dark); } + } + } + if (count == 0 || warm * 4 <= count){ + return false; + } + return mostly_dark(top) || mostly_dark(bottom) || mostly_dark(left) || mostly_dark(right); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h new file mode 100644 index 0000000000..0d469836d0 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h @@ -0,0 +1,33 @@ +/* BDSP AutoStory - Indoors + * + * From: https://github.com/PokemonAutomation/ + * + * Whether the screen shows a room: the walker leaves a building by it. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Indoors_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Indoors_H + +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Whether the screen shows a building's inside: its warm walls and floor +// fill much of the view (Pokemon Centers, the Trainers' School: 42..65% of +// the screen), and the room floats in black, which shows along at least one +// edge (15% or more of a strip 12% deep). Warmth alone is not enough: the +// evening tint makes outside 50..72% warm (r256, r257, Sandgem), and +// Oreburgh's ground 58..69% by day. Of 1412 lab screenshots, the rule calls +// every outside one outside, at all hours, and the rooms rooms. A fade is +// dark everywhere but not warm, so it is not inside. +bool indoors(const ImageViewRGB32& screen); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp new file mode 100644 index 0000000000..ecf90056c7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -0,0 +1,135 @@ +/* BDSP AutoStory - Location + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/Language.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "PokemonBDSP_AutoStory_Location.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// The location banner: a white box at the top left that names a map for a +// few seconds after the player enters it. Left of the centred name it is plain +// white (251 on console B, against 27-75 for the trees there otherwise). +const ImageFloatBox LOCATION_BANNER_WHITE{0.035, 0.035, 0.085, 0.050}; +const ImageFloatBox LOCATION_BANNER_TEXT{0.100, 0.025, 0.300, 0.080}; + +// The save card's header: the map's name in salmon (255, 152, 140) on near +// white (245), top left, right of the card's red border. Too faint for OCR +// as it is: the reddish pixels are made black on white first. +const ImageFloatBox SAVE_SCREEN_LOCATION{0.036, 0.045, 0.274, 0.070}; +const uint32_t SAVE_SCREEN_TEXT_MIN = 0xff960000; +const uint32_t SAVE_SCREEN_TEXT_MAX = 0xffffa9a9; + +std::string digits_of(const std::string& text){ + std::string out; + for (char ch : text){ + if (std::isdigit((unsigned char)ch)){ + out.push_back(ch); + } + } + return out; +} + +} + + +std::string normalize_location_name(const std::string& text){ + std::string out; + for (unsigned char ch : text){ + if (std::isalnum(ch)){ + out.push_back((char)std::tolower(ch)); + } + } + return out; +} + +Language g_story_language = Language::English; + +bool story_reads_text(){ + return story_reads_text(g_story_language); +} + +bool story_reads_text(Language language){ + return language == Language::English; +} + +bool location_banner_showing(const ImageViewRGB32& screen){ + const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); + return white.average.sum() >= 690 && white.stddev.sum() <= 30; +} + +std::string location_banner_stats(const ImageViewRGB32& screen){ + const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); + return "mean " + std::to_string((int)white.average.sum()) + ", spread " + std::to_string((int)white.stddev.sum()); +} + +std::string read_location_banner(const ImageViewRGB32& screen){ + if (!location_banner_showing(screen)){ + return ""; + } + if (!story_reads_text()){ + return UNREAD_LOCATION; + } + return normalize_location_name(OCR::tesseract_ocr_read(Language::English, + extract_box_reference(screen, LOCATION_BANNER_TEXT))); +} + +std::string read_save_screen_location(const ImageViewRGB32& screen){ + if (!story_reads_text()){ + return UNREAD_LOCATION; + } + const ImageRGB32 text = to_blackwhite_rgb32_range( + extract_box_reference(screen, SAVE_SCREEN_LOCATION), true, SAVE_SCREEN_TEXT_MIN, SAVE_SCREEN_TEXT_MAX); + return normalize_location_name(OCR::tesseract_ocr_read(Language::English, text)); +} + +bool location_banner_matches(const std::string& read, const std::string& expected){ + // No reader for the game's language: a banner (or the save card) that + // showed is all there is; where we stand is the walk's to say. + if (read == UNREAD_LOCATION){ + return true; + } + const std::string a = normalize_location_name(read); + const std::string b = normalize_location_name(expected); + if (a.empty() || b.empty()){ + return false; + } + if (digits_of(a) != digits_of(b)){ + return false; + } + // Levenshtein distance. + std::vector row(b.size() + 1); + for (size_t j = 0; j <= b.size(); j++){ + row[j] = j; + } + for (size_t i = 1; i <= a.size(); i++){ + size_t diagonal = row[0]; + row[0] = i; + for (size_t j = 1; j <= b.size(); j++){ + const size_t above = row[j]; + row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] == b[j - 1] ? 0 : 1)}); + diagonal = above; + } + } + return row[b.size()] <= std::max(1, b.size() / 4); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h new file mode 100644 index 0000000000..4294ede6eb --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h @@ -0,0 +1,65 @@ +/* BDSP AutoStory - Location + * + * From: https://github.com/PokemonAutomation/ + * + * Where the game says the player is: the location banner shown on entering + * a map, and the map named on the save screen's card. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Location_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Location_H + +#include +#include "CommonFramework/Language.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Lower-case letters and digits only: "Route 202" and "ROUTE 2O2 " compare +// by "route202". +std::string normalize_location_name(const std::string& text); + +// The game's language, for the reads that still use text (item names, the +// location banner, a sign's or talk's first box), set from the program's +// options (StoryRunOptions). story_reads_text() says whether those reads +// have a model and dictionary for it (English only, so far); when not, +// callers use language-free checks: the banner's box showing, a sign's box +// opening, the first row of a Bag pocket. +extern Language g_story_language; +bool story_reads_text(); +bool story_reads_text(Language language); + +// What the reads below return for a banner or save card that showed, when +// there is no text reader for the game's language (story_reads_text()): +// location_banner_matches() takes it as a match. +inline const std::string UNREAD_LOCATION = "(unread)"; + +// The location banner (the white box at the top left that names a map for a +// few seconds after the player enters it), read by OCR and normalized; +// empty when no banner shows. +std::string read_location_banner(const ImageViewRGB32& screen); +// Whether the location banner's white box shows, without reading it. +bool location_banner_showing(const ImageViewRGB32& screen); +// The banner box's mean and spread, for the log. +std::string location_banner_stats(const ImageViewRGB32& screen); + +// The map named at the top left of the save screen's card ("Sandgem Town", +// "Route 202"), read by OCR and normalized; empty if nothing reads. +std::string read_save_screen_location(const ImageViewRGB32& screen); + +// Whether the location banner's OCR `read` names the map `expected`. Both +// are compared as lower-case letters and digits. OCR drops or swaps letters +// ("andgemltown" for Sandgem Town on r109), so up to a quarter of the name's +// letters may differ; the digits must match exactly, so Route 201 is never +// read as Route 202. +bool location_banner_matches(const std::string& read, const std::string& expected); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp new file mode 100644 index 0000000000..7ac01390db --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp @@ -0,0 +1,42 @@ +/* BDSP AutoStory - Overlay Log + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "PokemonBDSP_AutoStory_OverlayLog.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color){ + // The log box is 35% of the video wide at a font 4% of its height: about + // 34 characters. + constexpr size_t WIDTH = 34; + std::string line; + size_t start = 0; + while (start < text.size()){ + size_t end = text.find(' ', start); + if (end == std::string::npos){ + end = text.size(); + } + const std::string word = text.substr(start, end - start); + if (!line.empty() && line.size() + 1 + word.size() > WIDTH){ + overlay.add_log(line, color); + line = " "; + } + line += (line.empty() || line == " " ? "" : " ") + word; + start = end + 1; + } + if (!line.empty()){ + overlay.add_log(line, color); + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h new file mode 100644 index 0000000000..f030b54f1c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory - Overlay Log + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_OverlayLog_H +#define PokemonAutomation_PokemonBDSP_AutoStory_OverlayLog_H + +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Adds `text` to the video overlay's log, split at spaces into lines short +// enough for the log box, which draws each line on one row and cuts it off. +void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp new file mode 100644 index 0000000000..8b32093da4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -0,0 +1,926 @@ +/* BDSP AutoStory - Route Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Navigation/Pokemon_RouteLog.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP_AutoStory_OverlayLog.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +bool across(Direction direction){ + return direction == Direction::EAST || direction == Direction::WEST; +} + +Milliseconds ms_per_tile(Direction direction){ + return across(direction) ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN; +} + +// The left stick for a direction. Row 0 is the top of a map, and the stick's +// y is positive upward. +void stick_for(Direction direction, double& x, double& y){ + x = 0; + y = 0; + switch (direction){ + case Direction::NORTH: y = 1; break; + case Direction::EAST: x = 1; break; + case Direction::SOUTH: y = -1; break; + case Direction::WEST: x = -1; break; + } +} + +// World tiles as tiles of the named maps, for the shared route log +// (Pokemon_RouteLog.h); a tile no map holds is a "world" tile. +std::vector route_tiles(const MapSet& maps, const std::vector& tiles){ + std::vector out; + for (const TilePos& pos : tiles){ + const std::optional local = maps.to_local(pos); + out.push_back(local ? RouteTile{local->map, local->pos} : RouteTile{"world", pos}); + } + return out; +} + +std::string describe(const MapSet& maps, const std::vector& tiles){ + return Pokemon::describe(route_tiles(maps, tiles)); +} + +RouteLogLeg route_log_leg(const TileLeg& leg){ + return {.action = RouteLegAction::WALK, .direction = leg.direction, .tiles = leg.tiles, + .end = leg.ends_at_wall ? RouteLegEnd::WALL : RouteLegEnd::OPEN}; +} + +PlannedRouteLeg planned_route_leg(const MapSet& maps, const PlannedLeg& leg){ + return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; +} + +void log_plan(VideoStream& stream, const MapSet& maps, const std::string& label, const std::vector& from, const std::vector& legs){ + std::vector logged; + for (const PlannedLeg& leg : legs){ + logged.push_back(planned_route_leg(maps, leg)); + } + stream.log(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged)); +} + +// Logs a leg that ended on its planned tiles, and how that is known: an +// open leg counts its taps through a wild battle, so its end is assumed; a +// leg to a wall is walked again to its wall. +void log_leg_done(VideoStream& stream, const MapSet& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ + const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; + stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), + assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); +} + +// The tiles the player passes when walking `tiles` tiles from `from`, the +// first one after `from` first. Stops early where the player is stopped. +std::vector leg_path(const TileGrid& grid, TilePos from, Direction direction, size_t tiles){ + std::vector path; + size_t travelled = 0; + while (travelled < tiles){ + const std::optional next = grid.step(from, direction); + if (!next){ + break; + } + travelled += std::abs(next->x - from.x) + std::abs(next->y - from.y); + from = *next; + path.push_back(from); + } + return path; +} + +// Holding toward a wall with people or Pokemon moving beside us never reads +// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking +// scrolls the view far more (35 to 90). True if holding toward (x, y) for +// 1.6 s changes the screen less than walking would: we stand at a wall. +bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ + constexpr double WALKING_CHANGE = 25.0; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + double most = 0; + for (size_t chunk = 0; chunk < 4 && before; chunk++){ + pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (!after){ + return false; + } + most = std::max(most, walk_screen_change(*before.frame, *after.frame)); + before = std::move(after); + } + stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + + (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); + return before && most < WALKING_CHANGE; +} + +// The map all of `tiles` are on, or empty if they are on several or none. +std::string map_of(const MapSet& maps, const std::vector& tiles){ + std::string name; + for (const TilePos& pos : tiles){ + const std::optional tile = maps.to_local(pos); + if (!tile || (!name.empty() && tile->map != name)){ + return ""; + } + name = tile->map; + } + return name; +} + +} + + +bool route_wall_readable(const TileGrid& grid, const std::vector& after){ + return std::none_of(after.begin(), after.end(), [&](const TilePos& pos){ + return grid.at(pos) == TileType::TALL_GRASS; + }); +} + +void route_status(VideoStream& stream, const std::string& text){ + stream.log("[Route] " + text, COLOR_BLUE); + add_wrapped_log(stream.overlay(), text, COLOR_WHITE); +} + +Milliseconds route_open_hold(Direction direction, size_t tiles){ + const Milliseconds step = ms_per_tile(direction); + return step * tiles - step / 2; +} + +void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ + double x, y; + stick_for(direction, x, y); + stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); + pbf_move_left_joystick(context, {x, y}, ROUTE_TURN_TAP, 300ms); + context.wait_for_all_requests(); +} + +std::optional talk_to_adjacent_npc( + VideoStream& stream, ProControllerContext& context, + const std::vector& directions, + const std::string& label, Milliseconds wait +){ + for (Direction direction : directions){ + route_turn(stream, context, direction); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, wait, {{dialog}}) == 0){ + stream.log("[Route] " + label + ": answered from the " + direction_name(direction) + "."); + return direction; + } + } + stream.log("[Route] " + label + ": nobody answered next to us."); + return std::nullopt; +} + +bool talk_with_nudges( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + Direction facing, const std::string& label, Milliseconds wait, + const std::function& accept +){ + const bool vertical = facing == Direction::NORTH || facing == Direction::SOUTH; + const Direction minus = vertical ? Direction::WEST : Direction::NORTH; + const Direction plus = vertical ? Direction::EAST : Direction::SOUTH; + int at = 0; + for (int offset : {0, -1, 1, -2, 2}){ + const Direction side = offset < at ? minus : plus; + for (; at != offset; at += offset < at ? -1 : 1){ + double x, y; + stick_for(side, x, y); + pbf_move_left_joystick(context, {x, y}, ROUTE_NUDGE_TAP, 300ms); + } + if (offset != 0){ + route_turn(stream, context, facing); + // A tap in a cave or in grass can start a wild battle, after + // which we are not where the nudges think: let the caller line up again. + if (run_from_late_encounter(stream, context, escape, label + ": nudge")){ + return false; + } + } + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, wait, {{dialog}}) == 0){ + const VideoSnapshot after = stream.video().snapshot(); + if (!accept || (before && after && accept(*before.frame, *after.frame))){ + stream.log("[Route] " + label + ": answered " + std::to_string(offset) + " nudge(s) from where we stood."); + return true; + } + stream.log("[Route] " + label + ": something else answered " + std::to_string(offset) + " nudge(s) from where we stood; closing it."); + for (size_t press = 0; press < 3; press++){ + pbf_press_button(context, BUTTON_B, 80ms, 700ms); + } + context.wait_for_all_requests(); + } + } + stream.log("[Route] " + label + ": nobody answered within two nudges either side."); + return false; +} + +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const MapSet& maps, + std::vector from, + const std::function& goal, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const std::vector& avoid, + const RobustPlanCosts& costs +){ + // Our own copy, so things found in the way can be added to it. Tiles to + // avoid are ground the game lets us walk on (a trainer's line of sight), + // so they are off the map, not walls: a leg must not end "at" one, or + // the walk would run on through it. + TileGrid grid = maps.world(); + for (const TilePos& pos : avoid){ + if (std::find(from.begin(), from.end(), pos) == from.end()){ + grid.set(pos, TileType::UNKNOWN); + } + } + // Which way the player faces, once a leg has set it. + std::optional facing; + // Every banner read on the way, to check the map we arrive on against. + const std::string map_start = map_of(maps, from); + std::vector all_banners; + for (size_t plans = 0; plans < 8; plans++){ + std::optional> legs = plan_robust_route(grid, from, goal, costs); + if (!legs){ + // From a few possible tiles the exact-open-leg costs found no + // plan where the default ones did (r240a, three tiles on Route + // 204's row 27); the default plan is longer but sound. + stream.log("[Route] " + label + ": no plan with the route costs from " + describe(maps, from) + + "; trying the default costs.", COLOR_ORANGE); + legs = plan_robust_route(grid, from, goal, RobustPlanCosts()); + } + if (!legs){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no plan from " + describe(maps, from) + ".", stream); + } + route_status(stream, label + ": " + std::to_string(legs->size()) + " legs planned"); + log_plan(stream, maps, label, from, *legs); + + bool replanned = false; + size_t leg_number = 0; + for (const PlannedLeg& planned : *legs){ + const TileLeg& leg = planned.leg; + leg_number++; + route_status(stream, route_leg_line(leg_number, legs->size(), planned_route_leg(maps, planned))); + double x, y; + stick_for(leg.direction, x, y); + const std::string leg_label = label + ": " + to_string(leg); + + // Note the location banners the walk passes, to check map changes. + // A wild battle or a second walk to the wall may follow the + // banner: any banner seen during the leg counts. + const std::string map_before = map_of(maps, from); + const std::string map_after = map_of(maps, planned.after); + std::vector banners; + auto banner_ok = [&]{ + return std::any_of(banners.begin(), banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_after); + }); + }; + auto watch_banner = [&](const ImageViewRGB32& screen){ + if (banners.empty() || (!map_after.empty() && !banner_ok() && banners.size() < 8)){ + const std::string read = read_location_banner(screen); + if (!read.empty() && (banners.empty() || banners.back() != read)){ + banners.push_back(read); + } + } + return false; + }; + + WalkResult result; + // Tiles the first tap of an open leg moved (see below). + size_t turn_moved = 0; + if (leg.ends_at_wall){ + // Run (B held) on the D-pad: a leg to a wall ends at the wall + // however fast we get there, and the D-pad keeps to the row. + const Milliseconds per_tile = ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; + // Long enough to reach the wall from the farthest tile, with room + // for a slow start; past the wall the player stands still. + const Milliseconds limit = ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; + result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + // A wild battle may end the walk early. Walking to a wall again + // ends in the same place, so finish the leg. + const bool interrupted = result.wild_battles > 0; + for (size_t again = 0; again < 3 && result.wild_battles > 0 && + (result.stop == WalkStop::LIMIT || result.stop == WalkStop::BLOCKED); again++ + ){ + stream.log("[Route] " + leg_label + ": a wild battle on the way; walking to the wall again."); + result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + } + // A wall well before the nearest one the map has: something the + // map does not know is in the way. Work out where we stopped from + // the time walked, put it on our map and plan again from there. + size_t nearest = SIZE_MAX; + for (const TilePos& pos : from){ + nearest = std::min(nearest, walk_to_wall(grid, pos, leg.direction).tiles); + } + auto tiles_moved = [&](const WalkResult& walk){ + const Milliseconds moving = walk.walked - ROUTE_START_DELAY - + (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); + return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); + }; + size_t walked_tiles = tiles_moved(result); + // A stick walk can catch a corner when we stand part of a tile + // off centre, where a D-pad tap, which moves whole tiles, gets + // through: on Route 203's stairs the walk west stopped nine + // times in a row and two taps crossed (r225a, 2026-10-31). + // Tap twice, then walk on to the wall. + if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ + const DpadPosition dpad = + leg.direction == Direction::NORTH ? DPAD_UP : + leg.direction == Direction::SOUTH ? DPAD_DOWN : + leg.direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); + for (size_t tap = 0; tap < 2; tap++){ + pbf_press_dpad(context, dpad, 160ms, 640ms); + } + context.wait_for_all_requests(); + result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + walked_tiles += tiles_moved(result); + } + // Someone walking about may stand in the way for a moment: wait + // and walk to the wall again before calling it an obstacle. + for (size_t again = 0; !interrupted && again < 2 && + result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++ + ){ + stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(context, 2500ms); + context.wait_for_all_requests(); + result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + walked_tiles += tiles_moved(result); + } + // Any stop short of the wall's distance: someone who stays put + // is talked to, and the leg is planned again from where we are. + // Within the time count's slack this may be the wall itself, + // where A does nothing and the new plan walks no further (r239a: + // a wanderer 3 tiles short passed as "about 8 of 10", and every + // later leg was off by those tiles). + if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ + // Someone who stays put: talk to them. A trainer we walked up + // to from the side starts the battle this way. + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + { + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, leg_label + ": someone is in the way; talking to them."); + fight("someone in the way"); + } + } + std::vector stopped; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, SIZE_MAX); + for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ + stopped.push_back(k == 0 ? pos : path[k - 1]); + } + } + std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": stopped early, and no tile fits.", stream); + } + if (stopped.size() == 1){ + const TilePos blocker = stopped[0].next(leg.direction); + grid.set(blocker, TileType::BLOCKED); + stream.log("[Route] " + leg_label + ": something the map lacks is at " + + maps.describe(blocker) + "; marked it.", COLOR_ORANGE); + } + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + "stopped early after about " + std::to_string(walked_tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); + facing = leg.direction; + from = stopped; + replanned = true; + break; + } + // A leg to the wall that ran out of time is not at its wall: + // the open leg before it stopped short, and it ran along + // another row (r127a went west along Sandgem's row 13 for row + // 14, and the next leg north walked into the lab). Out of tall + // grass a stop reads, so walk on: a player still moving is + // not where the plan has him. + if (!interrupted && result.stop == WalkStop::LIMIT && route_wall_readable(grid, planned.after)){ + const WalkResult check = walk_until_dpad(stream, context, escape, x, y, 2000ms, leg_label + ": at the wall?", nullptr, true); + if (check.stop == WalkStop::DIALOG){ + // A trainer who saw us froze the walk (the "!") until + // its time ran out, then walked over (r241a: Aroma Lady + // Taylor's line crosses Route 204's corridor). Battle, + // then plan again from where a sight line crosses the leg. + route_status(stream, leg_label + ": a trainer stopped us on the way; battling."); + fight("a trainer on the way"); + // Where they saw us from is not reliable (BDSP's trainers + // stand a few tiles from Platinum's, r242a), so do not + // guess the tile: a leg to a wall ends at its wall however + // far it got. Walk on to it. + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + } + if (check.stop != WalkStop::BLOCKED && + !(check.stop == WalkStop::LIMIT && holding_against_wall(stream, context, x, y, leg_label + ": at the wall?")) + ){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": ran out of time short of the wall, and walking on gets " + + walk_stop_name(check.stop) + ": not where the plan has us.", stream); + } + } + }else{ + // Whole tiles on the D-pad, a tap each: a tap moves exactly + // one tile, while a stick hold timed for three moved one in + // the Ravaged Path (r235b) and the belief drifted four rows. + // The first tap of a leg was meant to turn, but a 160 ms + // D-pad tap turns and steps whichever way the player faced + // (r248a: all seven did; r246a: "south 1" out of the cave + // went two tiles, onto the Youngster's row), so the tap is + // measured like any other and a step counts for the leg. + if (facing != leg.direction){ + const DpadPosition dpad = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + pbf_press_dpad(context, dpad, 160ms, 700ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ + turn_moved = 1; + stream.log("[Route] " + leg_label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); + } + } + if (leg.tiles > turn_moved){ + result = walk_tiles_dpad(stream, context, escape, x, y, leg.tiles - turn_moved, leg_label); + }else{ + result = WalkResult{WalkStop::LIMIT, 0ms, 0}; + } + // A wild battle starts once the step onto the grass is done, + // so the taps still count; the walk went on after it. + if (result.wild_battles > 0){ + stream.log("[Route] " + leg_label + ": " + std::to_string(result.wild_battles) + + " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); + } + // Taps that moved nothing: someone stands in the way (the + // Youngster below the Ravaged Path wanders across the row + // under the cliff, r239a, r240a). Two still taps end the walk, + // so the tiles moved are the taps less those two. Wait for + // them to move on and tap the rest; someone who stays is + // talked to, put on the map, and the leg planned again from + // the tile we stand on, which the taps give exactly. + auto taps_moved = [](const WalkResult& walk){ return walk.moved_chunks; }; + size_t moved = std::min(leg.tiles, taps_moved(result) + turn_moved); + // One wait: a wanderer in the way steps off in a few seconds; + // anything still there after that is talked to (r246a lost + // 20 s per block on three waits). + for (size_t again = 0; again < 1 && result.stop == WalkStop::BLOCKED && moved < leg.tiles; again++){ + stream.log("[Route] " + leg_label + ": blocked after " + std::to_string(moved) + " of " + + std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(context, 2500ms); + context.wait_for_all_requests(); + const WalkResult rest = walk_tiles_dpad(stream, context, escape, x, y, leg.tiles - moved, leg_label + ": the rest"); + moved = std::min(leg.tiles, moved + taps_moved(rest)); + result.stop = rest.stop; + result.walked += rest.walked; + result.wild_battles += rest.wild_battles; + } + if (result.stop == WalkStop::BLOCKED && moved < leg.tiles){ + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + { + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, leg_label + ": someone is in the way; talking to them."); + fight("someone in the way"); + } + } + std::vector stopped; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, moved); + stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); + } + std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); + if (stopped.size() == 1){ + const TilePos blocker = stopped[0].next(leg.direction); + grid.set(blocker, TileType::BLOCKED); + stream.log("[Route] " + leg_label + ": something the map lacks is at " + + maps.describe(blocker) + "; marked it.", COLOR_ORANGE); + } + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + "blocked after " + std::to_string(moved) + " of " + std::to_string(leg.tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); + facing = leg.direction; + from = stopped; + replanned = true; + break; + } + } + facing = leg.direction; + { + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + watch_banner(*screen.frame); + } + } + std::string banner_text; + for (const std::string& read : banners){ + banner_text += (banner_text.empty() ? "\"" : ", \"") + read + "\""; + } + all_banners.insert(all_banners.end(), banners.begin(), banners.end()); + // Entering another map shows its name; a missing or wrong name means + // the walk did not end where the plan says. From tiles on several + // maps (a leg not confirmed) the banner may already have shown. + if (!map_before.empty() && !map_after.empty() && map_after != map_before && !banner_ok()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": expected to enter " + map_after + ", but the location banner read " + + (banner_text.empty() ? "nothing" : banner_text) + ".", stream); + } + if (!banner_text.empty()){ + stream.log("[Route] " + leg_label + ": location banner " + banner_text + "."); + } + // Where each leg ended, to compare with the plan after a run. + if (VideoSnapshot screen = stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(leg_label) + ".png"); + } + if (result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": unexpected transition.", stream); + } + // A trainer who sees us freezes the player and walks over; the + // dialogue may open after the walk has ended. + const std::vector> paths = [&]{ + std::vector> out; + for (const TilePos& pos : from){ + out.push_back(leg_path(grid, pos, leg.direction, + leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); + } + return out; + }(); + std::vector watching; + for (size_t t = 0; t < trainers.size(); t++){ + const std::vector sight = sight_tiles(grid, trainers[t]); + for (const std::vector& path : paths){ + if (std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ + return std::find(sight.begin(), sight.end(), pos) != sight.end(); + })){ + watching.push_back(t); + break; + } + } + } + bool talking = result.stop == WalkStop::DIALOG; + if (!talking){ + ShortDialogWatcher dialog(COLOR_YELLOW); + const Milliseconds wait = watching.empty() ? 500ms : 4000ms; + talking = wait_until(stream, context, wait, {{dialog}}) == 0; + } + if (!talking){ + from = planned.after; + log_leg_done(stream, maps, label, planned, result.wild_battles); + continue; + } + + if (watching.empty()){ + // A trainer the map does not have. Fight, then plan again from + // anywhere the leg could have stopped. + std::vector anywhere = from; + for (const std::vector& path : paths){ + anywhere.insert(anywhere.end(), path.begin(), path.end()); + } + std::sort(anywhere.begin(), anywhere.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + anywhere.erase(std::unique(anywhere.begin(), anywhere.end()), anywhere.end()); + route_status(stream, label + ": a trainer the map lacks stopped us; fighting."); + fight("unknown trainer"); + if (leg.ends_at_wall){ + const Milliseconds limit = ROUTE_START_DELAY + + ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(stream, context, escape, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + from = planned.after; + log_leg_done(stream, maps, label, planned, 0); + continue; + } + from = anywhere; + replanned = true; + break; + } + // Each possible start tile was stopped on the first tile of its path + // inside a watching trainer's sight. + const TrainerSight trainer = trainers[watching[0]]; + std::vector stopped; + for (const std::vector& path : paths){ + for (const TilePos& pos : path){ + bool seen = false; + for (size_t t : watching){ + const std::vector sight = sight_tiles(grid, trainers[t]); + seen |= std::find(sight.begin(), sight.end(), pos) != sight.end(); + } + if (seen){ + stopped.push_back(pos); + break; + } + } + } + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": " + trainer.name + " talked, but no path enters their sight.", stream); + } + stream.log(ROUTE_LOG_TAG + route_trainer_line(label, trainer.name, route_tiles(maps, stopped))); + route_status(stream, "Fighting " + trainer.name + "."); + fight(trainer.name); + trainers.erase(trainers.begin() + watching[0]); + // Their sight line only says roughly where: BDSP's trainers stand a + // few tiles from Platinum's (r242a). A leg to a wall ends at its + // wall however far it got: walk on to it and carry on. An open leg + // counts its taps: the step that brought us into sight was complete. + if (leg.ends_at_wall){ + const Milliseconds limit = ROUTE_START_DELAY + + ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(stream, context, escape, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + from = planned.after; + log_leg_done(stream, maps, label, planned, 0); + continue; + } + // Only the taps that moved count: the trainer freezes the player + // on the tile they saw, and the taps pressed while they walk over + // move nobody (r248a: "4 taps" were 2 steps; the belief of two + // tiles then cost a 12-tile run to a wall and 5 back). + const size_t taps = std::min(leg.tiles, result.moved_chunks + turn_moved); + std::vector by_taps; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, leg.tiles); + const size_t k = std::min(taps, path.size()); + by_taps.push_back(k == 0 ? pos : path[k - 1]); + } + std::sort(by_taps.begin(), by_taps.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + by_taps.erase(std::unique(by_taps.begin(), by_taps.end()), by_taps.end()); + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + std::to_string(taps) + " taps before the battle", route_tiles(maps, by_taps)), COLOR_ORANGE); + from = by_taps; + replanned = true; + break; + } + if (!replanned){ + // Legs whose ends span two maps skip the per-leg check; the walk + // as a whole must still have seen the banner of the map it ends on. + const std::string map_end = map_of(maps, from); + const bool changed_map = !map_start.empty() && !map_end.empty() && map_end != map_start; + if (changed_map && std::none_of(all_banners.begin(), all_banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_end); + })){ + std::string seen; + for (const std::string& read : all_banners){ + seen += (seen.empty() ? "\"" : ", \"") + read + "\""; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": arrived at " + describe(maps, from) + " on the map, but " + map_end + + "'s location banner never showed (read " + (seen.empty() ? "nothing" : seen) + ").", stream); + } + stream.log(ROUTE_LOG_TAG + route_arrived_line(label, route_tiles(maps, from))); + return from; + } + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": too many plans.", stream); +} + + + +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const MapSet& maps, + std::vector from, + const std::vector& waypoints, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const RobustPlanCosts& costs +){ + for (size_t i = 0; i < waypoints.size(); i++){ + const TilePos goal = waypoints[i]; + from = walk_route(stream, context, escape, maps, from, + [&](const TilePos& pos){ return pos == goal; }, + trainers, fight, + label + " (" + std::to_string(i + 1) + "/" + std::to_string(waypoints.size()) + ", to " + maps.describe(goal) + ")", + {}, costs); + } + return from; +} + +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const MapSet& maps, + std::vector& at, + const TilePos& npc, + const std::vector& sides, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label +){ + for (Direction side : sides){ + const TilePos stand = npc.next(side); + if (!maps.world().standable(stand)){ + continue; + } + at = walk_route(stream, context, escape, maps, at, [&](const TilePos& pos){ return pos == stand; }, + trainers, fight, label); + // Face the NPC: the opposite of the side we stand on. + const Direction face = + side == Direction::NORTH ? Direction::SOUTH : + side == Direction::SOUTH ? Direction::NORTH : + side == Direction::EAST ? Direction::WEST : Direction::EAST; + route_turn(stream, context, face); + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, label + ": talking from " + maps.describe(stand) + "."); + return; + } + route_status(stream, label + ": nobody answered from " + maps.describe(stand) + "; trying another side."); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no side of " + maps.describe(npc) + " started a dialogue.", stream); +} + +void finish_dialogue( + VideoStream& stream, ProControllerContext& context, + Button button, const std::string& label, Milliseconds timeout +){ + // No line counts: talks differ in length (r115a: 8 A left the Poke Mart + // lady's Potions talk open). The box blinks off between lines and during + // "obtained" jingles, so only a stable absence ends the talk. + const WallClock none = WallClock::max(); + ShortDialogDetector dialog; + OverworldDetector overworld; + const WallClock deadline = current_time() + timeout; + WallClock quiet_since = none; + WallClock unknown_since = none; + while (current_time() < deadline){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const WallClock now = current_time(); + const bool talking = frame && dialog.detect(frame); + if (frame && !talking && overworld.detect(frame)){ + unknown_since = none; + if (quiet_since == none){ + quiet_since = now; + }else if (now - quiet_since >= 1500ms){ + return; + } + context.wait_for(200ms); + continue; + } + quiet_since = none; + // Press only on a box (an A on the overworld talks again), or on a + // screen that is neither for 3 s. + if (talking || (unknown_since != none && now - unknown_since >= 3000ms)){ + unknown_since = none; + pbf_press_button(context, button, 80ms, 1200ms); + continue; + } + if (unknown_since == none){ + unknown_since = now; + } + context.wait_for(200ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": the dialogue did not end within " + + std::to_string(std::chrono::duration_cast(timeout).count()) + " s.", stream); +} + +void enter_door_north(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ + // Sideways steps to try after a blocked walk: east, then back west and + // one more. + const Direction steps[] = {Direction::EAST, Direction::WEST, Direction::WEST}; + for (size_t attempt = 0; attempt <= 3; attempt++){ + const WalkResult walk = walk_until(stream, context, escape, 0, 1, 2500ms, label); + if (walk.stop == WalkStop::TRANSITION){ + context.wait_for(1500ms); + return; + } + if (attempt == 3){ + break; + } + route_status(stream, label + ": the door frame stopped us; one tile " + + (steps[attempt] == Direction::EAST ? "east." : "west.")); + route_turn(stream, context, steps[attempt]); + const double x = steps[attempt] == Direction::EAST ? 1 : -1; + pbf_move_left_joystick(context, {x, 0}, route_open_hold(steps[attempt], 1), 200ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": could not go through the door.", stream); +} + +bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room){ + if (brightness < DOOR_FADE_DARK){ + watch.saw_dark = true; + } + return watch.saw_dark && !room && brightness >= DOOR_OUTSIDE_BRIGHT; +} + +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room){ + // Both are asked: the fade watch keeps what it has seen. + const bool faded = door_fade_look(watch, brightness, room); + return faded || !room; +} + +namespace{ + +// Three looks half a second apart, none a room: outside, whatever the fade +// did. A look of a lit Center can miss its black edge (r259 at 800 ms), +// three in a row standing still do not. +bool outside_for_three_looks(VideoStream& stream, ProControllerContext& context, const std::string& label){ + for (size_t look = 0; look < 3; look++){ + context.wait_for_all_requests(); + VideoSnapshot now = stream.video().snapshot(); + const bool room = !now || indoors(*now.frame); + stream.log("[Route] " + label + ": standing look " + std::to_string(look + 1) + ": " + (room ? "a room" : "not a room") + "."); + if (room){ + return false; + } + pbf_wait(context, 500ms); + } + return true; +} + +} + +void leave_door_south(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ + // A last line of talk may still be up, and it would stop the walk. + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + // Stop the walk at the fade or at the first look that is not a room: a + // stick held on walks on outside (r124a ten rows south of the Mart's + // door, r257 into a house, r261 three rows). Standing looks then confirm. + DoorFadeWatch watch; + auto out_of_door = [&](const ImageViewRGB32& screen){ + const double brightness = image_stats(screen).average.sum(); + const bool room = indoors(screen); + const bool out = door_walk_stops(watch, brightness, room); + stream.log(std::format("[Route] {}: look: brightness {:.0f}, {}, fade {}.", label, brightness, + room ? "a room" : "not a room", watch.saw_dark ? "seen" : "not seen")); + return out; + }; + bool out = false; + for (size_t tries = 0; tries < 5 && !out; tries++){ + const WalkResult walk = walk_until(stream, context, escape, 0, -1, 6000ms, label, out_of_door); + if (walk.stop == WalkStop::DIALOG){ + pbf_mash_button(context, BUTTON_B, 2000ms); + } + // Let the fade-in finish, then look again standing still. + pbf_wait(context, 1500ms); + out = walk.stop == WalkStop::TRANSITION || outside_for_three_looks(stream, context, label); + if (!out){ + stream.log("[Route] " + label + ": still indoors; walking on."); + } + } + if (!out){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": did not get back out.", stream); + } + // Let the fade-in finish. + pbf_wait(context, 1500ms); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h new file mode 100644 index 0000000000..b7057b6303 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -0,0 +1,203 @@ +/* BDSP AutoStory - Route Walk + * + * From: https://github.com/PokemonAutomation/ + * + * Walks a plan made on the story maps: legs to a wall are held until the + * player stops, open legs for their length in time. Tracks every tile the + * player may be on, and plans again after a trainer stops the walk. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H +#define PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H + +#include +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Walking time per tile, measured on the console from the post-catch save: +// a row is deeper than a column is wide under the tilted camera. +constexpr Milliseconds ROUTE_MS_PER_TILE_ACROSS = Milliseconds(160); // east or west +constexpr Milliseconds ROUTE_MS_PER_TILE_UP_DOWN = Milliseconds(240); // north or south +// From pressing the stick to the first step, when standing still. +constexpr Milliseconds ROUTE_START_DELAY = Milliseconds(250); +// Running (B held) covers a tile in this fraction of the walking time. B +// held with the D-pad did not run: r238a walked the Ravaged Path's corridor +// at 160 ms a tile, and a limit set for twice that speed cut a wall leg +// short by two tiles. 1 until running is measured to work. +constexpr int ROUTE_RUN_SPEEDUP = 1; + +// How long to hold the stick for an open leg of `tiles` tiles, for a player +// already facing that way: a step once started is always finished, so stop +// half a tile early. On the console a 1290ms hold west went 9 tiles and an +// 800ms hold south 4: the player starts at once and finishes the step. +Milliseconds route_open_hold(Pokemon::Direction direction, size_t tiles); + +// A tap this short turns a player who faces another way, without a step: on +// the console a 150ms tap south after walking north only turned the player. +constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(100); + +// Turns the player to face `direction` without moving. Only for a player who +// faces another way: one already facing it may take a step. +void route_turn(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction); + +// Talks to someone on a tile next to ours, without trusting the tile we +// counted ourselves onto: for each of `directions` in order, turns that way, +// presses A and waits up to `wait` for a dialogue. Returns the direction that +// answered, its dialogue open, or none if nobody did. Turns are taps (see +// route_turn()), so do not list the way the player already faces first. +std::optional talk_to_adjacent_npc( + VideoStream& stream, ProControllerContext& context, + const std::vector& directions, + const std::string& label, Milliseconds wait = Milliseconds(1500) +); + +// Talks to someone straight ahead in `facing`, where we may stand a little +// off their tile: BDSP moves by any distance, and a sideways tap moves about +// a third of a tile (a 50ms tap 0.2 to 0.45 tiles, a 100ms turn tap 0.4, on +// the console 2026-10-31). Presses A, and while nobody answers shifts by one +// such tap at a time to -1, +1, -2, +2 taps from the start and tries again. +// Never turns away and back between tries: each turn is a tap that moves us +// (s03e2 drifted east of Roark a third of a tile per try). Returns whether a +// dialogue opened, left open; false also after a wild battle on a nudge, +// which leaves us off the place we were. +// `accept`, if given, looks at the screen before A and once the dialogue +// is open, and says whether it is the one wanted: a rock or a sign next to +// someone answers A too (r187a read "It's a rugged rock" beside Roark). A +// dialogue it turns down is closed with B and the nudges go on. +bool talk_with_nudges( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + Pokemon::Direction facing, const std::string& label, + Milliseconds wait = Milliseconds(3000), + const std::function& accept = nullptr +); +constexpr Milliseconds ROUTE_NUDGE_TAP = Milliseconds(50); + +// Whether a walk to the wall that ends on any of `after` can tell it has +// stopped: not in tall grass, whose sway keeps the screen changing at a +// wall. Elsewhere, a walk to the wall that stops confirms where it ended. +bool route_wall_readable(const Pokemon::TileGrid& grid, const std::vector& after); + +// Shows what the route walk is doing in the log and the video overlay. +void route_status(VideoStream& stream, const std::string& text); + +// Called when a trainer's dialogue has opened, or when someone standing in +// the way was talked to. Fights the battle if one starts, and returns to the +// overworld either way. +using RouteTrainerFight = std::function; + +// Walks from any of the world tiles `from` of `maps` until every tile the +// player may be on is `goal`, and returns those tiles. Trainers in +// `trainers` who see the player are fought with `fight` and taken off the +// list. Tiles in `avoid` (where someone wanders) are planned as walls. +// Fails with a screenshot +// on a transition, a dialogue no trainer explains, a wild battle during an +// open leg, or when no plan exists. A walk from one map to another must see +// the second map's location banner on the way: a walk whose legs all ended +// on walls can still end in the wrong place (r124a saved in Sandgem Town +// after "arriving" on Route 202). +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const Pokemon::MapSet& maps, + std::vector from, + const std::function& goal, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const std::vector& avoid = {}, + const Pokemon::RobustPlanCosts& costs = route_plan_costs() +); + +// Walks `from` to each of `waypoints` in turn, with walk_route() for every +// stretch, and returns where the last one ended. The corners of a line a +// human walked, so the plan is that line, with the walker's handling of +// trainers and blocks on each stretch (Route 204 North, 2026-10-31). +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const Pokemon::MapSet& maps, + std::vector from, + const std::vector& waypoints, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const Pokemon::RobustPlanCosts& costs = waypoint_plan_costs() +); + +// Walks next to the NPC on world tile `npc`, faces them and presses A, trying +// the sides in `sides` (the side of the NPC to stand on) in order until a +// dialogue opens. `at` is where the player may be, and is updated. Fails with +// a screenshot if no side starts a dialogue. A press from a diagonal tile, +// or from a tile a row off, talks to nobody, so each side is tried in turn. +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const Pokemon::MapSet& maps, + std::vector& at, + const Pokemon::TilePos& npc, + const std::vector& sides, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label +); + +// Finishes a dialogue that has opened, however many lines it has: presses +// `button` while a dialogue box shows, and returns once the overworld has +// shown with no box for 1.5 s (the box blinks off between lines and during +// "obtained" jingles). Use A where the talk asks Yes/No or gives an item (A +// answers Yes and closes "obtained" lines; B would answer No), B elsewhere +// (B never starts a new talk). Fails with a screenshot after `timeout`. +void finish_dialogue( + VideoStream& stream, ProControllerContext& context, + Button button, const std::string& label, + Milliseconds timeout = std::chrono::seconds(40) +); + +// For a player on the tile below a door: walks north through it. If a wall +// stops the walk (the door frame, a tile off the opening), steps a tile east, +// then west, and tries again. Fails with a screenshot if no step goes in. +void enter_door_north(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); + +// For a player inside a building whose door is straight south: closes any +// talk still up and walks south out of it, stopping at the door's fade or +// the first look that is not a room, so it ends a tile or so outside. Out is +// then three standing looks in a row that are not a room: one look of a lit +// Center can miss its black edge (r259). Fails with a screenshot if five +// walks south get nowhere. +void leave_door_south(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); + +// Brightness (mean r + g + b) below which a look is the door's fade, and +// above which a look that is not a room is outside. +inline constexpr double DOOR_FADE_DARK = 120.0; +inline constexpr double DOOR_OUTSIDE_BRIGHT = 200.0; + +// What leave_door_south() has seen of the door's fade so far. +struct DoorFadeWatch{ + bool saw_dark = false; +}; +// Takes one look (its brightness, and whether indoors() read a room) and +// returns whether we are out: a dark look seen, and now a bright one that +// is not a room. +bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room); +// Whether leave_door_south()'s walk stops at this look: at the door's fade +// out (door_fade_look()), or at the first look that is not a room, so a +// held stick does not walk on outside (r261 three rows). +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp new file mode 100644 index 0000000000..eb0ec4ce02 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -0,0 +1,469 @@ +/* BDSP AutoStory - Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageDiff.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +const char* walk_stop_name(WalkStop stop){ + switch (stop){ + case WalkStop::ARRIVED: return "arrived"; + case WalkStop::DIALOG: return "dialog"; + case WalkStop::TRANSITION: return "transition"; + case WalkStop::BLOCKED: return "blocked"; + case WalkStop::LIMIT: return "limit"; + } + return "?"; +} + +double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after){ + // Small copies: only the whole-view scroll matters, not detail. + return ImageMatch::pixel_RMSD(before.scale_to(192, 108), after.scale_to(192, 108)); +} + + +namespace{ + +// The battle's text box, held for 250ms. +class BattleDialogWatcher : public DetectorToFinder{ +public: + explicit BattleDialogWatcher(Color color) + : DetectorToFinder("BattleDialogWatcher", std::chrono::milliseconds(250), color) + {} +}; + +VideoSnapshot snapshot_or_fail(VideoStream& stream, const std::string& label){ + VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No video frame.", stream); + } + return frame; +} + +// After a black screen: a wild battle if a battle screen shows up, else a transition. +bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context){ + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 100, 10, + BlackScreenWatcher::FinderType::GONE, 500ms); + const int ret = wait_until(stream, context, 10s, {{battle_menu}, {battle_dialog}, {gone}}); + if (ret == 2){ + // The battle intro also starts from black; give its first screen a moment. + BattleMenuWatcher late_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher late_dialog(COLOR_YELLOW); + return wait_until(stream, context, 2s, {{late_menu}, {late_dialog}}) >= 0; + } + return ret >= 0; +} + +// A grass encounter opens with a leaf animation, neither white nor black, and +// shows the battle's text box a few seconds later. After a change of the +// whole screen that walking cannot make, wait for the battle to show. +bool battle_follows(VideoStream& stream, ProControllerContext& context){ + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + return wait_until(stream, context, 5s, {{battle_menu}, {battle_dialog}}) >= 0; +} + +} + +// A step in tall grass that was started before the stick was released still +// finishes, and its encounter starts after the walk has ended. Watch a little +// longer, and run from it. Returns whether there was one. +bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ + VideoSnapshot before = stream.video().snapshot(); + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = wait_until(stream, context, 1200ms, {{encounter_flash}, {black}, {battle_menu}, {battle_dialog}}); + bool battle = ret == 0 || ret == 2 || ret == 3 || (ret == 1 && black_screen_was_battle(stream, context)); + if (ret < 0){ + VideoSnapshot after = stream.video().snapshot(); + battle = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE && + battle_follows(stream, context); + } + if (!battle){ + return false; + } + stream.log("[Walk] " + label + ": wild battle after the walk; running."); + escape(stream, context); + context.wait_for_all_requests(); + return true; +} +namespace{ + +// Samples the screen every 100ms for probe_walk() and stops it once the view +// has been still for 800ms. +class WalkProbeSampler : public VisualInferenceCallback{ +public: + WalkProbeSampler(WalkProbe& result, double moving_change, WallClock start) + : VisualInferenceCallback("WalkProbeSampler") + , m_result(result) + , m_moving_change(moving_change) + , m_start(start) + {} + virtual void make_overlays(VideoOverlaySet&) const override{} + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override{ + if (m_has_last && timestamp - m_last_time < 100ms){ + return false; + } + const Milliseconds at = std::chrono::duration_cast(timestamp - m_start); + ImageRGB32 small = frame.scale_to(192, 108); + if (m_has_last){ + const double change = ImageMatch::pixel_RMSD(m_last, small); + m_result.samples.push_back({at, change}); + if (change >= m_moving_change){ + if (m_result.first_move < 0ms){ + m_result.first_move = at; + } + m_result.last_move = at; + } + } + m_last = std::move(small); + m_last_time = timestamp; + m_has_last = true; + const Milliseconds still_since = m_result.last_move < 0ms ? 0ms : m_result.last_move; + return at - still_since >= 800ms; + } + +private: + WalkProbe& m_result; + double m_moving_change; + WallClock m_start; + ImageRGB32 m_last; + WallClock m_last_time; + bool m_has_last = false; +}; + +} + + +namespace{ +WalkResult walk_until_impl( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived, + bool wild, bool dpad, bool run, bool taps = false +); +} + +WalkResult walk_until( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived +){ + return walk_until_impl(stream, context, escape, x, y, limit, label, arrived, true, false, false); +} + +WalkResult walk_until_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived, + bool run +){ + return walk_until_impl(stream, context, escape, x, y, limit, label, arrived, true, true, run); +} + +WalkResult walk_tiles_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, size_t tiles, const std::string& label +){ + return walk_until_impl(stream, context, escape, x, y, 800ms * tiles, label, nullptr, true, true, false, true); +} + +WalkResult walk_until_no_wild( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +){ + // With no wild Pokemon any battle is a trainer's, and the walk stops at + // it (DIALOG) without running. + const WildBattleEscape no_wild = [](VideoStream& stream, ProControllerContext&){ + throw InternalProgramError(&stream.logger(), PA_CURRENT_FUNCTION, "walk_until_no_wild() ran from a battle."); + }; + return walk_until_impl(stream, context, no_wild, x, y, limit, label, nullptr, false, false, false); +} + +namespace{ +WalkResult walk_until_impl( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived, + bool wild, bool dpad, bool run, bool taps +){ + // A tile a tap: 160 ms held, the rest released, so each chunk is one tile. + const Milliseconds CHUNK = taps ? 800ms : 400ms; + const DpadPosition dpad_direction = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; + WalkResult result{WalkStop::LIMIT, 0ms, 0}; + size_t unchanged = 0; + // The largest change a chunk of this walk made: a step. A bump into an + // NPC measured 17.2 after steps of 63 to 65 (r250a, Floaroma's old + // woman), over the fixed line of 16, so "still" is also anything under + // 40% of the walk's own steps. + double largest = 0; + + context.wait_for_all_requests(); + VideoSnapshot before = snapshot_or_fail(stream, label); + auto finish = [&](WalkStop stop){ + result.stop = stop; + if (wild && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && + run_from_late_encounter(stream, context, escape, label) + ){ + result.wild_battles++; + } + stream.log("[Walk] " + label + ": " + walk_stop_name(stop) + " after " + + std::to_string(result.walked.count()) + "ms, " + + std::to_string(result.wild_battles) + " wild battle(s)."); + return result; + }; + + while (true){ + if (arrived && arrived(*before.frame)){ + return finish(WalkStop::ARRIVED); + } + if (result.walked >= limit){ + return finish(WalkStop::LIMIT); + } + const Milliseconds chunk = std::min(CHUNK, limit - result.walked); + + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + ShortDialogWatcher dialog(COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [&](ProControllerContext& context){ + if (taps){ + pbf_press_dpad(context, dpad_direction, 160ms, chunk - 160ms); + }else if (dpad && run){ + ssf_press_button(context, BUTTON_B, 0ms, chunk, 0ms); + ssf_press_dpad(context, dpad_direction, chunk, chunk, 0ms); + }else if (dpad){ + pbf_press_dpad(context, dpad_direction, chunk, 0ms); + }else{ + pbf_move_left_joystick(context, {x, y}, chunk, 0ms); + } + }, + {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} + ); + result.walked += chunk; + + bool battle = ret == 0 || ret == 2 || ret == 4; + if (ret == 1){ + battle = black_screen_was_battle(stream, context); + if (!battle){ + return finish(WalkStop::TRANSITION); + } + } + if (battle && !wild){ + stream.log("[Walk] " + label + ": a battle started; a trainer's here."); + return finish(WalkStop::DIALOG); + } + if (battle){ + result.wild_battles++; + stream.log("[Walk] " + label + ": wild battle; running."); + escape(stream, context); + context.wait_for_all_requests(); + before = snapshot_or_fail(stream, label); + unchanged = 0; + continue; + } + if (ret == 3){ + return finish(WalkStop::DIALOG); + } + + context.wait_for_all_requests(); + VideoSnapshot after = snapshot_or_fail(stream, label); + const double change = walk_screen_change(*before.frame, *after.frame); + stream.log("[Walk] " + label + ": " + std::to_string(result.walked.count()) + + "ms, screen change " + std::to_string(change) + "."); + if (change > WALK_SCENE_CHANGE && !wild && battle_follows(stream, context)){ + stream.log("[Walk] " + label + ": a battle started; a trainer's here."); + return finish(WalkStop::DIALOG); + } + if (change > WALK_SCENE_CHANGE && battle_follows(stream, context)){ + result.wild_battles++; + stream.log("[Walk] " + label + ": wild battle (leaf opening); running."); + escape(stream, context); + context.wait_for_all_requests(); + before = snapshot_or_fail(stream, label); + unchanged = 0; + continue; + } + // Two still chunks in a row, so one dropped frame does not stop us. A + // frame exactly the same is a stalled video or a game hitch, not a wall: + // standing at a wall still animates. + if (change == 0){ + before = std::move(after); + continue; + } + largest = std::max(largest, change); + const double line = taps ? std::max(WALK_TAP_BLOCKED_CHANGE, 0.4 * largest) : WALK_BLOCKED_CHANGE; + const bool still = change < line; + if (!still){ + result.moved_chunks++; + } + unchanged = still ? unchanged + 1 : 0; + if (unchanged >= 2){ + return finish(WalkStop::BLOCKED); + } + before = std::move(after); + } +} +} + +WalkResult walk_for( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds duration, const std::string& label +){ + WalkResult result{WalkStop::LIMIT, duration, 0}; + context.wait_for_all_requests(); + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + ShortDialogWatcher dialog(COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [&](ProControllerContext& context){ + pbf_move_left_joystick(context, {x, y}, duration, 0ms); + }, + {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} + ); + bool battle = ret == 0 || ret == 2 || ret == 4; + if (ret == 1){ + battle = black_screen_was_battle(stream, context); + if (!battle){ + result.stop = WalkStop::TRANSITION; + } + } + if (battle){ + result.wild_battles = 1; + stream.log("[Walk] " + label + ": wild battle; running."); + escape(stream, context); + context.wait_for_all_requests(); + } + if (ret == 3){ + result.stop = WalkStop::DIALOG; + } + // An encounter after the hold leaves us where the walk ended, so it does + // not count as cutting the walk short. + if (ret < 0){ + run_from_late_encounter(stream, context, escape, label); + } + stream.log("[Walk] " + label + ": " + walk_stop_name(result.stop) + " after a " + + std::to_string(duration.count()) + "ms hold, " + std::to_string(result.wild_battles) + " wild battle(s)."); + return result; +} + +WalkResult walk_to_talk( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label +){ + WalkResult total{WalkStop::LIMIT, 0ms, 0}; + while (total.walked < limit){ + const WalkResult step = walk_until(stream, context, escape, x, y, + std::min(400ms, limit - total.walked), label); + total.walked += step.walked; + total.wild_battles += step.wild_battles; + if (step.stop == WalkStop::DIALOG || step.stop == WalkStop::TRANSITION){ + total.stop = step.stop; + return total; + } + ShortDialogWatcher dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [](ProControllerContext& context){ + pbf_press_button(context, BUTTON_A, 80ms, 720ms); + }, + {{dialog}} + ); + if (ret == 0){ + total.stop = WalkStop::DIALOG; + stream.log("[Walk] " + label + ": talking after " + std::to_string(total.walked.count()) + "ms."); + return total; + } + } + stream.log("[Walk] " + label + ": nobody to talk to within " + std::to_string(limit.count()) + "ms."); + return total; +} + +WalkProbe probe_walk( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, + double moving_change, const std::string& label +){ + WalkProbe result; + context.wait_for_all_requests(); + WalkProbeSampler sampler(result, moving_change, current_time()); + run_until( + stream, context, + [&](ProControllerContext& context){ + pbf_move_left_joystick(context, {x, y}, limit, 0ms); + }, + {{sampler}} + ); + context.wait_for_all_requests(); + stream.log("[Walk] " + label + ": probe moved from " + std::to_string(result.first_move.count()) + + "ms to " + std::to_string(result.last_move.count()) + "ms."); + return result; +} + +WalkResult walk_to_wall( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label +){ + const WalkResult result = walk_until(stream, context, escape, x, y, limit, label); + if (result.stop == WalkStop::DIALOG || result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": " + walk_stop_name(result.stop) + " instead of a wall.", stream); + } + if (result.stop == WalkStop::LIMIT){ + stream.log("[Walk] " + label + ": held for the whole limit without seeing the stop; taking the wall as reached."); + } + return result; +} + +WalkResult walk_expecting( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + WalkStop expected, + const std::function& arrived +){ + const WalkResult result = walk_until(stream, context, escape, x, y, limit, label, arrived); + if (result.stop != expected){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": expected " + walk_stop_name(expected) + ", got " + + walk_stop_name(result.stop) + ".", stream); + } + return result; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h new file mode 100644 index 0000000000..15622b26f4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -0,0 +1,172 @@ +/* BDSP AutoStory - Walk + * + * From: https://github.com/PokemonAutomation/ + * + * Walking that reads the screen instead of trusting a duration: hold a + * direction and stop on whatever the screen says happened. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walk_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Walk_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Runs from a wild battle that has interrupted a walk, and returns once the +// player stands on the overworld again. The walker only sees that a battle +// started; how to leave it is the program's battle code, passed in. +using WildBattleEscape = std::function; + +enum class WalkStop{ + ARRIVED, // `arrived` matched. + DIALOG, // A dialogue box opened: an NPC or a trainer stopped us. + TRANSITION, // The screen went black and came back without a battle: a door or a new area. + BLOCKED, // The screen stopped changing: we are walking into a wall. + LIMIT, // Walked `limit` without any of the above. +}; +const char* walk_stop_name(WalkStop stop); + +struct WalkResult{ + WalkStop stop; + Milliseconds walked; + size_t wild_battles; + // Chunks whose screen change passed the blocked threshold: on a tap + // walk, the taps that moved a tile. A trainer's freeze and a bump do + // not count (r248a: "4 taps before the battle" were 2 steps and 2 + // taps into Taylor's freeze). + size_t moved_chunks = 0; +}; + +// Holds the left stick at (x, y) until one of the WalkStop events. Wild battles on +// the way are run from, and walking resumes. Does not throw for any WalkStop; the +// caller decides which ones are expected. +WalkResult walk_until( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived = nullptr +); + +// walk_until() on the D-pad, for a straight walk along a row or a column +// ((x, y) is one of the four directions). The D-pad moves whole tiles and +// keeps to the row: the stick drifts off it, catches tree corners and slides +// a row on stairs (Route 203, r226a and r227b; by hand the D-pad went +// through the same places). With `run`, B is held too: twice as fast, and +// as tile-true. +WalkResult walk_until_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + const std::function& arrived = nullptr, + bool run = false +); + +// A set number of whole tiles on the D-pad, a tap a tile (a tap moves +// exactly one tile; a stick hold timed for n tiles is a tile or two off), +// with the same watch for wild battles and dialogue as walk_until(). Ends +// LIMIT after `tiles` taps, or BLOCKED when two taps in a row move nothing. +WalkResult walk_tiles_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, size_t tiles, const std::string& label +); + +// walk_until() where there are no wild Pokemon (a gym, a building): any +// battle that starts is a trainer's, so the walk stops with DIALOG, left in +// the battle for the caller to fight, instead of trying to run from it +// (r206d tried to run from the second Oreburgh Gym trainer). +WalkResult walk_until_no_wild( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, const std::string& label +); + +// Holds the left stick at (x, y) for `duration` in one go, for a walk of a +// set length: walk_until() checks the screen between short holds, and each +// break can cost a step. Stops early for a dialog or a transition. A wild +// battle is run from and ends the walk with `wild_battles` 1, at a place the +// caller cannot know. +WalkResult walk_for( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds duration, const std::string& label +); + +// walk_until() that requires `expected`. Fails with the screenshot otherwise. +WalkResult walk_expecting( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label, + WalkStop expected, + const std::function& arrived = nullptr +); + +// After a move that may have walked onto an encounter tile (a tap in a cave +// or in tall grass): waits briefly for a wild battle and runs from it. +// Returns whether there was one; we may then be off the place we expected. +bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); + +// walk_until() toward a wall the route was measured to end at. Seeing the +// stop only ends the hold early: holding on for the whole `limit` also +// counts as there, as people or animations nearby can keep the screen +// changing at a wall (s03a1: a hiker and a child by the Mart fence; s03c1: +// Roark's pickaxe; r162a: Jubilife's TV). A wrong place is caught by the +// next gate. Fails on a dialog or a transition: we are not where we think. +WalkResult walk_to_wall( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label +); + +// Walks toward someone to talk to: after each short stretch, presses A and stops +// once a dialogue opens. Use it where walking into the person does not read as +// BLOCKED, e.g. in tall grass, whose sway keeps the screen changing. +WalkResult walk_to_talk( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + double x, double y, Milliseconds limit, const std::string& label +); + +// How much the screen changed between two frames, 0 for identical. Walking scrolls +// the whole view: 16 to 85 per 400ms on Route 202, against about 6 for running in +// place against trees. +double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after); +constexpr double WALK_BLOCKED_CHANGE = 10.0; +// For whole-tile D-pad taps: a tap that moves changes the screen by 21 to 50 +// (r244a: 40 per tap along Route 204's row 27); a tap into a wall or an NPC +// bumps for 10 to 13.4, and an NPC walking past does the same, so 10 counted +// bumps as steps (r244a, 2026-10-31). +constexpr double WALK_TAP_BLOCKED_CHANGE = 16.0; +// More than walking changes in 400ms: a new scene, such as a battle opening. +constexpr double WALK_SCENE_CHANGE = 120.0; + + +// One straight walk timed finely, to check a map against the game: holds the +// stick at (x, y) and samples the screen every 100ms until the view has been +// still for 800ms or `limit` runs out. Times count from the start of the hold. +// A sample moved if it changed by at least `moving_change`: still is 4 to 7, +// walking 16 to 85. Handles no battles or dialogs: use it on open ground, +// away from tall grass. +struct WalkProbe{ + Milliseconds first_move{-1}; // the first sample that moved; -1 if none did + Milliseconds last_move{-1}; // the last sample that moved; -1 if none did + struct Sample{ + Milliseconds at; + double change; // walk_screen_change() since the previous sample + }; + std::vector samples; +}; +WalkProbe probe_walk( + VideoStream& stream, ProControllerContext& context, + double x, double y, Milliseconds limit, + double moving_change, const std::string& label +); + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index d0c16d6bac..30d16298fc 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1574,8 +1574,20 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.cpp Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.cpp @@ -1602,6 +1614,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Inference/PokemonBDSP_MapDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_MarkFinder.cpp Source/PokemonBDSP/Inference/PokemonBDSP_MarkFinder.h + Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp + Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.cpp Source/PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h Source/PokemonBDSP/Inference/PokemonBDSP_PokeballSpriteMatcher.cpp @@ -1652,6 +1666,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/PokemonBDSP_Panels.h Source/PokemonBDSP/PokemonBDSP_Settings.cpp Source/PokemonBDSP/PokemonBDSP_Settings.h + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp + Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h Source/PokemonBDSP/PokemonBDSP_Tests.cpp Source/PokemonBDSP/PokemonBDSP_Tests.h Source/PokemonBDSP/Programs/Eggs/PokemonBDSP_EggAutonomous.cpp From de7c108c52d2ecaaf2aaa821f28194ded2616ab0 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 13:39:41 -0800 Subject: [PATCH 02/39] BDSP navigation: the walker plans on the story world The walker takes a StoryWorld where it took a Pokemon::MapSet or TileGrid, and the planner's types from PokemonBDSP. Its copy of the world uncovers the tiles to avoid and blocks what it finds in the way, as it set them UNKNOWN and BLOCKED before. Nothing else changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 34 +++++++++---------- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 21 ++++++------ 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index 8b32093da4..c8382de696 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -58,7 +58,7 @@ void stick_for(Direction direction, double& x, double& y){ // World tiles as tiles of the named maps, for the shared route log // (Pokemon_RouteLog.h); a tile no map holds is a "world" tile. -std::vector route_tiles(const MapSet& maps, const std::vector& tiles){ +std::vector route_tiles(const StoryWorld& maps, const std::vector& tiles){ std::vector out; for (const TilePos& pos : tiles){ const std::optional local = maps.to_local(pos); @@ -67,7 +67,7 @@ std::vector route_tiles(const MapSet& maps, const std::vector& tiles){ +std::string describe(const StoryWorld& maps, const std::vector& tiles){ return Pokemon::describe(route_tiles(maps, tiles)); } @@ -76,11 +76,11 @@ RouteLogLeg route_log_leg(const TileLeg& leg){ .end = leg.ends_at_wall ? RouteLegEnd::WALL : RouteLegEnd::OPEN}; } -PlannedRouteLeg planned_route_leg(const MapSet& maps, const PlannedLeg& leg){ +PlannedRouteLeg planned_route_leg(const StoryWorld& maps, const PlannedLeg& leg){ return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; } -void log_plan(VideoStream& stream, const MapSet& maps, const std::string& label, const std::vector& from, const std::vector& legs){ +void log_plan(VideoStream& stream, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ std::vector logged; for (const PlannedLeg& leg : legs){ logged.push_back(planned_route_leg(maps, leg)); @@ -91,7 +91,7 @@ void log_plan(VideoStream& stream, const MapSet& maps, const std::string& label, // Logs a leg that ended on its planned tiles, and how that is known: an // open leg counts its taps through a wild battle, so its end is assumed; a // leg to a wall is walked again to its wall. -void log_leg_done(VideoStream& stream, const MapSet& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ +void log_leg_done(VideoStream& stream, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); @@ -99,7 +99,7 @@ void log_leg_done(VideoStream& stream, const MapSet& maps, const std::string& la // The tiles the player passes when walking `tiles` tiles from `from`, the // first one after `from` first. Stops early where the player is stopped. -std::vector leg_path(const TileGrid& grid, TilePos from, Direction direction, size_t tiles){ +std::vector leg_path(const StoryWorld& grid, TilePos from, Direction direction, size_t tiles){ std::vector path; size_t travelled = 0; while (travelled < tiles){ @@ -139,7 +139,7 @@ bool holding_against_wall(VideoStream& stream, ProControllerContext& context, do } // The map all of `tiles` are on, or empty if they are on several or none. -std::string map_of(const MapSet& maps, const std::vector& tiles){ +std::string map_of(const StoryWorld& maps, const std::vector& tiles){ std::string name; for (const TilePos& pos : tiles){ const std::optional tile = maps.to_local(pos); @@ -154,9 +154,9 @@ std::string map_of(const MapSet& maps, const std::vector& tiles){ } -bool route_wall_readable(const TileGrid& grid, const std::vector& after){ +bool route_wall_readable(const StoryWorld& grid, const std::vector& after){ return std::none_of(after.begin(), after.end(), [&](const TilePos& pos){ - return grid.at(pos) == TileType::TALL_GRASS; + return grid.tile(pos) == TileKind::TALL_GRASS; }); } @@ -245,7 +245,7 @@ bool talk_with_nudges( std::vector walk_route( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const MapSet& maps, + const StoryWorld& maps, std::vector from, const std::function& goal, std::vector& trainers, @@ -258,10 +258,10 @@ std::vector walk_route( // avoid are ground the game lets us walk on (a trainer's line of sight), // so they are off the map, not walls: a leg must not end "at" one, or // the walk would run on through it. - TileGrid grid = maps.world(); + StoryWorld grid = maps; for (const TilePos& pos : avoid){ if (std::find(from.begin(), from.end(), pos) == from.end()){ - grid.set(pos, TileType::UNKNOWN); + grid.uncover(pos); } } // Which way the player faces, once a leg has set it. @@ -415,7 +415,7 @@ std::vector walk_route( } if (stopped.size() == 1){ const TilePos blocker = stopped[0].next(leg.direction); - grid.set(blocker, TileType::BLOCKED); + grid.block(blocker); stream.log("[Route] " + leg_label + ": something the map lacks is at " + maps.describe(blocker) + "; marked it.", COLOR_ORANGE); } @@ -531,7 +531,7 @@ std::vector walk_route( stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); if (stopped.size() == 1){ const TilePos blocker = stopped[0].next(leg.direction); - grid.set(blocker, TileType::BLOCKED); + grid.block(blocker); stream.log("[Route] " + leg_label + ": something the map lacks is at " + maps.describe(blocker) + "; marked it.", COLOR_ORANGE); } @@ -723,7 +723,7 @@ std::vector walk_route( std::vector walk_waypoints( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const MapSet& maps, + const StoryWorld& maps, std::vector from, const std::vector& waypoints, std::vector& trainers, @@ -744,7 +744,7 @@ std::vector walk_waypoints( void talk_to_npc( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const MapSet& maps, + const StoryWorld& maps, std::vector& at, const TilePos& npc, const std::vector& sides, @@ -754,7 +754,7 @@ void talk_to_npc( ){ for (Direction side : sides){ const TilePos stand = npc.next(side); - if (!maps.world().standable(stand)){ + if (!maps.standable(stand)){ continue; } at = walk_route(stream, context, escape, maps, at, [&](const TilePos& pos){ return pos == stand; }, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index b7057b6303..995f341818 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -19,7 +19,8 @@ #include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "CommonFramework/Tools/VideoStream.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" -#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP_AutoStory_Walk.h" @@ -89,7 +90,7 @@ constexpr Milliseconds ROUTE_NUDGE_TAP = Milliseconds(50); // Whether a walk to the wall that ends on any of `after` can tell it has // stopped: not in tall grass, whose sway keeps the screen changing at a // wall. Elsewhere, a walk to the wall that stops confirms where it ended. -bool route_wall_readable(const Pokemon::TileGrid& grid, const std::vector& after); +bool route_wall_readable(const StoryWorld& grid, const std::vector& after); // Shows what the route walk is doing in the log and the video overlay. void route_status(VideoStream& stream, const std::string& text); @@ -111,14 +112,14 @@ using RouteTrainerFight = std::function; // after "arriving" on Route 202). std::vector walk_route( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const Pokemon::MapSet& maps, + const StoryWorld& maps, std::vector from, const std::function& goal, - std::vector& trainers, + std::vector& trainers, const RouteTrainerFight& fight, const std::string& label, const std::vector& avoid = {}, - const Pokemon::RobustPlanCosts& costs = route_plan_costs() + const RobustPlanCosts& costs = route_plan_costs() ); // Walks `from` to each of `waypoints` in turn, with walk_route() for every @@ -127,13 +128,13 @@ std::vector walk_route( // trainers and blocks on each stretch (Route 204 North, 2026-10-31). std::vector walk_waypoints( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const Pokemon::MapSet& maps, + const StoryWorld& maps, std::vector from, const std::vector& waypoints, - std::vector& trainers, + std::vector& trainers, const RouteTrainerFight& fight, const std::string& label, - const Pokemon::RobustPlanCosts& costs = waypoint_plan_costs() + const RobustPlanCosts& costs = waypoint_plan_costs() ); // Walks next to the NPC on world tile `npc`, faces them and presses A, trying @@ -143,11 +144,11 @@ std::vector walk_waypoints( // or from a tile a row off, talks to nobody, so each side is tried in turn. void talk_to_npc( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const Pokemon::MapSet& maps, + const StoryWorld& maps, std::vector& at, const Pokemon::TilePos& npc, const std::vector& sides, - std::vector& trainers, + std::vector& trainers, const RouteTrainerFight& fight, const std::string& label ); From 090783ff36b77480405b1596311f6044d8371a1e Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 13:52:15 -0800 Subject: [PATCH 03/39] BDSP navigation: the walker calls the planner's slide_to_wall() Follows the rename in #127; the walker's own walk_to_wall() is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index c8382de696..bb3a012840 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -342,7 +342,7 @@ std::vector walk_route( // the time walked, put it on our map and plan again from there. size_t nearest = SIZE_MAX; for (const TilePos& pos : from){ - nearest = std::min(nearest, walk_to_wall(grid, pos, leg.direction).tiles); + nearest = std::min(nearest, slide_to_wall(grid, pos, leg.direction).tiles); } auto tiles_moved = [&](const WalkResult& walk){ const Milliseconds moving = walk.walked - ROUTE_START_DELAY - From d5046f2663f6caa3fcd0db56bb2f870af1309a41 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:41:24 -0800 Subject: [PATCH 04/39] BDSP navigation: the walker calls the story world's free functions Follows #127: standable(), step() and describe() take the world first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index bb3a012840..7cb39ac94b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -103,7 +103,7 @@ std::vector leg_path(const StoryWorld& grid, TilePos from, Direction di std::vector path; size_t travelled = 0; while (travelled < tiles){ - const std::optional next = grid.step(from, direction); + const std::optional next = step(grid, from, direction); if (!next){ break; } @@ -417,7 +417,7 @@ std::vector walk_route( const TilePos blocker = stopped[0].next(leg.direction); grid.block(blocker); stream.log("[Route] " + leg_label + ": something the map lacks is at " + - maps.describe(blocker) + "; marked it.", COLOR_ORANGE); + describe(maps, blocker) + "; marked it.", COLOR_ORANGE); } stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), "stopped early after about " + std::to_string(walked_tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); @@ -533,7 +533,7 @@ std::vector walk_route( const TilePos blocker = stopped[0].next(leg.direction); grid.block(blocker); stream.log("[Route] " + leg_label + ": something the map lacks is at " + - maps.describe(blocker) + "; marked it.", COLOR_ORANGE); + describe(maps, blocker) + "; marked it.", COLOR_ORANGE); } stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), "blocked after " + std::to_string(moved) + " of " + std::to_string(leg.tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); @@ -736,7 +736,7 @@ std::vector walk_waypoints( from = walk_route(stream, context, escape, maps, from, [&](const TilePos& pos){ return pos == goal; }, trainers, fight, - label + " (" + std::to_string(i + 1) + "/" + std::to_string(waypoints.size()) + ", to " + maps.describe(goal) + ")", + label + " (" + std::to_string(i + 1) + "/" + std::to_string(waypoints.size()) + ", to " + describe(maps, goal) + ")", {}, costs); } return from; @@ -754,7 +754,7 @@ void talk_to_npc( ){ for (Direction side : sides){ const TilePos stand = npc.next(side); - if (!maps.standable(stand)){ + if (!standable(maps, stand)){ continue; } at = walk_route(stream, context, escape, maps, at, [&](const TilePos& pos){ return pos == stand; }, @@ -769,13 +769,13 @@ void talk_to_npc( context.wait_for_all_requests(); ShortDialogWatcher dialog(COLOR_CYAN); if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, label + ": talking from " + maps.describe(stand) + "."); + route_status(stream, label + ": talking from " + describe(maps, stand) + "."); return; } - route_status(stream, label + ": nobody answered from " + maps.describe(stand) + "; trying another side."); + route_status(stream, label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); } OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": no side of " + maps.describe(npc) + " started a dialogue.", stream); + label + ": no side of " + describe(maps, npc) + " started a dialogue.", stream); } void finish_dialogue( From 6daf60615b1c16999a8c80f42ef9f92d7591c204 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:45:05 -0800 Subject: [PATCH 05/39] BDSP walker: formatting the code-health check asks for namespace X{ with no space, and if bodies on their own lines. No code changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_OverworldDetector.cpp | 6 +++--- .../PokemonBDSP_AutoStory_OverworldDetector.h | 6 +++--- .../Utils/PokemonBDSP_AutoStory_Indoors.cpp | 16 ++++++++++++---- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp index 3e4e0f41ca..d9e759ec3a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp @@ -14,9 +14,9 @@ using namespace std::chrono_literals; -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ // --------------------------------------------------------------------------- diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h index 4741325711..cf3d1ea2b6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h @@ -31,9 +31,9 @@ #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" -namespace PokemonAutomation { -namespace NintendoSwitch { -namespace PokemonBDSP { +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ // Static (single-frame) version — use when you already have a frame. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp index 8819372066..12baf6710f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp @@ -41,10 +41,18 @@ bool indoors(const ImageViewRGB32& screen){ count++; warm += pixel.red() > 150 && pixel.red() > pixel.blue() + 70 && pixel.green() > pixel.blue() + 20; const bool dark = pixel.red() + pixel.green() + pixel.blue() < 60; - if (y < strip_h){ add_pixel(top, dark); } - if (y >= height - strip_h){ add_pixel(bottom, dark); } - if (x < strip_w){ add_pixel(left, dark); } - if (x >= width - strip_w){ add_pixel(right, dark); } + if (y < strip_h){ + add_pixel(top, dark); + } + if (y >= height - strip_h){ + add_pixel(bottom, dark); + } + if (x < strip_w){ + add_pixel(left, dark); + } + if (x >= width - strip_w){ + add_pixel(right, dark); + } } } if (count == 0 || warm * 4 <= count){ From 91dfd2e39fd4463c02eeaf10a97b1105e8c9e1a4 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:45:20 -0800 Subject: [PATCH 06/39] BDSP walker: the story language behind a getter and a setter Code health rejects the non-const extern g_story_language. The program sets the language once before the story runs, so it now calls set_story_language(), and readers call story_language(). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_Location.cpp | 17 +++++++++++++++-- .../Utils/PokemonBDSP_AutoStory_Location.h | 14 ++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp index ecf90056c7..6b63b5cf71 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -58,10 +58,23 @@ std::string normalize_location_name(const std::string& text){ return out; } -Language g_story_language = Language::English; +namespace{ +Language& story_language_setting(){ + static Language language = Language::English; + return language; +} +} + +Language story_language(){ + return story_language_setting(); +} + +void set_story_language(Language language){ + story_language_setting() = language; +} bool story_reads_text(){ - return story_reads_text(g_story_language); + return story_reads_text(story_language()); } bool story_reads_text(Language language){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h index 4294ede6eb..24bdc2ecc0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h @@ -24,12 +24,14 @@ namespace PokemonBDSP{ std::string normalize_location_name(const std::string& text); // The game's language, for the reads that still use text (item names, the -// location banner, a sign's or talk's first box), set from the program's -// options (StoryRunOptions). story_reads_text() says whether those reads -// have a model and dictionary for it (English only, so far); when not, -// callers use language-free checks: the banner's box showing, a sign's box -// opening, the first row of a Bag pocket. -extern Language g_story_language; +// location banner, a sign's or talk's first box), set once from the +// program's options (StoryRunOptions) before the story runs; English until +// then. story_reads_text() says whether those reads have a model and +// dictionary for it (English only, so far); when not, callers use +// language-free checks: the banner's box showing, a sign's box opening, the +// first row of a Bag pocket. +Language story_language(); +void set_story_language(Language language); bool story_reads_text(); bool story_reads_text(Language language); From 2bb2fb97f89cf72bd37e813a2fcfd8ac4640b668 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:47:51 -0800 Subject: [PATCH 07/39] BDSP walker: walks take a JoystickPosition, and options instead of bools Code health: walk_until() took 8 parameters, walk_until_dpad() 9 with a defaulted bool, walk_expecting() 9, and walk_until_impl() 12 with four bools over 126 lines. Every walk now takes the stick as one JoystickPosition. walk_until_dpad() takes DpadWalkOptions (arrived, and a DpadPace for B held) and walk_expecting() a WalkExpectation (the stop, and arrived). The walk loop is a WalkUntil class in the .cpp, with what it presses and how battles end it as enums. Behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 24 +- .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 378 +++++++++++------- .../Utils/PokemonBDSP_AutoStory_Walk.h | 59 ++- 3 files changed, 284 insertions(+), 177 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index 7cb39ac94b..129795d797 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -327,7 +327,7 @@ std::vector walk_route( // Long enough to reach the wall from the farthest tile, with room // for a slow start; past the wall the player stands still. const Milliseconds limit = ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; - result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); // A wild battle may end the walk early. Walking to a wall again // ends in the same place, so finish the leg. const bool interrupted = result.wild_battles > 0; @@ -335,7 +335,7 @@ std::vector walk_route( (result.stop == WalkStop::LIMIT || result.stop == WalkStop::BLOCKED); again++ ){ stream.log("[Route] " + leg_label + ": a wild battle on the way; walking to the wall again."); - result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); } // A wall well before the nearest one the map has: something the // map does not know is in the way. Work out where we stopped from @@ -366,7 +366,7 @@ std::vector walk_route( pbf_press_dpad(context, dpad, 160ms, 640ms); } context.wait_for_all_requests(); - result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); walked_tiles += tiles_moved(result); } // Someone walking about may stand in the way for a moment: wait @@ -378,7 +378,7 @@ std::vector walk_route( " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); pbf_wait(context, 2500ms); context.wait_for_all_requests(); - result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label, watch_banner, true); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); walked_tiles += tiles_moved(result); } // Any stop short of the wall's distance: someone who stays put @@ -433,7 +433,7 @@ std::vector walk_route( // grass a stop reads, so walk on: a player still moving is // not where the plan has him. if (!interrupted && result.stop == WalkStop::LIMIT && route_wall_readable(grid, planned.after)){ - const WalkResult check = walk_until_dpad(stream, context, escape, x, y, 2000ms, leg_label + ": at the wall?", nullptr, true); + const WalkResult check = walk_until_dpad(stream, context, escape, {x, y}, 2000ms, leg_label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); if (check.stop == WalkStop::DIALOG){ // A trainer who saw us froze the walk (the "!") until // its time ran out, then walked over (r241a: Aroma Lady @@ -446,7 +446,7 @@ std::vector walk_route( // guess the tile: a leg to a wall ends at its wall however // far it got. Walk on to it. stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - result = walk_until_dpad(stream, context, escape, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); } if (check.stop != WalkStop::BLOCKED && !(check.stop == WalkStop::LIMIT && holding_against_wall(stream, context, x, y, leg_label + ": at the wall?")) @@ -478,7 +478,7 @@ std::vector walk_route( } } if (leg.tiles > turn_moved){ - result = walk_tiles_dpad(stream, context, escape, x, y, leg.tiles - turn_moved, leg_label); + result = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - turn_moved, leg_label); }else{ result = WalkResult{WalkStop::LIMIT, 0ms, 0}; } @@ -505,7 +505,7 @@ std::vector walk_route( std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); pbf_wait(context, 2500ms); context.wait_for_all_requests(); - const WalkResult rest = walk_tiles_dpad(stream, context, escape, x, y, leg.tiles - moved, leg_label + ": the rest"); + const WalkResult rest = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - moved, leg_label + ": the rest"); moved = std::min(leg.tiles, moved + taps_moved(rest)); result.stop = rest.stop; result.walked += rest.walked; @@ -627,7 +627,7 @@ std::vector walk_route( const Milliseconds limit = ROUTE_START_DELAY + ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(stream, context, escape, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); from = planned.after; log_leg_done(stream, maps, label, planned, 0); continue; @@ -669,7 +669,7 @@ std::vector walk_route( const Milliseconds limit = ROUTE_START_DELAY + ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(stream, context, escape, x, y, limit, leg_label + ": on to the wall", watch_banner, true); + walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); from = planned.after; log_leg_done(stream, maps, label, planned, 0); continue; @@ -829,7 +829,7 @@ void enter_door_north(VideoStream& stream, ProControllerContext& context, const // one more. const Direction steps[] = {Direction::EAST, Direction::WEST, Direction::WEST}; for (size_t attempt = 0; attempt <= 3; attempt++){ - const WalkResult walk = walk_until(stream, context, escape, 0, 1, 2500ms, label); + const WalkResult walk = walk_until(stream, context, escape, {0, 1}, 2500ms, label); if (walk.stop == WalkStop::TRANSITION){ context.wait_for(1500ms); return; @@ -900,7 +900,7 @@ void leave_door_south(VideoStream& stream, ProControllerContext& context, const }; bool out = false; for (size_t tries = 0; tries < 5 && !out; tries++){ - const WalkResult walk = walk_until(stream, context, escape, 0, -1, 6000ms, label, out_of_door); + const WalkResult walk = walk_until(stream, context, escape, {0, -1}, 6000ms, label, out_of_door); if (walk.stop == WalkStop::DIALOG){ pbf_mash_button(context, BUTTON_B, 2000ms); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index eb0ec4ce02..5cdc90dc3e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -163,182 +163,273 @@ class WalkProbeSampler : public VisualInferenceCallback{ namespace{ -WalkResult walk_until_impl( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - const std::function& arrived, - bool wild, bool dpad, bool run, bool taps = false -); -} - -WalkResult walk_until( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - const std::function& arrived -){ - return walk_until_impl(stream, context, escape, x, y, limit, label, arrived, true, false, false); -} - -WalkResult walk_until_dpad( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - const std::function& arrived, - bool run -){ - return walk_until_impl(stream, context, escape, x, y, limit, label, arrived, true, true, run); -} -WalkResult walk_tiles_dpad( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, size_t tiles, const std::string& label -){ - return walk_until_impl(stream, context, escape, x, y, 800ms * tiles, label, nullptr, true, true, false, true); -} +// What a walk presses. +enum class WalkInput{ + STICK, + DPAD, + DPAD_RUN, // the D-pad with B held + TAPS, // a D-pad tap a tile, 800 ms each +}; +// What a battle that starts on the way is. +enum class WildPolicy{ + RUN_FROM, // a wild battle: run, and walk on + STOP_AT, // no wild Pokemon here: a trainer's, so the walk stops with DIALOG +}; -WalkResult walk_until_no_wild( - VideoStream& stream, ProControllerContext& context, - double x, double y, Milliseconds limit, const std::string& label -){ - // With no wild Pokemon any battle is a trainer's, and the walk stops at - // it (DIALOG) without running. - const WildBattleEscape no_wild = [](VideoStream& stream, ProControllerContext&){ - throw InternalProgramError(&stream.logger(), PA_CURRENT_FUNCTION, "walk_until_no_wild() ran from a battle."); - }; - return walk_until_impl(stream, context, no_wild, x, y, limit, label, nullptr, false, false, false); -} +struct WalkSpec{ + JoystickPosition stick; + Milliseconds limit = 0ms; + std::string label; + WalkArrived arrived = nullptr; + WalkInput input = WalkInput::STICK; + WildPolicy wild = WildPolicy::RUN_FROM; +}; -namespace{ -WalkResult walk_until_impl( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - const std::function& arrived, - bool wild, bool dpad, bool run, bool taps -){ - // A tile a tap: 160 ms held, the rest released, so each chunk is one tile. - const Milliseconds CHUNK = taps ? 800ms : 400ms; - const DpadPosition dpad_direction = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; - WalkResult result{WalkStop::LIMIT, 0ms, 0}; - size_t unchanged = 0; - // The largest change a chunk of this walk made: a step. A bump into an - // NPC measured 17.2 after steps of 63 to 65 (r250a, Floaroma's old - // woman), over the fixed line of 16, so "still" is also anything under - // 40% of the walk's own steps. - double largest = 0; +// What a chunk of the walk decided: stop with `stop`, walk the next chunk, +// or (after the watchers) look at how the screen changed. +enum class ChunkNext{ + STOP, + NEXT_CHUNK, + LOOK, +}; +struct ChunkVerdict{ + ChunkNext next = ChunkNext::LOOK; + WalkStop stop = WalkStop::LIMIT; +}; - context.wait_for_all_requests(); - VideoSnapshot before = snapshot_or_fail(stream, label); - auto finish = [&](WalkStop stop){ - result.stop = stop; - if (wild && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && - run_from_late_encounter(stream, context, escape, label) - ){ - result.wild_battles++; - } - stream.log("[Walk] " + label + ": " + walk_stop_name(stop) + " after " + - std::to_string(result.walked.count()) + "ms, " + - std::to_string(result.wild_battles) + " wild battle(s)."); - return result; - }; +// walk_until() and its variants: holds the input a chunk at a time, and +// after each chunk reads the watchers, then the screen. +class WalkUntil{ +public: + WalkUntil(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const WalkSpec& spec) + : m_stream(stream) + , m_context(context) + , m_escape(escape) + , m_spec(spec) + , m_dpad(spec.stick.x < 0 ? DPAD_LEFT : spec.stick.x > 0 ? DPAD_RIGHT : spec.stick.y > 0 ? DPAD_UP : DPAD_DOWN) + {} - while (true){ - if (arrived && arrived(*before.frame)){ - return finish(WalkStop::ARRIVED); - } - if (result.walked >= limit){ - return finish(WalkStop::LIMIT); + WalkResult run(){ + // A tile a tap: 160 ms held, the rest released, so each chunk is one tile. + const Milliseconds CHUNK = m_spec.input == WalkInput::TAPS ? 800ms : 400ms; + m_context.wait_for_all_requests(); + m_before = snapshot_or_fail(m_stream, m_spec.label); + while (true){ + if (m_spec.arrived && m_spec.arrived(*m_before.frame)){ + return finish(WalkStop::ARRIVED); + } + if (m_result.walked >= m_spec.limit){ + return finish(WalkStop::LIMIT); + } + const Milliseconds chunk = std::min(CHUNK, m_spec.limit - m_result.walked); + const int ret = hold(chunk); + m_result.walked += chunk; + ChunkVerdict verdict = after_watchers(ret); + if (verdict.next == ChunkNext::LOOK){ + verdict = look(); + } + if (verdict.next == ChunkNext::STOP){ + return finish(verdict.stop); + } } - const Milliseconds chunk = std::min(CHUNK, limit - result.walked); + } +private: + int hold(Milliseconds chunk){ WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); BlackScreenWatcher black(COLOR_RED); BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); ShortDialogWatcher dialog(COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); - const int ret = run_until( - stream, context, + return run_until( + m_stream, m_context, [&](ProControllerContext& context){ - if (taps){ - pbf_press_dpad(context, dpad_direction, 160ms, chunk - 160ms); - }else if (dpad && run){ - ssf_press_button(context, BUTTON_B, 0ms, chunk, 0ms); - ssf_press_dpad(context, dpad_direction, chunk, chunk, 0ms); - }else if (dpad){ - pbf_press_dpad(context, dpad_direction, chunk, 0ms); - }else{ - pbf_move_left_joystick(context, {x, y}, chunk, 0ms); - } + press(context, chunk); }, {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} ); - result.walked += chunk; + } + + void press(ProControllerContext& context, Milliseconds chunk) const{ + switch (m_spec.input){ + case WalkInput::TAPS: + pbf_press_dpad(context, m_dpad, 160ms, chunk - 160ms); + return; + case WalkInput::DPAD_RUN: + ssf_press_button(context, BUTTON_B, 0ms, chunk, 0ms); + ssf_press_dpad(context, m_dpad, chunk, chunk, 0ms); + return; + case WalkInput::DPAD: + pbf_press_dpad(context, m_dpad, chunk, 0ms); + return; + case WalkInput::STICK: + pbf_move_left_joystick(context, m_spec.stick, chunk, 0ms); + return; + } + } + // The watchers: 0 the encounter flash, 1 black, 2 the battle menu, 3 a + // dialogue, 4 the battle's text box. + ChunkVerdict after_watchers(int ret){ bool battle = ret == 0 || ret == 2 || ret == 4; if (ret == 1){ - battle = black_screen_was_battle(stream, context); + battle = black_screen_was_battle(m_stream, m_context); if (!battle){ - return finish(WalkStop::TRANSITION); + return {ChunkNext::STOP, WalkStop::TRANSITION}; } } - if (battle && !wild){ - stream.log("[Walk] " + label + ": a battle started; a trainer's here."); - return finish(WalkStop::DIALOG); + if (battle && m_spec.wild == WildPolicy::STOP_AT){ + m_stream.log("[Walk] " + m_spec.label + ": a battle started; a trainer's here."); + return {ChunkNext::STOP, WalkStop::DIALOG}; } if (battle){ - result.wild_battles++; - stream.log("[Walk] " + label + ": wild battle; running."); - escape(stream, context); - context.wait_for_all_requests(); - before = snapshot_or_fail(stream, label); - unchanged = 0; - continue; + escape_battle("wild battle; running."); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; } if (ret == 3){ - return finish(WalkStop::DIALOG); + return {ChunkNext::STOP, WalkStop::DIALOG}; } + return {ChunkNext::LOOK, WalkStop::LIMIT}; + } - context.wait_for_all_requests(); - VideoSnapshot after = snapshot_or_fail(stream, label); - const double change = walk_screen_change(*before.frame, *after.frame); - stream.log("[Walk] " + label + ": " + std::to_string(result.walked.count()) + + ChunkVerdict look(){ + m_context.wait_for_all_requests(); + VideoSnapshot after = snapshot_or_fail(m_stream, m_spec.label); + const double change = walk_screen_change(*m_before.frame, *after.frame); + m_stream.log("[Walk] " + m_spec.label + ": " + std::to_string(m_result.walked.count()) + "ms, screen change " + std::to_string(change) + "."); - if (change > WALK_SCENE_CHANGE && !wild && battle_follows(stream, context)){ - stream.log("[Walk] " + label + ": a battle started; a trainer's here."); - return finish(WalkStop::DIALOG); + if (change > WALK_SCENE_CHANGE && m_spec.wild == WildPolicy::STOP_AT && battle_follows(m_stream, m_context)){ + m_stream.log("[Walk] " + m_spec.label + ": a battle started; a trainer's here."); + return {ChunkNext::STOP, WalkStop::DIALOG}; } - if (change > WALK_SCENE_CHANGE && battle_follows(stream, context)){ - result.wild_battles++; - stream.log("[Walk] " + label + ": wild battle (leaf opening); running."); - escape(stream, context); - context.wait_for_all_requests(); - before = snapshot_or_fail(stream, label); - unchanged = 0; - continue; + if (change > WALK_SCENE_CHANGE && battle_follows(m_stream, m_context)){ + escape_battle("wild battle (leaf opening); running."); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; } // Two still chunks in a row, so one dropped frame does not stop us. A // frame exactly the same is a stalled video or a game hitch, not a wall: // standing at a wall still animates. if (change == 0){ - before = std::move(after); - continue; + m_before = std::move(after); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; } - largest = std::max(largest, change); - const double line = taps ? std::max(WALK_TAP_BLOCKED_CHANGE, 0.4 * largest) : WALK_BLOCKED_CHANGE; - const bool still = change < line; + const bool still = still_chunk(change); if (!still){ - result.moved_chunks++; + m_result.moved_chunks++; + } + m_unchanged = still ? m_unchanged + 1 : 0; + if (m_unchanged >= 2){ + return {ChunkNext::STOP, WalkStop::BLOCKED}; } - unchanged = still ? unchanged + 1 : 0; - if (unchanged >= 2){ - return finish(WalkStop::BLOCKED); + m_before = std::move(after); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; + } + + // The largest change a chunk of this walk made is a step. A bump into an + // NPC measured 17.2 after steps of 63 to 65 (r250a, Floaroma's old + // woman), over the fixed line of 16, so for taps "still" is also + // anything under 40% of the walk's own steps. + bool still_chunk(double change){ + m_largest = std::max(m_largest, change); + const double line = m_spec.input == WalkInput::TAPS + ? std::max(WALK_TAP_BLOCKED_CHANGE, 0.4 * m_largest) + : WALK_BLOCKED_CHANGE; + return change < line; + } + + void escape_battle(const std::string& what){ + m_result.wild_battles++; + m_stream.log("[Walk] " + m_spec.label + ": " + what); + m_escape(m_stream, m_context); + m_context.wait_for_all_requests(); + m_before = snapshot_or_fail(m_stream, m_spec.label); + m_unchanged = 0; + } + + WalkResult finish(WalkStop stop){ + m_result.stop = stop; + if (m_spec.wild == WildPolicy::RUN_FROM && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && + run_from_late_encounter(m_stream, m_context, m_escape, m_spec.label) + ){ + m_result.wild_battles++; } - before = std::move(after); + m_stream.log("[Walk] " + m_spec.label + ": " + walk_stop_name(stop) + " after " + + std::to_string(m_result.walked.count()) + "ms, " + + std::to_string(m_result.wild_battles) + " wild battle(s)."); + return m_result; } + + VideoStream& m_stream; + ProControllerContext& m_context; + const WildBattleEscape& m_escape; + const WalkSpec& m_spec; + const DpadPosition m_dpad; + WalkResult m_result{WalkStop::LIMIT, 0ms, 0}; + VideoSnapshot m_before; + size_t m_unchanged = 0; + double m_largest = 0; +}; + +WalkResult walk_until_spec( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const WalkSpec& spec +){ + return WalkUntil(stream, context, escape, spec).run(); } + +} + +WalkResult walk_until( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkArrived& arrived +){ + return walk_until_spec(stream, context, escape, { + .stick = stick, .limit = limit, .label = label, .arrived = arrived, + .input = WalkInput::STICK, .wild = WildPolicy::RUN_FROM, + }); +} + +WalkResult walk_until_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const DpadWalkOptions& options +){ + return walk_until_spec(stream, context, escape, { + .stick = stick, .limit = limit, .label = label, .arrived = options.arrived, + .input = options.pace == DpadPace::RUN ? WalkInput::DPAD_RUN : WalkInput::DPAD, + .wild = WildPolicy::RUN_FROM, + }); +} + +WalkResult walk_tiles_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, size_t tiles, const std::string& label +){ + return walk_until_spec(stream, context, escape, { + .stick = stick, .limit = 800ms * tiles, .label = label, .arrived = nullptr, + .input = WalkInput::TAPS, .wild = WildPolicy::RUN_FROM, + }); +} + +WalkResult walk_until_no_wild( + VideoStream& stream, ProControllerContext& context, + JoystickPosition stick, Milliseconds limit, const std::string& label +){ + // With no wild Pokemon any battle is a trainer's, and the walk stops at + // it (DIALOG) without running. + const WildBattleEscape no_wild = [](VideoStream& stream, ProControllerContext&){ + throw InternalProgramError(&stream.logger(), PA_CURRENT_FUNCTION, "walk_until_no_wild() ran from a battle."); + }; + return walk_until_spec(stream, context, no_wild, { + .stick = stick, .limit = limit, .label = label, .arrived = nullptr, + .input = WalkInput::STICK, .wild = WildPolicy::STOP_AT, + }); } WalkResult walk_for( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds duration, const std::string& label + JoystickPosition stick, Milliseconds duration, const std::string& label ){ WalkResult result{WalkStop::LIMIT, duration, 0}; context.wait_for_all_requests(); @@ -350,7 +441,7 @@ WalkResult walk_for( const int ret = run_until( stream, context, [&](ProControllerContext& context){ - pbf_move_left_joystick(context, {x, y}, duration, 0ms); + pbf_move_left_joystick(context, stick, duration, 0ms); }, {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} ); @@ -382,11 +473,11 @@ WalkResult walk_for( WalkResult walk_to_talk( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label + JoystickPosition stick, Milliseconds limit, const std::string& label ){ WalkResult total{WalkStop::LIMIT, 0ms, 0}; while (total.walked < limit){ - const WalkResult step = walk_until(stream, context, escape, x, y, + const WalkResult step = walk_until(stream, context, escape, stick, std::min(400ms, limit - total.walked), label); total.walked += step.walked; total.wild_battles += step.wild_battles; @@ -414,7 +505,7 @@ WalkResult walk_to_talk( WalkProbe probe_walk( VideoStream& stream, ProControllerContext& context, - double x, double y, Milliseconds limit, + JoystickPosition stick, Milliseconds limit, double moving_change, const std::string& label ){ WalkProbe result; @@ -423,7 +514,7 @@ WalkProbe probe_walk( run_until( stream, context, [&](ProControllerContext& context){ - pbf_move_left_joystick(context, {x, y}, limit, 0ms); + pbf_move_left_joystick(context, stick, limit, 0ms); }, {{sampler}} ); @@ -435,9 +526,9 @@ WalkProbe probe_walk( WalkResult walk_to_wall( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label + JoystickPosition stick, Milliseconds limit, const std::string& label ){ - const WalkResult result = walk_until(stream, context, escape, x, y, limit, label); + const WalkResult result = walk_until(stream, context, escape, stick, limit, label); if (result.stop == WalkStop::DIALOG || result.stop == WalkStop::TRANSITION){ OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, label + ": " + walk_stop_name(result.stop) + " instead of a wall.", stream); @@ -450,14 +541,13 @@ WalkResult walk_to_wall( WalkResult walk_expecting( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - WalkStop expected, - const std::function& arrived + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkExpectation& expected ){ - const WalkResult result = walk_until(stream, context, escape, x, y, limit, label, arrived); - if (result.stop != expected){ + const WalkResult result = walk_until(stream, context, escape, stick, limit, label, expected.arrived); + if (result.stop != expected.stop){ OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": expected " + walk_stop_name(expected) + ", got " + + label + ": expected " + walk_stop_name(expected.stop) + ", got " + walk_stop_name(result.stop) + ".", stream); } return result; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h index 15622b26f4..9beb976f6e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -16,6 +16,7 @@ #include "Common/Cpp/Time.h" #include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "CommonFramework/Tools/VideoStream.h" +#include "Controllers/Joystick.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" namespace PokemonAutomation{ @@ -37,6 +38,9 @@ enum class WalkStop{ }; const char* walk_stop_name(WalkStop stop); +// Whether the walk has arrived, from the screen after each chunk. +using WalkArrived = std::function; + struct WalkResult{ WalkStop stop; Milliseconds walked; @@ -48,26 +52,35 @@ struct WalkResult{ size_t moved_chunks = 0; }; -// Holds the left stick at (x, y) until one of the WalkStop events. Wild battles on +// Holds the left stick at `stick` until one of the WalkStop events. Wild battles on // the way are run from, and walking resumes. Does not throw for any WalkStop; the // caller decides which ones are expected. WalkResult walk_until( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - const std::function& arrived = nullptr + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkArrived& arrived = nullptr ); +// How walk_until_dpad() presses: RUN holds B too, twice as fast and as +// tile-true. +enum class DpadPace{ + WALK, + RUN, +}; +struct DpadWalkOptions{ + WalkArrived arrived = nullptr; + DpadPace pace = DpadPace::WALK; +}; + // walk_until() on the D-pad, for a straight walk along a row or a column -// ((x, y) is one of the four directions). The D-pad moves whole tiles and +// (`stick` is one of the four directions). The D-pad moves whole tiles and // keeps to the row: the stick drifts off it, catches tree corners and slides // a row on stairs (Route 203, r226a and r227b; by hand the D-pad went -// through the same places). With `run`, B is held too: twice as fast, and -// as tile-true. +// through the same places). WalkResult walk_until_dpad( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - const std::function& arrived = nullptr, - bool run = false + JoystickPosition stick, Milliseconds limit, const std::string& label, + const DpadWalkOptions& options = {} ); // A set number of whole tiles on the D-pad, a tap a tile (a tap moves @@ -76,7 +89,7 @@ WalkResult walk_until_dpad( // LIMIT after `tiles` taps, or BLOCKED when two taps in a row move nothing. WalkResult walk_tiles_dpad( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, size_t tiles, const std::string& label + JoystickPosition stick, size_t tiles, const std::string& label ); // walk_until() where there are no wild Pokemon (a gym, a building): any @@ -85,25 +98,29 @@ WalkResult walk_tiles_dpad( // (r206d tried to run from the second Oreburgh Gym trainer). WalkResult walk_until_no_wild( VideoStream& stream, ProControllerContext& context, - double x, double y, Milliseconds limit, const std::string& label + JoystickPosition stick, Milliseconds limit, const std::string& label ); -// Holds the left stick at (x, y) for `duration` in one go, for a walk of a +// Holds the left stick at `stick` for `duration` in one go, for a walk of a // set length: walk_until() checks the screen between short holds, and each // break can cost a step. Stops early for a dialog or a transition. A wild // battle is run from and ends the walk with `wild_battles` 1, at a place the // caller cannot know. WalkResult walk_for( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds duration, const std::string& label + JoystickPosition stick, Milliseconds duration, const std::string& label ); -// walk_until() that requires `expected`. Fails with the screenshot otherwise. +// The stop walk_expecting() requires, and what counts as arrived. +struct WalkExpectation{ + WalkStop stop = WalkStop::ARRIVED; + WalkArrived arrived = nullptr; +}; +// walk_until() that requires `expected.stop`. Fails with the screenshot otherwise. WalkResult walk_expecting( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label, - WalkStop expected, - const std::function& arrived = nullptr + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkExpectation& expected ); // After a move that may have walked onto an encounter tile (a tap in a cave @@ -119,7 +136,7 @@ bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, // next gate. Fails on a dialog or a transition: we are not where we think. WalkResult walk_to_wall( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label + JoystickPosition stick, Milliseconds limit, const std::string& label ); // Walks toward someone to talk to: after each short stretch, presses A and stops @@ -127,7 +144,7 @@ WalkResult walk_to_wall( // BLOCKED, e.g. in tall grass, whose sway keeps the screen changing. WalkResult walk_to_talk( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - double x, double y, Milliseconds limit, const std::string& label + JoystickPosition stick, Milliseconds limit, const std::string& label ); // How much the screen changed between two frames, 0 for identical. Walking scrolls @@ -145,7 +162,7 @@ constexpr double WALK_SCENE_CHANGE = 120.0; // One straight walk timed finely, to check a map against the game: holds the -// stick at (x, y) and samples the screen every 100ms until the view has been +// stick at `stick` and samples the screen every 100ms until the view has been // still for 800ms or `limit` runs out. Times count from the start of the hold. // A sample moved if it changed by at least `moving_change`: still is 4 to 7, // walking 16 to 85. Handles no battles or dialogs: use it on open ground, @@ -161,7 +178,7 @@ struct WalkProbe{ }; WalkProbe probe_walk( VideoStream& stream, ProControllerContext& context, - double x, double y, Milliseconds limit, + JoystickPosition stick, Milliseconds limit, double moving_change, const std::string& label ); From b678b4a377d8ba20276422d7822baf7bff959e25 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:49:09 -0800 Subject: [PATCH 08/39] BDSP walker: walk_route() and its helpers move to PlannedRoute RouteWalk.cpp was 926 lines, and splitting walk_route() into functions would take it past the 1000-line limit. walk_route(), walk_waypoints(), talk_to_npc(), RouteTrainerFight and the route-log helpers they use move unchanged to PokemonBDSP_AutoStory_PlannedRoute.{h,cpp}. RouteWalk exposes route_ms_per_tile() and route_stick() for them. Review with: git diff --color-moved HEAD~1 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 670 ++++++++++++++++++ .../PokemonBDSP_AutoStory_PlannedRoute.h | 92 +++ .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 664 +---------------- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 72 +- SerialPrograms/cmake/SourceFiles.cmake | 2 + 5 files changed, 789 insertions(+), 711 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp new file mode 100644 index 0000000000..5d80952053 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -0,0 +1,670 @@ +/* BDSP AutoStory - Planned Route + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Navigation/Pokemon_RouteLog.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_PlannedRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +// World tiles as tiles of the named maps, for the shared route log +// (Pokemon_RouteLog.h); a tile no map holds is a "world" tile. +std::vector route_tiles(const StoryWorld& maps, const std::vector& tiles){ + std::vector out; + for (const TilePos& pos : tiles){ + const std::optional local = maps.to_local(pos); + out.push_back(local ? RouteTile{local->map, local->pos} : RouteTile{"world", pos}); + } + return out; +} + +std::string describe(const StoryWorld& maps, const std::vector& tiles){ + return Pokemon::describe(route_tiles(maps, tiles)); +} + +RouteLogLeg route_log_leg(const TileLeg& leg){ + return {.action = RouteLegAction::WALK, .direction = leg.direction, .tiles = leg.tiles, + .end = leg.ends_at_wall ? RouteLegEnd::WALL : RouteLegEnd::OPEN}; +} + +PlannedRouteLeg planned_route_leg(const StoryWorld& maps, const PlannedLeg& leg){ + return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; +} + +void log_plan(VideoStream& stream, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ + std::vector logged; + for (const PlannedLeg& leg : legs){ + logged.push_back(planned_route_leg(maps, leg)); + } + stream.log(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged)); +} + +// Logs a leg that ended on its planned tiles, and how that is known: an +// open leg counts its taps through a wild battle, so its end is assumed; a +// leg to a wall is walked again to its wall. +void log_leg_done(VideoStream& stream, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ + const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; + stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), + assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); +} + +// The tiles the player passes when walking `tiles` tiles from `from`, the +// first one after `from` first. Stops early where the player is stopped. +std::vector leg_path(const StoryWorld& grid, TilePos from, Direction direction, size_t tiles){ + std::vector path; + size_t travelled = 0; + while (travelled < tiles){ + const std::optional next = step(grid, from, direction); + if (!next){ + break; + } + travelled += std::abs(next->x - from.x) + std::abs(next->y - from.y); + from = *next; + path.push_back(from); + } + return path; +} + +// Holding toward a wall with people or Pokemon moving beside us never reads +// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking +// scrolls the view far more (35 to 90). True if holding toward (x, y) for +// 1.6 s changes the screen less than walking would: we stand at a wall. +bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ + constexpr double WALKING_CHANGE = 25.0; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + double most = 0; + for (size_t chunk = 0; chunk < 4 && before; chunk++){ + pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (!after){ + return false; + } + most = std::max(most, walk_screen_change(*before.frame, *after.frame)); + before = std::move(after); + } + stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + + (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); + return before && most < WALKING_CHANGE; +} + +// The map all of `tiles` are on, or empty if they are on several or none. +std::string map_of(const StoryWorld& maps, const std::vector& tiles){ + std::string name; + for (const TilePos& pos : tiles){ + const std::optional tile = maps.to_local(pos); + if (!tile || (!name.empty() && tile->map != name)){ + return ""; + } + name = tile->map; + } + return name; +} + +} + + +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, + std::vector from, + const std::function& goal, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const std::vector& avoid, + const RobustPlanCosts& costs +){ + // Our own copy, so things found in the way can be added to it. Tiles to + // avoid are ground the game lets us walk on (a trainer's line of sight), + // so they are off the map, not walls: a leg must not end "at" one, or + // the walk would run on through it. + StoryWorld grid = maps; + for (const TilePos& pos : avoid){ + if (std::find(from.begin(), from.end(), pos) == from.end()){ + grid.uncover(pos); + } + } + // Which way the player faces, once a leg has set it. + std::optional facing; + // Every banner read on the way, to check the map we arrive on against. + const std::string map_start = map_of(maps, from); + std::vector all_banners; + for (size_t plans = 0; plans < 8; plans++){ + std::optional> legs = plan_robust_route(grid, from, goal, costs); + if (!legs){ + // From a few possible tiles the exact-open-leg costs found no + // plan where the default ones did (r240a, three tiles on Route + // 204's row 27); the default plan is longer but sound. + stream.log("[Route] " + label + ": no plan with the route costs from " + describe(maps, from) + + "; trying the default costs.", COLOR_ORANGE); + legs = plan_robust_route(grid, from, goal, RobustPlanCosts()); + } + if (!legs){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no plan from " + describe(maps, from) + ".", stream); + } + route_status(stream, label + ": " + std::to_string(legs->size()) + " legs planned"); + log_plan(stream, maps, label, from, *legs); + + bool replanned = false; + size_t leg_number = 0; + for (const PlannedLeg& planned : *legs){ + const TileLeg& leg = planned.leg; + leg_number++; + route_status(stream, route_leg_line(leg_number, legs->size(), planned_route_leg(maps, planned))); + const double x = route_stick(leg.direction).x; + const double y = route_stick(leg.direction).y; + const std::string leg_label = label + ": " + to_string(leg); + + // Note the location banners the walk passes, to check map changes. + // A wild battle or a second walk to the wall may follow the + // banner: any banner seen during the leg counts. + const std::string map_before = map_of(maps, from); + const std::string map_after = map_of(maps, planned.after); + std::vector banners; + auto banner_ok = [&]{ + return std::any_of(banners.begin(), banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_after); + }); + }; + auto watch_banner = [&](const ImageViewRGB32& screen){ + if (banners.empty() || (!map_after.empty() && !banner_ok() && banners.size() < 8)){ + const std::string read = read_location_banner(screen); + if (!read.empty() && (banners.empty() || banners.back() != read)){ + banners.push_back(read); + } + } + return false; + }; + + WalkResult result; + // Tiles the first tap of an open leg moved (see below). + size_t turn_moved = 0; + if (leg.ends_at_wall){ + // Run (B held) on the D-pad: a leg to a wall ends at the wall + // however fast we get there, and the D-pad keeps to the row. + const Milliseconds per_tile = route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; + // Long enough to reach the wall from the farthest tile, with room + // for a slow start; past the wall the player stands still. + const Milliseconds limit = ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); + // A wild battle may end the walk early. Walking to a wall again + // ends in the same place, so finish the leg. + const bool interrupted = result.wild_battles > 0; + for (size_t again = 0; again < 3 && result.wild_battles > 0 && + (result.stop == WalkStop::LIMIT || result.stop == WalkStop::BLOCKED); again++ + ){ + stream.log("[Route] " + leg_label + ": a wild battle on the way; walking to the wall again."); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); + } + // A wall well before the nearest one the map has: something the + // map does not know is in the way. Work out where we stopped from + // the time walked, put it on our map and plan again from there. + size_t nearest = SIZE_MAX; + for (const TilePos& pos : from){ + nearest = std::min(nearest, slide_to_wall(grid, pos, leg.direction).tiles); + } + auto tiles_moved = [&](const WalkResult& walk){ + const Milliseconds moving = walk.walked - ROUTE_START_DELAY - + (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); + return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); + }; + size_t walked_tiles = tiles_moved(result); + // A stick walk can catch a corner when we stand part of a tile + // off centre, where a D-pad tap, which moves whole tiles, gets + // through: on Route 203's stairs the walk west stopped nine + // times in a row and two taps crossed (r225a, 2026-10-31). + // Tap twice, then walk on to the wall. + if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ + const DpadPosition dpad = + leg.direction == Direction::NORTH ? DPAD_UP : + leg.direction == Direction::SOUTH ? DPAD_DOWN : + leg.direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); + for (size_t tap = 0; tap < 2; tap++){ + pbf_press_dpad(context, dpad, 160ms, 640ms); + } + context.wait_for_all_requests(); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); + walked_tiles += tiles_moved(result); + } + // Someone walking about may stand in the way for a moment: wait + // and walk to the wall again before calling it an obstacle. + for (size_t again = 0; !interrupted && again < 2 && + result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++ + ){ + stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(context, 2500ms); + context.wait_for_all_requests(); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); + walked_tiles += tiles_moved(result); + } + // Any stop short of the wall's distance: someone who stays put + // is talked to, and the leg is planned again from where we are. + // Within the time count's slack this may be the wall itself, + // where A does nothing and the new plan walks no further (r239a: + // a wanderer 3 tiles short passed as "about 8 of 10", and every + // later leg was off by those tiles). + if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ + // Someone who stays put: talk to them. A trainer we walked up + // to from the side starts the battle this way. + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + { + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, leg_label + ": someone is in the way; talking to them."); + fight("someone in the way"); + } + } + std::vector stopped; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, SIZE_MAX); + for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ + stopped.push_back(k == 0 ? pos : path[k - 1]); + } + } + std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": stopped early, and no tile fits.", stream); + } + if (stopped.size() == 1){ + const TilePos blocker = stopped[0].next(leg.direction); + grid.block(blocker); + stream.log("[Route] " + leg_label + ": something the map lacks is at " + + describe(maps, blocker) + "; marked it.", COLOR_ORANGE); + } + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + "stopped early after about " + std::to_string(walked_tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); + facing = leg.direction; + from = stopped; + replanned = true; + break; + } + // A leg to the wall that ran out of time is not at its wall: + // the open leg before it stopped short, and it ran along + // another row (r127a went west along Sandgem's row 13 for row + // 14, and the next leg north walked into the lab). Out of tall + // grass a stop reads, so walk on: a player still moving is + // not where the plan has him. + if (!interrupted && result.stop == WalkStop::LIMIT && route_wall_readable(grid, planned.after)){ + const WalkResult check = walk_until_dpad(stream, context, escape, {x, y}, 2000ms, leg_label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); + if (check.stop == WalkStop::DIALOG){ + // A trainer who saw us froze the walk (the "!") until + // its time ran out, then walked over (r241a: Aroma Lady + // Taylor's line crosses Route 204's corridor). Battle, + // then plan again from where a sight line crosses the leg. + route_status(stream, leg_label + ": a trainer stopped us on the way; battling."); + fight("a trainer on the way"); + // Where they saw us from is not reliable (BDSP's trainers + // stand a few tiles from Platinum's, r242a), so do not + // guess the tile: a leg to a wall ends at its wall however + // far it got. Walk on to it. + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); + } + if (check.stop != WalkStop::BLOCKED && + !(check.stop == WalkStop::LIMIT && holding_against_wall(stream, context, x, y, leg_label + ": at the wall?")) + ){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": ran out of time short of the wall, and walking on gets " + + walk_stop_name(check.stop) + ": not where the plan has us.", stream); + } + } + }else{ + // Whole tiles on the D-pad, a tap each: a tap moves exactly + // one tile, while a stick hold timed for three moved one in + // the Ravaged Path (r235b) and the belief drifted four rows. + // The first tap of a leg was meant to turn, but a 160 ms + // D-pad tap turns and steps whichever way the player faced + // (r248a: all seven did; r246a: "south 1" out of the cave + // went two tiles, onto the Youngster's row), so the tap is + // measured like any other and a step counts for the leg. + if (facing != leg.direction){ + const DpadPosition dpad = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + pbf_press_dpad(context, dpad, 160ms, 700ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ + turn_moved = 1; + stream.log("[Route] " + leg_label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); + } + } + if (leg.tiles > turn_moved){ + result = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - turn_moved, leg_label); + }else{ + result = WalkResult{WalkStop::LIMIT, 0ms, 0}; + } + // A wild battle starts once the step onto the grass is done, + // so the taps still count; the walk went on after it. + if (result.wild_battles > 0){ + stream.log("[Route] " + leg_label + ": " + std::to_string(result.wild_battles) + + " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); + } + // Taps that moved nothing: someone stands in the way (the + // Youngster below the Ravaged Path wanders across the row + // under the cliff, r239a, r240a). Two still taps end the walk, + // so the tiles moved are the taps less those two. Wait for + // them to move on and tap the rest; someone who stays is + // talked to, put on the map, and the leg planned again from + // the tile we stand on, which the taps give exactly. + auto taps_moved = [](const WalkResult& walk){ return walk.moved_chunks; }; + size_t moved = std::min(leg.tiles, taps_moved(result) + turn_moved); + // One wait: a wanderer in the way steps off in a few seconds; + // anything still there after that is talked to (r246a lost + // 20 s per block on three waits). + for (size_t again = 0; again < 1 && result.stop == WalkStop::BLOCKED && moved < leg.tiles; again++){ + stream.log("[Route] " + leg_label + ": blocked after " + std::to_string(moved) + " of " + + std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(context, 2500ms); + context.wait_for_all_requests(); + const WalkResult rest = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - moved, leg_label + ": the rest"); + moved = std::min(leg.tiles, moved + taps_moved(rest)); + result.stop = rest.stop; + result.walked += rest.walked; + result.wild_battles += rest.wild_battles; + } + if (result.stop == WalkStop::BLOCKED && moved < leg.tiles){ + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + { + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, leg_label + ": someone is in the way; talking to them."); + fight("someone in the way"); + } + } + std::vector stopped; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, moved); + stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); + } + std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); + if (stopped.size() == 1){ + const TilePos blocker = stopped[0].next(leg.direction); + grid.block(blocker); + stream.log("[Route] " + leg_label + ": something the map lacks is at " + + describe(maps, blocker) + "; marked it.", COLOR_ORANGE); + } + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + "blocked after " + std::to_string(moved) + " of " + std::to_string(leg.tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); + facing = leg.direction; + from = stopped; + replanned = true; + break; + } + } + facing = leg.direction; + { + context.wait_for_all_requests(); + if (VideoSnapshot screen = stream.video().snapshot()){ + watch_banner(*screen.frame); + } + } + std::string banner_text; + for (const std::string& read : banners){ + banner_text += (banner_text.empty() ? "\"" : ", \"") + read + "\""; + } + all_banners.insert(all_banners.end(), banners.begin(), banners.end()); + // Entering another map shows its name; a missing or wrong name means + // the walk did not end where the plan says. From tiles on several + // maps (a leg not confirmed) the banner may already have shown. + if (!map_before.empty() && !map_after.empty() && map_after != map_before && !banner_ok()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": expected to enter " + map_after + ", but the location banner read " + + (banner_text.empty() ? "nothing" : banner_text) + ".", stream); + } + if (!banner_text.empty()){ + stream.log("[Route] " + leg_label + ": location banner " + banner_text + "."); + } + // Where each leg ended, to compare with the plan after a run. + if (VideoSnapshot screen = stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(leg_label) + ".png"); + } + if (result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": unexpected transition.", stream); + } + // A trainer who sees us freezes the player and walks over; the + // dialogue may open after the walk has ended. + const std::vector> paths = [&]{ + std::vector> out; + for (const TilePos& pos : from){ + out.push_back(leg_path(grid, pos, leg.direction, + leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); + } + return out; + }(); + std::vector watching; + for (size_t t = 0; t < trainers.size(); t++){ + const std::vector sight = sight_tiles(grid, trainers[t]); + for (const std::vector& path : paths){ + if (std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ + return std::find(sight.begin(), sight.end(), pos) != sight.end(); + })){ + watching.push_back(t); + break; + } + } + } + bool talking = result.stop == WalkStop::DIALOG; + if (!talking){ + ShortDialogWatcher dialog(COLOR_YELLOW); + const Milliseconds wait = watching.empty() ? 500ms : 4000ms; + talking = wait_until(stream, context, wait, {{dialog}}) == 0; + } + if (!talking){ + from = planned.after; + log_leg_done(stream, maps, label, planned, result.wild_battles); + continue; + } + + if (watching.empty()){ + // A trainer the map does not have. Fight, then plan again from + // anywhere the leg could have stopped. + std::vector anywhere = from; + for (const std::vector& path : paths){ + anywhere.insert(anywhere.end(), path.begin(), path.end()); + } + std::sort(anywhere.begin(), anywhere.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + anywhere.erase(std::unique(anywhere.begin(), anywhere.end()), anywhere.end()); + route_status(stream, label + ": a trainer the map lacks stopped us; fighting."); + fight("unknown trainer"); + if (leg.ends_at_wall){ + const Milliseconds limit = ROUTE_START_DELAY + + route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); + from = planned.after; + log_leg_done(stream, maps, label, planned, 0); + continue; + } + from = anywhere; + replanned = true; + break; + } + // Each possible start tile was stopped on the first tile of its path + // inside a watching trainer's sight. + const TrainerSight trainer = trainers[watching[0]]; + std::vector stopped; + for (const std::vector& path : paths){ + for (const TilePos& pos : path){ + bool seen = false; + for (size_t t : watching){ + const std::vector sight = sight_tiles(grid, trainers[t]); + seen |= std::find(sight.begin(), sight.end(), pos) != sight.end(); + } + if (seen){ + stopped.push_back(pos); + break; + } + } + } + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + leg_label + ": " + trainer.name + " talked, but no path enters their sight.", stream); + } + stream.log(ROUTE_LOG_TAG + route_trainer_line(label, trainer.name, route_tiles(maps, stopped))); + route_status(stream, "Fighting " + trainer.name + "."); + fight(trainer.name); + trainers.erase(trainers.begin() + watching[0]); + // Their sight line only says roughly where: BDSP's trainers stand a + // few tiles from Platinum's (r242a). A leg to a wall ends at its + // wall however far it got: walk on to it and carry on. An open leg + // counts its taps: the step that brought us into sight was complete. + if (leg.ends_at_wall){ + const Milliseconds limit = ROUTE_START_DELAY + + route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; + stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); + from = planned.after; + log_leg_done(stream, maps, label, planned, 0); + continue; + } + // Only the taps that moved count: the trainer freezes the player + // on the tile they saw, and the taps pressed while they walk over + // move nobody (r248a: "4 taps" were 2 steps; the belief of two + // tiles then cost a 12-tile run to a wall and 5 back). + const size_t taps = std::min(leg.tiles, result.moved_chunks + turn_moved); + std::vector by_taps; + for (const TilePos& pos : from){ + const std::vector path = leg_path(grid, pos, leg.direction, leg.tiles); + const size_t k = std::min(taps, path.size()); + by_taps.push_back(k == 0 ? pos : path[k - 1]); + } + std::sort(by_taps.begin(), by_taps.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + by_taps.erase(std::unique(by_taps.begin(), by_taps.end()), by_taps.end()); + stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), + std::to_string(taps) + " taps before the battle", route_tiles(maps, by_taps)), COLOR_ORANGE); + from = by_taps; + replanned = true; + break; + } + if (!replanned){ + // Legs whose ends span two maps skip the per-leg check; the walk + // as a whole must still have seen the banner of the map it ends on. + const std::string map_end = map_of(maps, from); + const bool changed_map = !map_start.empty() && !map_end.empty() && map_end != map_start; + if (changed_map && std::none_of(all_banners.begin(), all_banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_end); + })){ + std::string seen; + for (const std::string& read : all_banners){ + seen += (seen.empty() ? "\"" : ", \"") + read + "\""; + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": arrived at " + describe(maps, from) + " on the map, but " + map_end + + "'s location banner never showed (read " + (seen.empty() ? "nothing" : seen) + ").", stream); + } + stream.log(ROUTE_LOG_TAG + route_arrived_line(label, route_tiles(maps, from))); + return from; + } + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": too many plans.", stream); +} + + + +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, + std::vector from, + const std::vector& waypoints, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const RobustPlanCosts& costs +){ + for (size_t i = 0; i < waypoints.size(); i++){ + const TilePos goal = waypoints[i]; + from = walk_route(stream, context, escape, maps, from, + [&](const TilePos& pos){ return pos == goal; }, + trainers, fight, + label + " (" + std::to_string(i + 1) + "/" + std::to_string(waypoints.size()) + ", to " + describe(maps, goal) + ")", + {}, costs); + } + return from; +} + +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, + std::vector& at, + const TilePos& npc, + const std::vector& sides, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label +){ + for (Direction side : sides){ + const TilePos stand = npc.next(side); + if (!standable(maps, stand)){ + continue; + } + at = walk_route(stream, context, escape, maps, at, [&](const TilePos& pos){ return pos == stand; }, + trainers, fight, label); + // Face the NPC: the opposite of the side we stand on. + const Direction face = + side == Direction::NORTH ? Direction::SOUTH : + side == Direction::SOUTH ? Direction::NORTH : + side == Direction::EAST ? Direction::WEST : Direction::EAST; + route_turn(stream, context, face); + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, label + ": talking from " + describe(maps, stand) + "."); + return; + } + route_status(stream, label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": no side of " + describe(maps, npc) + " started a dialogue.", stream); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h new file mode 100644 index 0000000000..6950867b70 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -0,0 +1,92 @@ +/* BDSP AutoStory - Planned Route + * + * From: https://github.com/PokemonAutomation/ + * + * Walks a plan made on the story maps: legs to a wall are held until the + * player stops, open legs are tapped a tile at a time. Tracks every tile + * the player may be on, and plans again after something stops the walk. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_H +#define PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_H + +#include +#include +#include +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Called when a trainer's dialogue has opened, or when someone standing in +// the way was talked to. Fights the battle if one starts, and returns to the +// overworld either way. +using RouteTrainerFight = std::function; + +// Walks from any of the world tiles `from` of `maps` until every tile the +// player may be on is `goal`, and returns those tiles. Trainers in +// `trainers` who see the player are fought with `fight` and taken off the +// list. Tiles in `avoid` (where someone wanders) are planned as walls. +// Fails with a screenshot +// on a transition, a dialogue no trainer explains, a wild battle during an +// open leg, or when no plan exists. A walk from one map to another must see +// the second map's location banner on the way: a walk whose legs all ended +// on walls can still end in the wrong place (r124a saved in Sandgem Town +// after "arriving" on Route 202). +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, + std::vector from, + const std::function& goal, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const std::vector& avoid = {}, + const RobustPlanCosts& costs = route_plan_costs() +); + +// Walks `from` to each of `waypoints` in turn, with walk_route() for every +// stretch, and returns where the last one ended. The corners of a line a +// human walked, so the plan is that line, with the walker's handling of +// trainers and blocks on each stretch (Route 204 North, 2026-10-31). +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, + std::vector from, + const std::vector& waypoints, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label, + const RobustPlanCosts& costs = waypoint_plan_costs() +); + +// Walks next to the NPC on world tile `npc`, faces them and presses A, trying +// the sides in `sides` (the side of the NPC to stand on) in order until a +// dialogue opens. `at` is where the player may be, and is updated. Fails with +// a screenshot if no side starts a dialogue. A press from a diagonal tile, +// or from a tile a row off, talks to nobody, so each side is tried in turn. +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, + std::vector& at, + const Pokemon::TilePos& npc, + const std::vector& sides, + std::vector& trainers, + const RouteTrainerFight& fight, + const std::string& label +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index 129795d797..d7d6367e9c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -39,118 +39,24 @@ bool across(Direction direction){ return direction == Direction::EAST || direction == Direction::WEST; } -Milliseconds ms_per_tile(Direction direction){ - return across(direction) ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN; -} - -// The left stick for a direction. Row 0 is the top of a map, and the stick's -// y is positive upward. -void stick_for(Direction direction, double& x, double& y){ - x = 0; - y = 0; - switch (direction){ - case Direction::NORTH: y = 1; break; - case Direction::EAST: x = 1; break; - case Direction::SOUTH: y = -1; break; - case Direction::WEST: x = -1; break; - } -} - -// World tiles as tiles of the named maps, for the shared route log -// (Pokemon_RouteLog.h); a tile no map holds is a "world" tile. -std::vector route_tiles(const StoryWorld& maps, const std::vector& tiles){ - std::vector out; - for (const TilePos& pos : tiles){ - const std::optional local = maps.to_local(pos); - out.push_back(local ? RouteTile{local->map, local->pos} : RouteTile{"world", pos}); - } - return out; -} - -std::string describe(const StoryWorld& maps, const std::vector& tiles){ - return Pokemon::describe(route_tiles(maps, tiles)); -} - -RouteLogLeg route_log_leg(const TileLeg& leg){ - return {.action = RouteLegAction::WALK, .direction = leg.direction, .tiles = leg.tiles, - .end = leg.ends_at_wall ? RouteLegEnd::WALL : RouteLegEnd::OPEN}; -} - -PlannedRouteLeg planned_route_leg(const StoryWorld& maps, const PlannedLeg& leg){ - return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; -} - -void log_plan(VideoStream& stream, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ - std::vector logged; - for (const PlannedLeg& leg : legs){ - logged.push_back(planned_route_leg(maps, leg)); - } - stream.log(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged)); } -// Logs a leg that ended on its planned tiles, and how that is known: an -// open leg counts its taps through a wild battle, so its end is assumed; a -// leg to a wall is walked again to its wall. -void log_leg_done(VideoStream& stream, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ - const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; - stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), - assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); -} - -// The tiles the player passes when walking `tiles` tiles from `from`, the -// first one after `from` first. Stops early where the player is stopped. -std::vector leg_path(const StoryWorld& grid, TilePos from, Direction direction, size_t tiles){ - std::vector path; - size_t travelled = 0; - while (travelled < tiles){ - const std::optional next = step(grid, from, direction); - if (!next){ - break; - } - travelled += std::abs(next->x - from.x) + std::abs(next->y - from.y); - from = *next; - path.push_back(from); - } - return path; -} - -// Holding toward a wall with people or Pokemon moving beside us never reads -// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking -// scrolls the view far more (35 to 90). True if holding toward (x, y) for -// 1.6 s changes the screen less than walking would: we stand at a wall. -bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ - constexpr double WALKING_CHANGE = 25.0; - context.wait_for_all_requests(); - VideoSnapshot before = stream.video().snapshot(); - double most = 0; - for (size_t chunk = 0; chunk < 4 && before; chunk++){ - pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); - context.wait_for_all_requests(); - VideoSnapshot after = stream.video().snapshot(); - if (!after){ - return false; - } - most = std::max(most, walk_screen_change(*before.frame, *after.frame)); - before = std::move(after); - } - stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + - (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); - return before && most < WALKING_CHANGE; +Milliseconds route_ms_per_tile(Direction direction){ + return across(direction) ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN; } -// The map all of `tiles` are on, or empty if they are on several or none. -std::string map_of(const StoryWorld& maps, const std::vector& tiles){ - std::string name; - for (const TilePos& pos : tiles){ - const std::optional tile = maps.to_local(pos); - if (!tile || (!name.empty() && tile->map != name)){ - return ""; - } - name = tile->map; +JoystickPosition route_stick(Direction direction){ + switch (direction){ + case Direction::NORTH: + return {0, 1}; + case Direction::EAST: + return {1, 0}; + case Direction::SOUTH: + return {0, -1}; + case Direction::WEST: + return {-1, 0}; } - return name; -} - + return {0, 0}; } @@ -166,15 +72,13 @@ void route_status(VideoStream& stream, const std::string& text){ } Milliseconds route_open_hold(Direction direction, size_t tiles){ - const Milliseconds step = ms_per_tile(direction); + const Milliseconds step = route_ms_per_tile(direction); return step * tiles - step / 2; } void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ - double x, y; - stick_for(direction, x, y); stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); - pbf_move_left_joystick(context, {x, y}, ROUTE_TURN_TAP, 300ms); + pbf_move_left_joystick(context, route_stick(direction), ROUTE_TURN_TAP, 300ms); context.wait_for_all_requests(); } @@ -209,9 +113,7 @@ bool talk_with_nudges( for (int offset : {0, -1, 1, -2, 2}){ const Direction side = offset < at ? minus : plus; for (; at != offset; at += offset < at ? -1 : 1){ - double x, y; - stick_for(side, x, y); - pbf_move_left_joystick(context, {x, y}, ROUTE_NUDGE_TAP, 300ms); + pbf_move_left_joystick(context, route_stick(side), ROUTE_NUDGE_TAP, 300ms); } if (offset != 0){ route_turn(stream, context, facing); @@ -243,540 +145,6 @@ bool talk_with_nudges( return false; } -std::vector walk_route( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::function& goal, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const std::vector& avoid, - const RobustPlanCosts& costs -){ - // Our own copy, so things found in the way can be added to it. Tiles to - // avoid are ground the game lets us walk on (a trainer's line of sight), - // so they are off the map, not walls: a leg must not end "at" one, or - // the walk would run on through it. - StoryWorld grid = maps; - for (const TilePos& pos : avoid){ - if (std::find(from.begin(), from.end(), pos) == from.end()){ - grid.uncover(pos); - } - } - // Which way the player faces, once a leg has set it. - std::optional facing; - // Every banner read on the way, to check the map we arrive on against. - const std::string map_start = map_of(maps, from); - std::vector all_banners; - for (size_t plans = 0; plans < 8; plans++){ - std::optional> legs = plan_robust_route(grid, from, goal, costs); - if (!legs){ - // From a few possible tiles the exact-open-leg costs found no - // plan where the default ones did (r240a, three tiles on Route - // 204's row 27); the default plan is longer but sound. - stream.log("[Route] " + label + ": no plan with the route costs from " + describe(maps, from) + - "; trying the default costs.", COLOR_ORANGE); - legs = plan_robust_route(grid, from, goal, RobustPlanCosts()); - } - if (!legs){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": no plan from " + describe(maps, from) + ".", stream); - } - route_status(stream, label + ": " + std::to_string(legs->size()) + " legs planned"); - log_plan(stream, maps, label, from, *legs); - - bool replanned = false; - size_t leg_number = 0; - for (const PlannedLeg& planned : *legs){ - const TileLeg& leg = planned.leg; - leg_number++; - route_status(stream, route_leg_line(leg_number, legs->size(), planned_route_leg(maps, planned))); - double x, y; - stick_for(leg.direction, x, y); - const std::string leg_label = label + ": " + to_string(leg); - - // Note the location banners the walk passes, to check map changes. - // A wild battle or a second walk to the wall may follow the - // banner: any banner seen during the leg counts. - const std::string map_before = map_of(maps, from); - const std::string map_after = map_of(maps, planned.after); - std::vector banners; - auto banner_ok = [&]{ - return std::any_of(banners.begin(), banners.end(), [&](const std::string& read){ - return location_banner_matches(read, map_after); - }); - }; - auto watch_banner = [&](const ImageViewRGB32& screen){ - if (banners.empty() || (!map_after.empty() && !banner_ok() && banners.size() < 8)){ - const std::string read = read_location_banner(screen); - if (!read.empty() && (banners.empty() || banners.back() != read)){ - banners.push_back(read); - } - } - return false; - }; - - WalkResult result; - // Tiles the first tap of an open leg moved (see below). - size_t turn_moved = 0; - if (leg.ends_at_wall){ - // Run (B held) on the D-pad: a leg to a wall ends at the wall - // however fast we get there, and the D-pad keeps to the row. - const Milliseconds per_tile = ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; - // Long enough to reach the wall from the farthest tile, with room - // for a slow start; past the wall the player stands still. - const Milliseconds limit = ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - // A wild battle may end the walk early. Walking to a wall again - // ends in the same place, so finish the leg. - const bool interrupted = result.wild_battles > 0; - for (size_t again = 0; again < 3 && result.wild_battles > 0 && - (result.stop == WalkStop::LIMIT || result.stop == WalkStop::BLOCKED); again++ - ){ - stream.log("[Route] " + leg_label + ": a wild battle on the way; walking to the wall again."); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - } - // A wall well before the nearest one the map has: something the - // map does not know is in the way. Work out where we stopped from - // the time walked, put it on our map and plan again from there. - size_t nearest = SIZE_MAX; - for (const TilePos& pos : from){ - nearest = std::min(nearest, slide_to_wall(grid, pos, leg.direction).tiles); - } - auto tiles_moved = [&](const WalkResult& walk){ - const Milliseconds moving = walk.walked - ROUTE_START_DELAY - - (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); - return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); - }; - size_t walked_tiles = tiles_moved(result); - // A stick walk can catch a corner when we stand part of a tile - // off centre, where a D-pad tap, which moves whole tiles, gets - // through: on Route 203's stairs the walk west stopped nine - // times in a row and two taps crossed (r225a, 2026-10-31). - // Tap twice, then walk on to the wall. - if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ - const DpadPosition dpad = - leg.direction == Direction::NORTH ? DPAD_UP : - leg.direction == Direction::SOUTH ? DPAD_DOWN : - leg.direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); - for (size_t tap = 0; tap < 2; tap++){ - pbf_press_dpad(context, dpad, 160ms, 640ms); - } - context.wait_for_all_requests(); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - walked_tiles += tiles_moved(result); - } - // Someone walking about may stand in the way for a moment: wait - // and walk to the wall again before calling it an obstacle. - for (size_t again = 0; !interrupted && again < 2 && - result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++ - ){ - stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(context, 2500ms); - context.wait_for_all_requests(); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - walked_tiles += tiles_moved(result); - } - // Any stop short of the wall's distance: someone who stays put - // is talked to, and the leg is planned again from where we are. - // Within the time count's slack this may be the wall itself, - // where A does nothing and the new plan walks no further (r239a: - // a wanderer 3 tiles short passed as "about 8 of 10", and every - // later leg was off by those tiles). - if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ - // Someone who stays put: talk to them. A trainer we walked up - // to from the side starts the battle this way. - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - { - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, leg_label + ": someone is in the way; talking to them."); - fight("someone in the way"); - } - } - std::vector stopped; - for (const TilePos& pos : from){ - const std::vector path = leg_path(grid, pos, leg.direction, SIZE_MAX); - for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ - stopped.push_back(k == 0 ? pos : path[k - 1]); - } - } - std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); - if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": stopped early, and no tile fits.", stream); - } - if (stopped.size() == 1){ - const TilePos blocker = stopped[0].next(leg.direction); - grid.block(blocker); - stream.log("[Route] " + leg_label + ": something the map lacks is at " + - describe(maps, blocker) + "; marked it.", COLOR_ORANGE); - } - stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), - "stopped early after about " + std::to_string(walked_tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); - facing = leg.direction; - from = stopped; - replanned = true; - break; - } - // A leg to the wall that ran out of time is not at its wall: - // the open leg before it stopped short, and it ran along - // another row (r127a went west along Sandgem's row 13 for row - // 14, and the next leg north walked into the lab). Out of tall - // grass a stop reads, so walk on: a player still moving is - // not where the plan has him. - if (!interrupted && result.stop == WalkStop::LIMIT && route_wall_readable(grid, planned.after)){ - const WalkResult check = walk_until_dpad(stream, context, escape, {x, y}, 2000ms, leg_label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); - if (check.stop == WalkStop::DIALOG){ - // A trainer who saw us froze the walk (the "!") until - // its time ran out, then walked over (r241a: Aroma Lady - // Taylor's line crosses Route 204's corridor). Battle, - // then plan again from where a sight line crosses the leg. - route_status(stream, leg_label + ": a trainer stopped us on the way; battling."); - fight("a trainer on the way"); - // Where they saw us from is not reliable (BDSP's trainers - // stand a few tiles from Platinum's, r242a), so do not - // guess the tile: a leg to a wall ends at its wall however - // far it got. Walk on to it. - stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); - } - if (check.stop != WalkStop::BLOCKED && - !(check.stop == WalkStop::LIMIT && holding_against_wall(stream, context, x, y, leg_label + ": at the wall?")) - ){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": ran out of time short of the wall, and walking on gets " + - walk_stop_name(check.stop) + ": not where the plan has us.", stream); - } - } - }else{ - // Whole tiles on the D-pad, a tap each: a tap moves exactly - // one tile, while a stick hold timed for three moved one in - // the Ravaged Path (r235b) and the belief drifted four rows. - // The first tap of a leg was meant to turn, but a 160 ms - // D-pad tap turns and steps whichever way the player faced - // (r248a: all seven did; r246a: "south 1" out of the cave - // went two tiles, onto the Youngster's row), so the tap is - // measured like any other and a step counts for the leg. - if (facing != leg.direction){ - const DpadPosition dpad = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; - context.wait_for_all_requests(); - VideoSnapshot before = stream.video().snapshot(); - pbf_press_dpad(context, dpad, 160ms, 700ms); - context.wait_for_all_requests(); - VideoSnapshot after = stream.video().snapshot(); - if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ - turn_moved = 1; - stream.log("[Route] " + leg_label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); - } - } - if (leg.tiles > turn_moved){ - result = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - turn_moved, leg_label); - }else{ - result = WalkResult{WalkStop::LIMIT, 0ms, 0}; - } - // A wild battle starts once the step onto the grass is done, - // so the taps still count; the walk went on after it. - if (result.wild_battles > 0){ - stream.log("[Route] " + leg_label + ": " + std::to_string(result.wild_battles) + - " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); - } - // Taps that moved nothing: someone stands in the way (the - // Youngster below the Ravaged Path wanders across the row - // under the cliff, r239a, r240a). Two still taps end the walk, - // so the tiles moved are the taps less those two. Wait for - // them to move on and tap the rest; someone who stays is - // talked to, put on the map, and the leg planned again from - // the tile we stand on, which the taps give exactly. - auto taps_moved = [](const WalkResult& walk){ return walk.moved_chunks; }; - size_t moved = std::min(leg.tiles, taps_moved(result) + turn_moved); - // One wait: a wanderer in the way steps off in a few seconds; - // anything still there after that is talked to (r246a lost - // 20 s per block on three waits). - for (size_t again = 0; again < 1 && result.stop == WalkStop::BLOCKED && moved < leg.tiles; again++){ - stream.log("[Route] " + leg_label + ": blocked after " + std::to_string(moved) + " of " + - std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(context, 2500ms); - context.wait_for_all_requests(); - const WalkResult rest = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - moved, leg_label + ": the rest"); - moved = std::min(leg.tiles, moved + taps_moved(rest)); - result.stop = rest.stop; - result.walked += rest.walked; - result.wild_battles += rest.wild_battles; - } - if (result.stop == WalkStop::BLOCKED && moved < leg.tiles){ - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - { - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, leg_label + ": someone is in the way; talking to them."); - fight("someone in the way"); - } - } - std::vector stopped; - for (const TilePos& pos : from){ - const std::vector path = leg_path(grid, pos, leg.direction, moved); - stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); - } - std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); - if (stopped.size() == 1){ - const TilePos blocker = stopped[0].next(leg.direction); - grid.block(blocker); - stream.log("[Route] " + leg_label + ": something the map lacks is at " + - describe(maps, blocker) + "; marked it.", COLOR_ORANGE); - } - stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), - "blocked after " + std::to_string(moved) + " of " + std::to_string(leg.tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); - facing = leg.direction; - from = stopped; - replanned = true; - break; - } - } - facing = leg.direction; - { - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - watch_banner(*screen.frame); - } - } - std::string banner_text; - for (const std::string& read : banners){ - banner_text += (banner_text.empty() ? "\"" : ", \"") + read + "\""; - } - all_banners.insert(all_banners.end(), banners.begin(), banners.end()); - // Entering another map shows its name; a missing or wrong name means - // the walk did not end where the plan says. From tiles on several - // maps (a leg not confirmed) the banner may already have shown. - if (!map_before.empty() && !map_after.empty() && map_after != map_before && !banner_ok()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": expected to enter " + map_after + ", but the location banner read " + - (banner_text.empty() ? "nothing" : banner_text) + ".", stream); - } - if (!banner_text.empty()){ - stream.log("[Route] " + leg_label + ": location banner " + banner_text + "."); - } - // Where each leg ended, to compare with the plan after a run. - if (VideoSnapshot screen = stream.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; - Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(leg_label) + ".png"); - } - if (result.stop == WalkStop::TRANSITION){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": unexpected transition.", stream); - } - // A trainer who sees us freezes the player and walks over; the - // dialogue may open after the walk has ended. - const std::vector> paths = [&]{ - std::vector> out; - for (const TilePos& pos : from){ - out.push_back(leg_path(grid, pos, leg.direction, - leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); - } - return out; - }(); - std::vector watching; - for (size_t t = 0; t < trainers.size(); t++){ - const std::vector sight = sight_tiles(grid, trainers[t]); - for (const std::vector& path : paths){ - if (std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ - return std::find(sight.begin(), sight.end(), pos) != sight.end(); - })){ - watching.push_back(t); - break; - } - } - } - bool talking = result.stop == WalkStop::DIALOG; - if (!talking){ - ShortDialogWatcher dialog(COLOR_YELLOW); - const Milliseconds wait = watching.empty() ? 500ms : 4000ms; - talking = wait_until(stream, context, wait, {{dialog}}) == 0; - } - if (!talking){ - from = planned.after; - log_leg_done(stream, maps, label, planned, result.wild_battles); - continue; - } - - if (watching.empty()){ - // A trainer the map does not have. Fight, then plan again from - // anywhere the leg could have stopped. - std::vector anywhere = from; - for (const std::vector& path : paths){ - anywhere.insert(anywhere.end(), path.begin(), path.end()); - } - std::sort(anywhere.begin(), anywhere.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - anywhere.erase(std::unique(anywhere.begin(), anywhere.end()), anywhere.end()); - route_status(stream, label + ": a trainer the map lacks stopped us; fighting."); - fight("unknown trainer"); - if (leg.ends_at_wall){ - const Milliseconds limit = ROUTE_START_DELAY + - ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; - stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); - from = planned.after; - log_leg_done(stream, maps, label, planned, 0); - continue; - } - from = anywhere; - replanned = true; - break; - } - // Each possible start tile was stopped on the first tile of its path - // inside a watching trainer's sight. - const TrainerSight trainer = trainers[watching[0]]; - std::vector stopped; - for (const std::vector& path : paths){ - for (const TilePos& pos : path){ - bool seen = false; - for (size_t t : watching){ - const std::vector sight = sight_tiles(grid, trainers[t]); - seen |= std::find(sight.begin(), sight.end(), pos) != sight.end(); - } - if (seen){ - stopped.push_back(pos); - break; - } - } - } - if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": " + trainer.name + " talked, but no path enters their sight.", stream); - } - stream.log(ROUTE_LOG_TAG + route_trainer_line(label, trainer.name, route_tiles(maps, stopped))); - route_status(stream, "Fighting " + trainer.name + "."); - fight(trainer.name); - trainers.erase(trainers.begin() + watching[0]); - // Their sight line only says roughly where: BDSP's trainers stand a - // few tiles from Platinum's (r242a). A leg to a wall ends at its - // wall however far it got: walk on to it and carry on. An open leg - // counts its taps: the step that brought us into sight was complete. - if (leg.ends_at_wall){ - const Milliseconds limit = ROUTE_START_DELAY + - ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; - stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); - from = planned.after; - log_leg_done(stream, maps, label, planned, 0); - continue; - } - // Only the taps that moved count: the trainer freezes the player - // on the tile they saw, and the taps pressed while they walk over - // move nobody (r248a: "4 taps" were 2 steps; the belief of two - // tiles then cost a 12-tile run to a wall and 5 back). - const size_t taps = std::min(leg.tiles, result.moved_chunks + turn_moved); - std::vector by_taps; - for (const TilePos& pos : from){ - const std::vector path = leg_path(grid, pos, leg.direction, leg.tiles); - const size_t k = std::min(taps, path.size()); - by_taps.push_back(k == 0 ? pos : path[k - 1]); - } - std::sort(by_taps.begin(), by_taps.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - by_taps.erase(std::unique(by_taps.begin(), by_taps.end()), by_taps.end()); - stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), - std::to_string(taps) + " taps before the battle", route_tiles(maps, by_taps)), COLOR_ORANGE); - from = by_taps; - replanned = true; - break; - } - if (!replanned){ - // Legs whose ends span two maps skip the per-leg check; the walk - // as a whole must still have seen the banner of the map it ends on. - const std::string map_end = map_of(maps, from); - const bool changed_map = !map_start.empty() && !map_end.empty() && map_end != map_start; - if (changed_map && std::none_of(all_banners.begin(), all_banners.end(), [&](const std::string& read){ - return location_banner_matches(read, map_end); - })){ - std::string seen; - for (const std::string& read : all_banners){ - seen += (seen.empty() ? "\"" : ", \"") + read + "\""; - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": arrived at " + describe(maps, from) + " on the map, but " + map_end + - "'s location banner never showed (read " + (seen.empty() ? "nothing" : seen) + ").", stream); - } - stream.log(ROUTE_LOG_TAG + route_arrived_line(label, route_tiles(maps, from))); - return from; - } - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": too many plans.", stream); -} - - - -std::vector walk_waypoints( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::vector& waypoints, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const RobustPlanCosts& costs -){ - for (size_t i = 0; i < waypoints.size(); i++){ - const TilePos goal = waypoints[i]; - from = walk_route(stream, context, escape, maps, from, - [&](const TilePos& pos){ return pos == goal; }, - trainers, fight, - label + " (" + std::to_string(i + 1) + "/" + std::to_string(waypoints.size()) + ", to " + describe(maps, goal) + ")", - {}, costs); - } - return from; -} - -void talk_to_npc( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector& at, - const TilePos& npc, - const std::vector& sides, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label -){ - for (Direction side : sides){ - const TilePos stand = npc.next(side); - if (!standable(maps, stand)){ - continue; - } - at = walk_route(stream, context, escape, maps, at, [&](const TilePos& pos){ return pos == stand; }, - trainers, fight, label); - // Face the NPC: the opposite of the side we stand on. - const Direction face = - side == Direction::NORTH ? Direction::SOUTH : - side == Direction::SOUTH ? Direction::NORTH : - side == Direction::EAST ? Direction::WEST : Direction::EAST; - route_turn(stream, context, face); - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, label + ": talking from " + describe(maps, stand) + "."); - return; - } - route_status(stream, label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": no side of " + describe(maps, npc) + " started a dialogue.", stream); -} void finish_dialogue( VideoStream& stream, ProControllerContext& context, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index 995f341818..6654a93379 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -2,9 +2,9 @@ * * From: https://github.com/PokemonAutomation/ * - * Walks a plan made on the story maps: legs to a wall are held until the - * player stops, open legs for their length in time. Tracks every tile the - * player may be on, and plans again after a trainer stops the walk. + * What walking on the story maps is made of: walking times per tile, the + * stick for a direction, turning, talking to someone next to us, finishing + * a dialogue, and going through doors. PlannedRoute walks whole plans. * */ @@ -19,9 +19,7 @@ #include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "CommonFramework/Tools/VideoStream.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP_AutoStory_Walk.h" namespace PokemonAutomation{ @@ -41,6 +39,12 @@ constexpr Milliseconds ROUTE_START_DELAY = Milliseconds(250); // short by two tiles. 1 until running is measured to work. constexpr int ROUTE_RUN_SPEEDUP = 1; +// Walking time per tile in `direction`. +Milliseconds route_ms_per_tile(Pokemon::Direction direction); +// The left stick for a direction. Row 0 is the top of a map, and the +// stick's y is positive upward. +JoystickPosition route_stick(Pokemon::Direction direction); + // How long to hold the stick for an open leg of `tiles` tiles, for a player // already facing that way: a step once started is always finished, so stop // half a tile early. On the console a 1290ms hold west went 9 tiles and an @@ -95,64 +99,6 @@ bool route_wall_readable(const StoryWorld& grid, const std::vector; - -// Walks from any of the world tiles `from` of `maps` until every tile the -// player may be on is `goal`, and returns those tiles. Trainers in -// `trainers` who see the player are fought with `fight` and taken off the -// list. Tiles in `avoid` (where someone wanders) are planned as walls. -// Fails with a screenshot -// on a transition, a dialogue no trainer explains, a wild battle during an -// open leg, or when no plan exists. A walk from one map to another must see -// the second map's location banner on the way: a walk whose legs all ended -// on walls can still end in the wrong place (r124a saved in Sandgem Town -// after "arriving" on Route 202). -std::vector walk_route( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::function& goal, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const std::vector& avoid = {}, - const RobustPlanCosts& costs = route_plan_costs() -); - -// Walks `from` to each of `waypoints` in turn, with walk_route() for every -// stretch, and returns where the last one ended. The corners of a line a -// human walked, so the plan is that line, with the walker's handling of -// trainers and blocks on each stretch (Route 204 North, 2026-10-31). -std::vector walk_waypoints( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::vector& waypoints, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const RobustPlanCosts& costs = waypoint_plan_costs() -); - -// Walks next to the NPC on world tile `npc`, faces them and presses A, trying -// the sides in `sides` (the side of the NPC to stand on) in order until a -// dialogue opens. `at` is where the player may be, and is updated. Fails with -// a screenshot if no side starts a dialogue. A press from a diagonal tile, -// or from a tile a row off, talks to nobody, so each side is tried in turn. -void talk_to_npc( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector& at, - const Pokemon::TilePos& npc, - const std::vector& sides, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label -); - // Finishes a dialogue that has opened, however many lines it has: presses // `button` while a dialogue box shows, and returns once the overworld has // shown with no box for 1.5 s (the box blinks off between lines and during diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 6647e962b6..4c4e69c4f8 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1657,6 +1657,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp From 6a24b565fcce07886e813d4ff500daf6fe19725c Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:51:40 -0800 Subject: [PATCH 09/39] BDSP walker: walk_route() as a RouteWalker with one function per decision Code health: walk_route() was 475 lines nested 7 deep with 11 parameters, walk_waypoints() and talk_to_npc() had 10. walk_route() is now a RouteWalker class: planning, a leg to a wall, an open leg, the banner check, the trainers after a leg and each replan are their own functions, with the same steps in the same order. The trainers and their fight go together in RouteTrainers; where a walk goes is a RouteWalkPlan, WaypointWalkPlan or NpcTalk. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 1107 +++++++++-------- .../PokemonBDSP_AutoStory_PlannedRoute.h | 95 +- 2 files changed, 673 insertions(+), 529 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 5d80952053..c9c56096bc 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "Common/Cpp/PrettyPrint.h" #include "Common/Cpp/Filesystem/Filesystem.h" #include "CommonFramework/GlobalAutoPaths.h" @@ -128,522 +129,650 @@ std::string map_of(const StoryWorld& maps, const std::vector& tiles){ } -std::vector walk_route( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::function& goal, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const std::vector& avoid, - const RobustPlanCosts& costs -){ - // Our own copy, so things found in the way can be added to it. Tiles to - // avoid are ground the game lets us walk on (a trainer's line of sight), - // so they are off the map, not walls: a leg must not end "at" one, or - // the walk would run on through it. - StoryWorld grid = maps; - for (const TilePos& pos : avoid){ - if (std::find(from.begin(), from.end(), pos) == from.end()){ - grid.uncover(pos); +namespace{ + +// Sorted by row, then column, without repeats. +std::vector sorted_unique(std::vector tiles){ + std::sort(tiles.begin(), tiles.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + tiles.erase(std::unique(tiles.begin(), tiles.end()), tiles.end()); + return tiles; +} + +std::string quoted_list(const std::vector& reads){ + std::string text; + for (const std::string& read : reads){ + text += (text.empty() ? "\"" : ", \"") + read + "\""; + } + return text; +} + +// Long enough to reach the wall from the farthest tile, with room for a +// slow start; past the wall the player stands still. +Milliseconds wall_limit(const TileLeg& leg){ + const Milliseconds per_tile = route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; + return ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; +} + +// Tiles a walk to the wall moved, from the time it walked. +size_t tiles_moved(const WalkResult& walk, Milliseconds per_tile){ + const Milliseconds moving = walk.walked - ROUTE_START_DELAY - + (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); + return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); +} + +// Whether a leg's walk goes on to the next leg or the route plans again. +enum class LegNext{ + NEXT_LEG, + REPLAN, +}; + +// One planned leg as it is walked: where it goes, and the location +// banners read on the way. +struct LegWalk{ + const PlannedLeg& planned; + std::string label; + JoystickPosition stick; + std::string map_before; + std::string map_after; + std::vector banners = {}; + WalkResult result{WalkStop::LIMIT, 0ms, 0}; + // Tiles the first tap of an open leg moved (see open_leg()). + size_t turn_moved = 0; + + const TileLeg& leg() const{ + return planned.leg; + } + bool banner_ok() const{ + return std::any_of(banners.begin(), banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_after); + }); + } + // Notes the location banners the walk passes, to check map changes. A + // wild battle or a second walk to the wall may follow the banner: any + // banner seen during the leg counts. + bool watch_banner(const ImageViewRGB32& screen){ + if (banners.empty() || (!map_after.empty() && !banner_ok() && banners.size() < 8)){ + const std::string read = read_location_banner(screen); + if (!read.empty() && (banners.empty() || banners.back() != read)){ + banners.push_back(read); + } } + return false; + } + DpadWalkOptions run_watching(){ + return {.arrived = [this](const ImageViewRGB32& screen){ return watch_banner(screen); }, .pace = DpadPace::RUN}; } +}; + +// walk_route(): plans, walks each leg, and plans again from where the walk +// turned out to be when something stopped it. +class RouteWalker{ +public: + RouteWalker( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, RouteTrainers& trainers, const RouteWalkPlan& plan + ) + : m_stream(stream) + , m_context(context) + , m_escape(escape) + , m_maps(maps) + , m_grid(maps) + , m_trainers(trainers) + , m_plan(plan) + {} + + std::vector run(std::vector from); + +private: + std::vector plan_legs() const; + LegNext walk_leg(const PlannedLeg& planned, size_t number, size_t count); + std::vector arrive() const; + + // Legs to a wall. + LegNext wall_leg(LegWalk& walk); + size_t unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest); + LegNext stopped_short_of_wall(LegWalk& walk, size_t walked_tiles); + void check_at_wall(LegWalk& walk); + void on_to_wall(LegWalk& walk); + + // Open legs, tapped. + LegNext open_leg(LegWalk& walk); + void turn_tap(LegWalk& walk); + LegNext blocked_open_leg(LegWalk& walk, size_t moved); + + // After each leg. + void check_banners(LegWalk& walk); + LegNext after_leg(LegWalk& walk); + std::vector watching_trainers(const std::vector>& paths) const; + LegNext unknown_trainer(LegWalk& walk, const std::vector>& paths); + LegNext known_trainer(LegWalk& walk, const std::vector>& paths, const std::vector& watching); + + void talk_to_blocker(const std::string& leg_label); + LegNext replan_from(const LegWalk& walk, std::vector stopped, const std::string& why); + void mark_blocker(const LegWalk& walk, const std::vector& stopped); + + VideoStream& m_stream; + ProControllerContext& m_context; + const WildBattleEscape& m_escape; + const StoryWorld& m_maps; + // Our own copy, so things found in the way can be added to it. + StoryWorld m_grid; + RouteTrainers& m_trainers; + const RouteWalkPlan& m_plan; + std::vector m_from; // Which way the player faces, once a leg has set it. - std::optional facing; + std::optional m_facing; // Every banner read on the way, to check the map we arrive on against. - const std::string map_start = map_of(maps, from); - std::vector all_banners; + std::vector m_all_banners; + std::string m_map_start; +}; + +std::vector RouteWalker::run(std::vector from){ + // Tiles to avoid are ground the game lets us walk on (a trainer's line + // of sight), so they are off the map, not walls: a leg must not end + // "at" one, or the walk would run on through it. + for (const TilePos& pos : m_plan.avoid){ + if (std::find(from.begin(), from.end(), pos) == from.end()){ + m_grid.uncover(pos); + } + } + m_from = std::move(from); + m_map_start = map_of(m_maps, m_from); for (size_t plans = 0; plans < 8; plans++){ - std::optional> legs = plan_robust_route(grid, from, goal, costs); - if (!legs){ - // From a few possible tiles the exact-open-leg costs found no - // plan where the default ones did (r240a, three tiles on Route - // 204's row 27); the default plan is longer but sound. - stream.log("[Route] " + label + ": no plan with the route costs from " + describe(maps, from) + - "; trying the default costs.", COLOR_ORANGE); - legs = plan_robust_route(grid, from, goal, RobustPlanCosts()); + const std::vector legs = plan_legs(); + route_status(m_stream, m_plan.label + ": " + std::to_string(legs.size()) + " legs planned"); + log_plan(m_stream, m_maps, m_plan.label, m_from, legs); + bool replanned = false; + for (size_t number = 1; number <= legs.size() && !replanned; number++){ + replanned = walk_leg(legs[number - 1], number, legs.size()) == LegNext::REPLAN; } - if (!legs){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": no plan from " + describe(maps, from) + ".", stream); + if (!replanned){ + return arrive(); } - route_status(stream, label + ": " + std::to_string(legs->size()) + " legs planned"); - log_plan(stream, maps, label, from, *legs); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + m_plan.label + ": too many plans.", m_stream); +} - bool replanned = false; - size_t leg_number = 0; - for (const PlannedLeg& planned : *legs){ - const TileLeg& leg = planned.leg; - leg_number++; - route_status(stream, route_leg_line(leg_number, legs->size(), planned_route_leg(maps, planned))); - const double x = route_stick(leg.direction).x; - const double y = route_stick(leg.direction).y; - const std::string leg_label = label + ": " + to_string(leg); - - // Note the location banners the walk passes, to check map changes. - // A wild battle or a second walk to the wall may follow the - // banner: any banner seen during the leg counts. - const std::string map_before = map_of(maps, from); - const std::string map_after = map_of(maps, planned.after); - std::vector banners; - auto banner_ok = [&]{ - return std::any_of(banners.begin(), banners.end(), [&](const std::string& read){ - return location_banner_matches(read, map_after); - }); - }; - auto watch_banner = [&](const ImageViewRGB32& screen){ - if (banners.empty() || (!map_after.empty() && !banner_ok() && banners.size() < 8)){ - const std::string read = read_location_banner(screen); - if (!read.empty() && (banners.empty() || banners.back() != read)){ - banners.push_back(read); - } - } - return false; - }; - - WalkResult result; - // Tiles the first tap of an open leg moved (see below). - size_t turn_moved = 0; - if (leg.ends_at_wall){ - // Run (B held) on the D-pad: a leg to a wall ends at the wall - // however fast we get there, and the D-pad keeps to the row. - const Milliseconds per_tile = route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; - // Long enough to reach the wall from the farthest tile, with room - // for a slow start; past the wall the player stands still. - const Milliseconds limit = ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - // A wild battle may end the walk early. Walking to a wall again - // ends in the same place, so finish the leg. - const bool interrupted = result.wild_battles > 0; - for (size_t again = 0; again < 3 && result.wild_battles > 0 && - (result.stop == WalkStop::LIMIT || result.stop == WalkStop::BLOCKED); again++ - ){ - stream.log("[Route] " + leg_label + ": a wild battle on the way; walking to the wall again."); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - } - // A wall well before the nearest one the map has: something the - // map does not know is in the way. Work out where we stopped from - // the time walked, put it on our map and plan again from there. - size_t nearest = SIZE_MAX; - for (const TilePos& pos : from){ - nearest = std::min(nearest, slide_to_wall(grid, pos, leg.direction).tiles); - } - auto tiles_moved = [&](const WalkResult& walk){ - const Milliseconds moving = walk.walked - ROUTE_START_DELAY - - (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); - return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); - }; - size_t walked_tiles = tiles_moved(result); - // A stick walk can catch a corner when we stand part of a tile - // off centre, where a D-pad tap, which moves whole tiles, gets - // through: on Route 203's stairs the walk west stopped nine - // times in a row and two taps crossed (r225a, 2026-10-31). - // Tap twice, then walk on to the wall. - if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ - const DpadPosition dpad = - leg.direction == Direction::NORTH ? DPAD_UP : - leg.direction == Direction::SOUTH ? DPAD_DOWN : - leg.direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); - for (size_t tap = 0; tap < 2; tap++){ - pbf_press_dpad(context, dpad, 160ms, 640ms); - } - context.wait_for_all_requests(); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - walked_tiles += tiles_moved(result); - } - // Someone walking about may stand in the way for a moment: wait - // and walk to the wall again before calling it an obstacle. - for (size_t again = 0; !interrupted && again < 2 && - result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++ - ){ - stream.log("[Route] " + leg_label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(context, 2500ms); - context.wait_for_all_requests(); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label, {.arrived = watch_banner, .pace = DpadPace::RUN}); - walked_tiles += tiles_moved(result); - } - // Any stop short of the wall's distance: someone who stays put - // is talked to, and the leg is planned again from where we are. - // Within the time count's slack this may be the wall itself, - // where A does nothing and the new plan walks no further (r239a: - // a wanderer 3 tiles short passed as "about 8 of 10", and every - // later leg was off by those tiles). - if (!interrupted && result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ - // Someone who stays put: talk to them. A trainer we walked up - // to from the side starts the battle this way. - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - { - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, leg_label + ": someone is in the way; talking to them."); - fight("someone in the way"); - } - } - std::vector stopped; - for (const TilePos& pos : from){ - const std::vector path = leg_path(grid, pos, leg.direction, SIZE_MAX); - for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ - stopped.push_back(k == 0 ? pos : path[k - 1]); - } - } - std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); - if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": stopped early, and no tile fits.", stream); - } - if (stopped.size() == 1){ - const TilePos blocker = stopped[0].next(leg.direction); - grid.block(blocker); - stream.log("[Route] " + leg_label + ": something the map lacks is at " + - describe(maps, blocker) + "; marked it.", COLOR_ORANGE); - } - stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), - "stopped early after about " + std::to_string(walked_tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); - facing = leg.direction; - from = stopped; - replanned = true; - break; - } - // A leg to the wall that ran out of time is not at its wall: - // the open leg before it stopped short, and it ran along - // another row (r127a went west along Sandgem's row 13 for row - // 14, and the next leg north walked into the lab). Out of tall - // grass a stop reads, so walk on: a player still moving is - // not where the plan has him. - if (!interrupted && result.stop == WalkStop::LIMIT && route_wall_readable(grid, planned.after)){ - const WalkResult check = walk_until_dpad(stream, context, escape, {x, y}, 2000ms, leg_label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); - if (check.stop == WalkStop::DIALOG){ - // A trainer who saw us froze the walk (the "!") until - // its time ran out, then walked over (r241a: Aroma Lady - // Taylor's line crosses Route 204's corridor). Battle, - // then plan again from where a sight line crosses the leg. - route_status(stream, leg_label + ": a trainer stopped us on the way; battling."); - fight("a trainer on the way"); - // Where they saw us from is not reliable (BDSP's trainers - // stand a few tiles from Platinum's, r242a), so do not - // guess the tile: a leg to a wall ends at its wall however - // far it got. Walk on to it. - stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - result = walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); - } - if (check.stop != WalkStop::BLOCKED && - !(check.stop == WalkStop::LIMIT && holding_against_wall(stream, context, x, y, leg_label + ": at the wall?")) - ){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": ran out of time short of the wall, and walking on gets " + - walk_stop_name(check.stop) + ": not where the plan has us.", stream); - } - } - }else{ - // Whole tiles on the D-pad, a tap each: a tap moves exactly - // one tile, while a stick hold timed for three moved one in - // the Ravaged Path (r235b) and the belief drifted four rows. - // The first tap of a leg was meant to turn, but a 160 ms - // D-pad tap turns and steps whichever way the player faced - // (r248a: all seven did; r246a: "south 1" out of the cave - // went two tiles, onto the Youngster's row), so the tap is - // measured like any other and a step counts for the leg. - if (facing != leg.direction){ - const DpadPosition dpad = x < 0 ? DPAD_LEFT : x > 0 ? DPAD_RIGHT : y > 0 ? DPAD_UP : DPAD_DOWN; - context.wait_for_all_requests(); - VideoSnapshot before = stream.video().snapshot(); - pbf_press_dpad(context, dpad, 160ms, 700ms); - context.wait_for_all_requests(); - VideoSnapshot after = stream.video().snapshot(); - if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ - turn_moved = 1; - stream.log("[Route] " + leg_label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); - } - } - if (leg.tiles > turn_moved){ - result = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - turn_moved, leg_label); - }else{ - result = WalkResult{WalkStop::LIMIT, 0ms, 0}; - } - // A wild battle starts once the step onto the grass is done, - // so the taps still count; the walk went on after it. - if (result.wild_battles > 0){ - stream.log("[Route] " + leg_label + ": " + std::to_string(result.wild_battles) + - " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); - } - // Taps that moved nothing: someone stands in the way (the - // Youngster below the Ravaged Path wanders across the row - // under the cliff, r239a, r240a). Two still taps end the walk, - // so the tiles moved are the taps less those two. Wait for - // them to move on and tap the rest; someone who stays is - // talked to, put on the map, and the leg planned again from - // the tile we stand on, which the taps give exactly. - auto taps_moved = [](const WalkResult& walk){ return walk.moved_chunks; }; - size_t moved = std::min(leg.tiles, taps_moved(result) + turn_moved); - // One wait: a wanderer in the way steps off in a few seconds; - // anything still there after that is talked to (r246a lost - // 20 s per block on three waits). - for (size_t again = 0; again < 1 && result.stop == WalkStop::BLOCKED && moved < leg.tiles; again++){ - stream.log("[Route] " + leg_label + ": blocked after " + std::to_string(moved) + " of " + - std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(context, 2500ms); - context.wait_for_all_requests(); - const WalkResult rest = walk_tiles_dpad(stream, context, escape, {x, y}, leg.tiles - moved, leg_label + ": the rest"); - moved = std::min(leg.tiles, moved + taps_moved(rest)); - result.stop = rest.stop; - result.walked += rest.walked; - result.wild_battles += rest.wild_battles; - } - if (result.stop == WalkStop::BLOCKED && moved < leg.tiles){ - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - { - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, leg_label + ": someone is in the way; talking to them."); - fight("someone in the way"); - } - } - std::vector stopped; - for (const TilePos& pos : from){ - const std::vector path = leg_path(grid, pos, leg.direction, moved); - stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); - } - std::sort(stopped.begin(), stopped.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - stopped.erase(std::unique(stopped.begin(), stopped.end()), stopped.end()); - if (stopped.size() == 1){ - const TilePos blocker = stopped[0].next(leg.direction); - grid.block(blocker); - stream.log("[Route] " + leg_label + ": something the map lacks is at " + - describe(maps, blocker) + "; marked it.", COLOR_ORANGE); - } - stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), - "blocked after " + std::to_string(moved) + " of " + std::to_string(leg.tiles) + " tiles", route_tiles(maps, stopped)), COLOR_ORANGE); - facing = leg.direction; - from = stopped; - replanned = true; - break; - } - } - facing = leg.direction; - { - context.wait_for_all_requests(); - if (VideoSnapshot screen = stream.video().snapshot()){ - watch_banner(*screen.frame); - } - } - std::string banner_text; - for (const std::string& read : banners){ - banner_text += (banner_text.empty() ? "\"" : ", \"") + read + "\""; - } - all_banners.insert(all_banners.end(), banners.begin(), banners.end()); - // Entering another map shows its name; a missing or wrong name means - // the walk did not end where the plan says. From tiles on several - // maps (a leg not confirmed) the banner may already have shown. - if (!map_before.empty() && !map_after.empty() && map_after != map_before && !banner_ok()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": expected to enter " + map_after + ", but the location banner read " + - (banner_text.empty() ? "nothing" : banner_text) + ".", stream); - } - if (!banner_text.empty()){ - stream.log("[Route] " + leg_label + ": location banner " + banner_text + "."); - } - // Where each leg ended, to compare with the plan after a run. - if (VideoSnapshot screen = stream.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; - Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(leg_label) + ".png"); - } - if (result.stop == WalkStop::TRANSITION){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": unexpected transition.", stream); - } - // A trainer who sees us freezes the player and walks over; the - // dialogue may open after the walk has ended. - const std::vector> paths = [&]{ - std::vector> out; - for (const TilePos& pos : from){ - out.push_back(leg_path(grid, pos, leg.direction, - leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); - } - return out; - }(); - std::vector watching; - for (size_t t = 0; t < trainers.size(); t++){ - const std::vector sight = sight_tiles(grid, trainers[t]); - for (const std::vector& path : paths){ - if (std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ - return std::find(sight.begin(), sight.end(), pos) != sight.end(); - })){ - watching.push_back(t); - break; - } - } - } - bool talking = result.stop == WalkStop::DIALOG; - if (!talking){ - ShortDialogWatcher dialog(COLOR_YELLOW); - const Milliseconds wait = watching.empty() ? 500ms : 4000ms; - talking = wait_until(stream, context, wait, {{dialog}}) == 0; - } - if (!talking){ - from = planned.after; - log_leg_done(stream, maps, label, planned, result.wild_battles); - continue; - } +std::vector RouteWalker::plan_legs() const{ + std::optional> legs = plan_robust_route(m_grid, m_from, m_plan.goal, m_plan.costs); + if (!legs){ + // From a few possible tiles the exact-open-leg costs found no + // plan where the default ones did (r240a, three tiles on Route + // 204's row 27); the default plan is longer but sound. + m_stream.log("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + + "; trying the default costs.", COLOR_ORANGE); + legs = plan_robust_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); + } + if (!legs){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + m_plan.label + ": no plan from " + describe(m_maps, m_from) + ".", m_stream); + } + return std::move(*legs); +} - if (watching.empty()){ - // A trainer the map does not have. Fight, then plan again from - // anywhere the leg could have stopped. - std::vector anywhere = from; - for (const std::vector& path : paths){ - anywhere.insert(anywhere.end(), path.begin(), path.end()); - } - std::sort(anywhere.begin(), anywhere.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - anywhere.erase(std::unique(anywhere.begin(), anywhere.end()), anywhere.end()); - route_status(stream, label + ": a trainer the map lacks stopped us; fighting."); - fight("unknown trainer"); - if (leg.ends_at_wall){ - const Milliseconds limit = ROUTE_START_DELAY + - route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; - stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); - from = planned.after; - log_leg_done(stream, maps, label, planned, 0); - continue; - } - from = anywhere; - replanned = true; - break; - } - // Each possible start tile was stopped on the first tile of its path - // inside a watching trainer's sight. - const TrainerSight trainer = trainers[watching[0]]; - std::vector stopped; - for (const std::vector& path : paths){ - for (const TilePos& pos : path){ - bool seen = false; - for (size_t t : watching){ - const std::vector sight = sight_tiles(grid, trainers[t]); - seen |= std::find(sight.begin(), sight.end(), pos) != sight.end(); - } - if (seen){ - stopped.push_back(pos); - break; - } - } - } - if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - leg_label + ": " + trainer.name + " talked, but no path enters their sight.", stream); - } - stream.log(ROUTE_LOG_TAG + route_trainer_line(label, trainer.name, route_tiles(maps, stopped))); - route_status(stream, "Fighting " + trainer.name + "."); - fight(trainer.name); - trainers.erase(trainers.begin() + watching[0]); - // Their sight line only says roughly where: BDSP's trainers stand a - // few tiles from Platinum's (r242a). A leg to a wall ends at its - // wall however far it got: walk on to it and carry on. An open leg - // counts its taps: the step that brought us into sight was complete. - if (leg.ends_at_wall){ - const Milliseconds limit = ROUTE_START_DELAY + - route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP * leg.tiles * 5 / 4 + 1000ms; - stream.log("[Route] " + leg_label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(stream, context, escape, {x, y}, limit, leg_label + ": on to the wall", {.arrived = watch_banner, .pace = DpadPace::RUN}); - from = planned.after; - log_leg_done(stream, maps, label, planned, 0); - continue; - } - // Only the taps that moved count: the trainer freezes the player - // on the tile they saw, and the taps pressed while they walk over - // move nobody (r248a: "4 taps" were 2 steps; the belief of two - // tiles then cost a 12-tile run to a wall and 5 back). - const size_t taps = std::min(leg.tiles, result.moved_chunks + turn_moved); - std::vector by_taps; - for (const TilePos& pos : from){ - const std::vector path = leg_path(grid, pos, leg.direction, leg.tiles); - const size_t k = std::min(taps, path.size()); - by_taps.push_back(k == 0 ? pos : path[k - 1]); - } - std::sort(by_taps.begin(), by_taps.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; +LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t count){ + route_status(m_stream, route_leg_line(number, count, planned_route_leg(m_maps, planned))); + LegWalk walk{ + .planned = planned, + .label = m_plan.label + ": " + to_string(planned.leg), + .stick = route_stick(planned.leg.direction), + .map_before = map_of(m_maps, m_from), + .map_after = map_of(m_maps, planned.after), + }; + const LegNext next = planned.leg.ends_at_wall ? wall_leg(walk) : open_leg(walk); + if (next == LegNext::REPLAN){ + return next; + } + m_facing = planned.leg.direction; + check_banners(walk); + if (walk.result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": unexpected transition.", m_stream); + } + return after_leg(walk); +} + +// Legs whose ends span two maps skip the per-leg check; the walk as a +// whole must still have seen the banner of the map it ends on. +std::vector RouteWalker::arrive() const{ + const std::string map_end = map_of(m_maps, m_from); + const bool changed_map = !m_map_start.empty() && !map_end.empty() && map_end != m_map_start; + const bool seen = std::any_of(m_all_banners.begin(), m_all_banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_end); + }); + if (changed_map && !seen){ + const std::string reads = quoted_list(m_all_banners); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + + "'s location banner never showed (read " + (reads.empty() ? "nothing" : reads) + ").", m_stream); + } + m_stream.log(ROUTE_LOG_TAG + route_arrived_line(m_plan.label, route_tiles(m_maps, m_from))); + return m_from; +} + + +// Run (B held) on the D-pad: a leg to a wall ends at the wall however fast +// we get there, and the D-pad keeps to the row. +LegNext RouteWalker::wall_leg(LegWalk& walk){ + const TileLeg& leg = walk.leg(); + const Milliseconds limit = wall_limit(leg); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + // A wild battle may end the walk early. Walking to a wall again ends in + // the same place, so finish the leg. + const bool interrupted = walk.result.wild_battles > 0; + for (size_t again = 0; again < 3 && walk.result.wild_battles > 0 && + (walk.result.stop == WalkStop::LIMIT || walk.result.stop == WalkStop::BLOCKED); again++ + ){ + m_stream.log("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again."); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + } + if (interrupted){ + return LegNext::NEXT_LEG; + } + // A wall well before the nearest one the map has: something the map + // does not know is in the way. Work out where we stopped from the time + // walked, put it on our map and plan again from there. + size_t nearest = SIZE_MAX; + for (const TilePos& pos : m_from){ + nearest = std::min(nearest, slide_to_wall(m_grid, pos, leg.direction).tiles); + } + size_t walked_tiles = tiles_moved(walk.result, route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP); + walked_tiles = unstick_wall_leg(walk, walked_tiles, nearest); + // Any stop short of the wall's distance: someone who stays put is + // talked to, and the leg is planned again from where we are. Within the + // time count's slack this may be the wall itself, where A does nothing + // and the new plan walks no further (r239a: a wanderer 3 tiles short + // passed as "about 8 of 10", and every later leg was off by those tiles). + if (walk.result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ + return stopped_short_of_wall(walk, walked_tiles); + } + // A leg to the wall that ran out of time is not at its wall: the open + // leg before it stopped short, and it ran along another row (r127a went + // west along Sandgem's row 13 for row 14, and the next leg north walked + // into the lab). Out of tall grass a stop reads, so walk on: a player + // still moving is not where the plan has him. + if (walk.result.stop == WalkStop::LIMIT && route_wall_readable(m_grid, walk.planned.after)){ + check_at_wall(walk); + } + return LegNext::NEXT_LEG; +} + +// A walk to the wall that stopped well short of it: tap through a corner, +// then wait for someone in the way. Returns the tiles walked by then. +size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest){ + const Milliseconds per_tile = route_ms_per_tile(walk.leg().direction) / ROUTE_RUN_SPEEDUP; + const Milliseconds limit = wall_limit(walk.leg()); + // A stick walk can catch a corner when we stand part of a tile off + // centre, where a D-pad tap, which moves whole tiles, gets through: on + // Route 203's stairs the walk west stopped nine times in a row and two + // taps crossed (r225a, 2026-10-31). Tap twice, then walk on to the wall. + if (walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ + const Direction direction = walk.leg().direction; + const DpadPosition dpad = + direction == Direction::NORTH ? DPAD_UP : + direction == Direction::SOUTH ? DPAD_DOWN : + direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); + for (size_t tap = 0; tap < 2; tap++){ + pbf_press_dpad(m_context, dpad, 160ms, 640ms); + } + m_context.wait_for_all_requests(); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + walked_tiles += tiles_moved(walk.result, per_tile); + } + // Someone walking about may stand in the way for a moment: wait and + // walk to the wall again before calling it an obstacle. + for (size_t again = 0; again < 2 && walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++){ + m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(m_context, 2500ms); + m_context.wait_for_all_requests(); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + walked_tiles += tiles_moved(walk.result, per_tile); + } + return walked_tiles; +} + +LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ + // Someone who stays put: talk to them. A trainer we walked up to from + // the side starts the battle this way. + talk_to_blocker(walk.label); + std::vector stopped; + for (const TilePos& pos : m_from){ + const std::vector path = leg_path(m_grid, pos, walk.leg().direction, SIZE_MAX); + for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ + stopped.push_back(k == 0 ? pos : path[k - 1]); + } + } + stopped = sorted_unique(std::move(stopped)); + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": stopped early, and no tile fits.", m_stream); + } + mark_blocker(walk, stopped); + return replan_from(walk, std::move(stopped), "stopped early after about " + std::to_string(walked_tiles) + " tiles"); +} + +void RouteWalker::check_at_wall(LegWalk& walk){ + const WalkResult check = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, 2000ms, + walk.label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); + if (check.stop == WalkStop::DIALOG){ + // A trainer who saw us froze the walk (the "!") until its time ran + // out, then walked over (r241a: Aroma Lady Taylor's line crosses + // Route 204's corridor). Battle, then walk on to the wall: where + // they saw us from is not reliable (BDSP's trainers stand a few + // tiles from Platinum's, r242a), and a leg to a wall ends at its + // wall however far it got. + route_status(m_stream, walk.label + ": a trainer stopped us on the way; battling."); + m_trainers.fight("a trainer on the way"); + m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.leg()), + walk.label + ": on to the wall", walk.run_watching()); + } + if (check.stop != WalkStop::BLOCKED && + !(check.stop == WalkStop::LIMIT && holding_against_wall(m_stream, m_context, walk.stick.x, walk.stick.y, walk.label + ": at the wall?")) + ){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": ran out of time short of the wall, and walking on gets " + + walk_stop_name(check.stop) + ": not where the plan has us.", m_stream); + } +} + +// After a battle on a leg to a wall: a leg to a wall ends at its wall +// however far it got, so walk on to it and carry on. +void RouteWalker::on_to_wall(LegWalk& walk){ + m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.leg()), + walk.label + ": on to the wall", walk.run_watching()); + m_from = walk.planned.after; + log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, 0); +} + + +// Whole tiles on the D-pad, a tap each: a tap moves exactly one tile, while +// a stick hold timed for three moved one in the Ravaged Path (r235b) and the +// belief drifted four rows. +LegNext RouteWalker::open_leg(LegWalk& walk){ + const TileLeg& leg = walk.leg(); + if (m_facing != leg.direction){ + turn_tap(walk); + } + if (leg.tiles > walk.turn_moved){ + walk.result = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - walk.turn_moved, walk.label); + } + // A wild battle starts once the step onto the grass is done, so the + // taps still count; the walk went on after it. + if (walk.result.wild_battles > 0){ + m_stream.log("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + + " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); + } + // Taps that moved nothing: someone stands in the way (the Youngster + // below the Ravaged Path wanders across the row under the cliff, r239a, + // r240a). Two still taps end the walk, so the tiles moved are the taps + // less those two. Wait once for them to move on and tap the rest (r246a + // lost 20 s per block on three waits); someone who stays is talked to, + // put on the map, and the leg planned again from the tile we stand on, + // which the taps give exactly. + size_t moved = std::min(leg.tiles, walk.result.moved_chunks + walk.turn_moved); + if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ + m_stream.log("[Route] " + walk.label + ": blocked after " + std::to_string(moved) + " of " + + std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(m_context, 2500ms); + m_context.wait_for_all_requests(); + const WalkResult rest = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - moved, walk.label + ": the rest"); + moved = std::min(leg.tiles, moved + rest.moved_chunks); + walk.result.stop = rest.stop; + walk.result.walked += rest.walked; + walk.result.wild_battles += rest.wild_battles; + } + if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ + return blocked_open_leg(walk, moved); + } + return LegNext::NEXT_LEG; +} + +// The first tap of a leg was meant to turn, but a 160 ms D-pad tap turns +// and steps whichever way the player faced (r248a: all seven did; r246a: +// "south 1" out of the cave went two tiles, onto the Youngster's row), so +// the tap is measured like any other and a step counts for the leg. +void RouteWalker::turn_tap(LegWalk& walk){ + const DpadPosition dpad = walk.stick.x < 0 ? DPAD_LEFT : walk.stick.x > 0 ? DPAD_RIGHT : walk.stick.y > 0 ? DPAD_UP : DPAD_DOWN; + m_context.wait_for_all_requests(); + VideoSnapshot before = m_stream.video().snapshot(); + pbf_press_dpad(m_context, dpad, 160ms, 700ms); + m_context.wait_for_all_requests(); + VideoSnapshot after = m_stream.video().snapshot(); + if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ + walk.turn_moved = 1; + m_stream.log("[Route] " + walk.label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); + } +} + +LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ + talk_to_blocker(walk.label); + std::vector stopped; + for (const TilePos& pos : m_from){ + const std::vector path = leg_path(m_grid, pos, walk.leg().direction, moved); + stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); + } + stopped = sorted_unique(std::move(stopped)); + mark_blocker(walk, stopped); + return replan_from(walk, std::move(stopped), + "blocked after " + std::to_string(moved) + " of " + std::to_string(walk.leg().tiles) + " tiles"); +} + + +// Entering another map shows its name; a missing or wrong name means the +// walk did not end where the plan says. From tiles on several maps (a leg +// not confirmed) the banner may already have shown. +void RouteWalker::check_banners(LegWalk& walk){ + m_context.wait_for_all_requests(); + if (VideoSnapshot screen = m_stream.video().snapshot()){ + walk.watch_banner(*screen.frame); + } + const std::string banner_text = quoted_list(walk.banners); + m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); + if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !walk.banner_ok()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + + (banner_text.empty() ? "nothing" : banner_text) + ".", m_stream); + } + if (!banner_text.empty()){ + m_stream.log("[Route] " + walk.label + ": location banner " + banner_text + "."); + } + // Where each leg ended, to compare with the plan after a run. + if (VideoSnapshot screen = m_stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(walk.label) + ".png"); + } +} + +// A trainer who sees us freezes the player and walks over; the dialogue +// may open after the walk has ended. +LegNext RouteWalker::after_leg(LegWalk& walk){ + const TileLeg& leg = walk.leg(); + std::vector> paths; + for (const TilePos& pos : m_from){ + paths.push_back(leg_path(m_grid, pos, leg.direction, leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); + } + const std::vector watching = watching_trainers(paths); + bool talking = walk.result.stop == WalkStop::DIALOG; + if (!talking){ + ShortDialogWatcher dialog(COLOR_YELLOW); + const Milliseconds wait = watching.empty() ? 500ms : 4000ms; + talking = wait_until(m_stream, m_context, wait, {{dialog}}) == 0; + } + if (!talking){ + m_from = walk.planned.after; + log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, walk.result.wild_battles); + return LegNext::NEXT_LEG; + } + if (watching.empty()){ + return unknown_trainer(walk, paths); + } + return known_trainer(walk, paths, watching); +} + +// The trainers whose sight one of the leg's possible paths enters. +std::vector RouteWalker::watching_trainers(const std::vector>& paths) const{ + std::vector watching; + for (size_t t = 0; t < m_trainers.sights.size(); t++){ + const std::vector sight = sight_tiles(m_grid, m_trainers.sights[t]); + const bool seen = std::any_of(paths.begin(), paths.end(), [&](const std::vector& path){ + return std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ + return std::find(sight.begin(), sight.end(), pos) != sight.end(); }); - by_taps.erase(std::unique(by_taps.begin(), by_taps.end()), by_taps.end()); - stream.log(ROUTE_LOG_TAG + route_replan_line(label, route_log_leg(leg), - std::to_string(taps) + " taps before the battle", route_tiles(maps, by_taps)), COLOR_ORANGE); - from = by_taps; - replanned = true; - break; + }); + if (seen){ + watching.push_back(t); } - if (!replanned){ - // Legs whose ends span two maps skip the per-leg check; the walk - // as a whole must still have seen the banner of the map it ends on. - const std::string map_end = map_of(maps, from); - const bool changed_map = !map_start.empty() && !map_end.empty() && map_end != map_start; - if (changed_map && std::none_of(all_banners.begin(), all_banners.end(), [&](const std::string& read){ - return location_banner_matches(read, map_end); - })){ - std::string seen; - for (const std::string& read : all_banners){ - seen += (seen.empty() ? "\"" : ", \"") + read + "\""; - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": arrived at " + describe(maps, from) + " on the map, but " + map_end + - "'s location banner never showed (read " + (seen.empty() ? "nothing" : seen) + ").", stream); - } - stream.log(ROUTE_LOG_TAG + route_arrived_line(label, route_tiles(maps, from))); - return from; + } + return watching; +} + +// A trainer the map does not have. Fight, then plan again from anywhere +// the leg could have stopped. +LegNext RouteWalker::unknown_trainer(LegWalk& walk, const std::vector>& paths){ + std::vector anywhere = m_from; + for (const std::vector& path : paths){ + anywhere.insert(anywhere.end(), path.begin(), path.end()); + } + anywhere = sorted_unique(std::move(anywhere)); + route_status(m_stream, m_plan.label + ": a trainer the map lacks stopped us; fighting."); + m_trainers.fight("unknown trainer"); + if (walk.leg().ends_at_wall){ + on_to_wall(walk); + return LegNext::NEXT_LEG; + } + m_from = std::move(anywhere); + return LegNext::REPLAN; +} + +LegNext RouteWalker::known_trainer(LegWalk& walk, const std::vector>& paths, const std::vector& watching){ + // Each possible start tile was stopped on the first tile of its path + // inside a watching trainer's sight. + const TrainerSight trainer = m_trainers.sights[watching[0]]; + std::vector stopped; + for (const std::vector& path : paths){ + const auto first_seen = std::find_if(path.begin(), path.end(), [&](const TilePos& pos){ + return std::any_of(watching.begin(), watching.end(), [&](size_t t){ + const std::vector sight = sight_tiles(m_grid, m_trainers.sights[t]); + return std::find(sight.begin(), sight.end(), pos) != sight.end(); + }); + }); + if (first_seen != path.end()){ + stopped.push_back(*first_seen); } } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": too many plans.", stream); + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": " + trainer.name + " talked, but no path enters their sight.", m_stream); + } + m_stream.log(ROUTE_LOG_TAG + route_trainer_line(m_plan.label, trainer.name, route_tiles(m_maps, stopped))); + route_status(m_stream, "Fighting " + trainer.name + "."); + m_trainers.fight(trainer.name); + m_trainers.sights.erase(m_trainers.sights.begin() + watching[0]); + // Their sight line only says roughly where: BDSP's trainers stand a few + // tiles from Platinum's (r242a). A leg to a wall walks on to its wall. + if (walk.leg().ends_at_wall){ + on_to_wall(walk); + return LegNext::NEXT_LEG; + } + // An open leg counts only the taps that moved: the trainer freezes the + // player on the tile they saw, and the taps pressed while they walk + // over move nobody (r248a: "4 taps" were 2 steps; the belief of two + // tiles then cost a 12-tile run to a wall and 5 back). + const size_t taps = std::min(walk.leg().tiles, walk.result.moved_chunks + walk.turn_moved); + std::vector by_taps; + for (const TilePos& pos : m_from){ + const std::vector path = leg_path(m_grid, pos, walk.leg().direction, walk.leg().tiles); + const size_t k = std::min(taps, path.size()); + by_taps.push_back(k == 0 ? pos : path[k - 1]); + } + return replan_from(walk, sorted_unique(std::move(by_taps)), std::to_string(taps) + " taps before the battle"); +} + + +// Presses A at whoever stopped the walk; a trainer we walked up to from +// the side starts the battle this way. +void RouteWalker::talk_to_blocker(const std::string& leg_label){ + pbf_press_button(m_context, BUTTON_A, 80ms, 200ms); + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(m_stream, m_context, 2000ms, {{dialog}}) == 0){ + route_status(m_stream, leg_label + ": someone is in the way; talking to them."); + m_trainers.fight("someone in the way"); + } +} + +// One tile we may stand on: what stopped us is on the next tile, and goes +// on our map. +void RouteWalker::mark_blocker(const LegWalk& walk, const std::vector& stopped){ + if (stopped.size() != 1){ + return; + } + const TilePos blocker = stopped[0].next(walk.leg().direction); + m_grid.block(blocker); + m_stream.log("[Route] " + walk.label + ": something the map lacks is at " + + describe(m_maps, blocker) + "; marked it.", COLOR_ORANGE); +} + +LegNext RouteWalker::replan_from(const LegWalk& walk, std::vector stopped, const std::string& why){ + m_stream.log(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.leg()), why, route_tiles(m_maps, stopped)), COLOR_ORANGE); + m_facing = walk.leg().direction; + m_from = std::move(stopped); + return LegNext::REPLAN; +} + } +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +){ + return RouteWalker(stream, context, escape, maps, trainers, plan).run(std::move(from)); +} std::vector walk_waypoints( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::vector& waypoints, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const RobustPlanCosts& costs + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const WaypointWalkPlan& plan ){ - for (size_t i = 0; i < waypoints.size(); i++){ - const TilePos goal = waypoints[i]; - from = walk_route(stream, context, escape, maps, from, - [&](const TilePos& pos){ return pos == goal; }, - trainers, fight, - label + " (" + std::to_string(i + 1) + "/" + std::to_string(waypoints.size()) + ", to " + describe(maps, goal) + ")", - {}, costs); + for (size_t i = 0; i < plan.waypoints.size(); i++){ + const TilePos goal = plan.waypoints[i]; + from = walk_route(stream, context, escape, maps, from, trainers, { + .goal = [goal](const TilePos& pos){ return pos == goal; }, + .label = plan.label + " (" + std::to_string(i + 1) + "/" + std::to_string(plan.waypoints.size()) + + ", to " + describe(maps, goal) + ")", + .avoid = {}, + .costs = plan.costs, + }); } return from; } void talk_to_npc( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector& at, - const TilePos& npc, - const std::vector& sides, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label + const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk ){ - for (Direction side : sides){ - const TilePos stand = npc.next(side); + for (Direction side : talk.sides){ + const TilePos stand = talk.npc.next(side); if (!standable(maps, stand)){ continue; } - at = walk_route(stream, context, escape, maps, at, [&](const TilePos& pos){ return pos == stand; }, - trainers, fight, label); + at = walk_route(stream, context, escape, maps, at, trainers, { + .goal = [stand](const TilePos& pos){ return pos == stand; }, + .label = talk.label, + .avoid = {}, + .costs = route_plan_costs(), + }); // Face the NPC: the opposite of the side we stand on. const Direction face = side == Direction::NORTH ? Direction::SOUTH : @@ -654,13 +783,13 @@ void talk_to_npc( context.wait_for_all_requests(); ShortDialogWatcher dialog(COLOR_CYAN); if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, label + ": talking from " + describe(maps, stand) + "."); + route_status(stream, talk.label + ": talking from " + describe(maps, stand) + "."); return; } - route_status(stream, label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); + route_status(stream, talk.label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); } OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": no side of " + describe(maps, npc) + " started a dialogue.", stream); + talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue.", stream); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h index 6950867b70..e6d38f1b5f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -31,57 +31,72 @@ namespace PokemonBDSP{ // overworld either way. using RouteTrainerFight = std::function; +// The trainers a route walk may meet, and how to fight them. A trainer who +// is fought is taken off `sights`. +struct RouteTrainers{ + std::vector sights; + RouteTrainerFight fight; +}; + +// Where walk_route() goes: until every tile the player may be on is +// `goal`. Tiles in `avoid` (where someone wanders, a trainer's sight) are +// planned as off the map. +struct RouteWalkPlan{ + std::function goal; + std::string label; + std::vector avoid = {}; + RobustPlanCosts costs = route_plan_costs(); +}; + // Walks from any of the world tiles `from` of `maps` until every tile the -// player may be on is `goal`, and returns those tiles. Trainers in -// `trainers` who see the player are fought with `fight` and taken off the -// list. Tiles in `avoid` (where someone wanders) are planned as walls. -// Fails with a screenshot -// on a transition, a dialogue no trainer explains, a wild battle during an -// open leg, or when no plan exists. A walk from one map to another must see -// the second map's location banner on the way: a walk whose legs all ended -// on walls can still end in the wrong place (r124a saved in Sandgem Town -// after "arriving" on Route 202). +// player may be on is `plan.goal`, and returns those tiles. Trainers who +// see the player are fought and taken off `trainers`. Fails with a +// screenshot on a transition, a dialogue no trainer explains, or when no +// plan exists. A walk from one map to another must see the second map's +// location banner on the way: a walk whose legs all ended on walls can +// still end in the wrong place (r124a saved in Sandgem Town after +// "arriving" on Route 202). std::vector walk_route( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::function& goal, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const std::vector& avoid = {}, - const RobustPlanCosts& costs = route_plan_costs() + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan ); -// Walks `from` to each of `waypoints` in turn, with walk_route() for every -// stretch, and returns where the last one ended. The corners of a line a -// human walked, so the plan is that line, with the walker's handling of -// trainers and blocks on each stretch (Route 204 North, 2026-10-31). +// The corners of a line a human walked, and the costs to plan each +// stretch with. +struct WaypointWalkPlan{ + std::vector waypoints; + std::string label; + RobustPlanCosts costs = waypoint_plan_costs(); +}; + +// Walks `from` to each of `plan.waypoints` in turn, with walk_route() for +// every stretch, and returns where the last one ended: the plan is the +// line, with the walker's handling of trainers and blocks on each stretch +// (Route 204 North, 2026-10-31). std::vector walk_waypoints( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector from, - const std::vector& waypoints, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label, - const RobustPlanCosts& costs = waypoint_plan_costs() + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const WaypointWalkPlan& plan ); -// Walks next to the NPC on world tile `npc`, faces them and presses A, trying -// the sides in `sides` (the side of the NPC to stand on) in order until a -// dialogue opens. `at` is where the player may be, and is updated. Fails with -// a screenshot if no side starts a dialogue. A press from a diagonal tile, -// or from a tile a row off, talks to nobody, so each side is tried in turn. +// Whom talk_to_npc() talks to: the NPC on world tile `npc`, from the sides +// in `sides` (the side of the NPC to stand on), in order. +struct NpcTalk{ + Pokemon::TilePos npc; + std::vector sides; + std::string label; +}; + +// Walks next to `talk.npc`, faces them and presses A, trying each side +// until a dialogue opens. `at` is where the player may be, and is updated. +// Fails with a screenshot if no side starts a dialogue. A press from a +// diagonal tile, or from a tile a row off, talks to nobody, so each side is +// tried in turn. void talk_to_npc( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, - std::vector& at, - const Pokemon::TilePos& npc, - const std::vector& sides, - std::vector& trainers, - const RouteTrainerFight& fight, - const std::string& label + const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk ); From c459094be13bbc202983d666a50ba00223af1e24 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:53:01 -0800 Subject: [PATCH 10/39] BDSP walker: the transition watchers take only what their callers set Code health: WhiteScreenWatcher and BlackScreenFlashWatcher had 8 parameters. Every caller used the default thresholds and durations, so WhiteScreenWatcher takes the colour, box, finder type and duration, and WhiteScreenOverWatcher the colour and box; the values are the old defaults. BlackScreenFlashWatcher had no callers and is removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_TransitionWatchers.cpp | 12 ++---- .../PokemonBDSP_TransitionWatchers.h | 38 ++++--------------- 2 files changed, 10 insertions(+), 40 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp index 3655b5244d..d774df0ddd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp @@ -1,15 +1,9 @@ #include "PokemonBDSP_TransitionWatchers.h" namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ -WhiteScreenOverWatcher::WhiteScreenOverWatcher( - Color color, const ImageFloatBox& box, - double min_rgb_sum, - double max_stddev_sum, - std::chrono::milliseconds hold_duration, - std::chrono::milliseconds release_duration -) +WhiteScreenOverWatcher::WhiteScreenOverWatcher(Color color, const ImageFloatBox& box) : VisualInferenceCallback("WhiteScreenOverWatcher") - , m_on(color, box, min_rgb_sum, max_stddev_sum, WhiteScreenWatcher::FinderType::PRESENT, hold_duration) - , m_off(color, box, min_rgb_sum, max_stddev_sum, WhiteScreenWatcher::FinderType::GONE, release_duration) + , m_on(color, box, WhiteScreenWatcher::FinderType::PRESENT, std::chrono::milliseconds(100)) + , m_off(color, box, WhiteScreenWatcher::FinderType::GONE, std::chrono::milliseconds(100)) {} void WhiteScreenOverWatcher::make_overlays(VideoOverlaySet& items) const{ m_on.make_overlays(items); diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h index ce5a34d2cb..8b40e51455 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h @@ -2,44 +2,20 @@ #include "CommonTools/VisualDetectors/BlackScreenDetector.h" namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ // AutoStory-specific transition handling; shared watchers retain upstream behavior. +// A white screen (RGB sum over 500, stddev sum under 10) in `box`, +// present or gone for `duration`. class WhiteScreenWatcher : public DetectorToFinder{ public: - WhiteScreenWatcher( - Color color = COLOR_RED, - const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, - double min_rgb_sum = 500, - double max_stddev_sum = 10, - FinderType finder_type = FinderType::PRESENT, - std::chrono::milliseconds duration = std::chrono::milliseconds(100) - ) - : DetectorToFinder("WhiteScreenWatcher", finder_type, duration, color, box, min_rgb_sum, max_stddev_sum) - {} -}; - -class BlackScreenFlashWatcher : public BlackScreenOverWatcher{ -public: - BlackScreenFlashWatcher( - Color color = COLOR_RED, - const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, - double max_rgb_sum = 100, - double max_stddev_sum = 10, - std::chrono::milliseconds hold_duration = std::chrono::milliseconds(20), - std::chrono::milliseconds release_duration = std::chrono::milliseconds(100) - ) - : BlackScreenOverWatcher(color, box, max_rgb_sum, max_stddev_sum, hold_duration, release_duration) + WhiteScreenWatcher(Color color, const ImageFloatBox& box, FinderType finder_type, std::chrono::milliseconds duration) + : DetectorToFinder("WhiteScreenWatcher", finder_type, duration, color, box, 500, 10) {} }; +// The screen in `box` went white for 100 ms, then stopped being white for +// 100 ms: an encounter's flash. class WhiteScreenOverWatcher : public VisualInferenceCallback{ public: - WhiteScreenOverWatcher( - Color color = COLOR_RED, - const ImageFloatBox& box = {0.1, 0.1, 0.8, 0.8}, - double min_rgb_sum = 500, - double max_stddev_sum = 10, - std::chrono::milliseconds hold_duration = std::chrono::milliseconds(100), - std::chrono::milliseconds release_duration = std::chrono::milliseconds(100) - ); + WhiteScreenOverWatcher(Color color, const ImageFloatBox& box); bool white_is_over(const ImageViewRGB32& frame); From 352c9fc2b4d3512b681406ef175e47f076099847 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 14:53:01 -0800 Subject: [PATCH 11/39] BDSP walker: LegWalk is a plain struct Code health asks for public data without methods: the banner checks and the run options are free functions of a LegWalk. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index c9c56096bc..f4ed4a1bb1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -180,31 +180,31 @@ struct LegWalk{ WalkResult result{WalkStop::LIMIT, 0ms, 0}; // Tiles the first tap of an open leg moved (see open_leg()). size_t turn_moved = 0; +}; - const TileLeg& leg() const{ - return planned.leg; - } - bool banner_ok() const{ - return std::any_of(banners.begin(), banners.end(), [&](const std::string& read){ - return location_banner_matches(read, map_after); - }); - } - // Notes the location banners the walk passes, to check map changes. A - // wild battle or a second walk to the wall may follow the banner: any - // banner seen during the leg counts. - bool watch_banner(const ImageViewRGB32& screen){ - if (banners.empty() || (!map_after.empty() && !banner_ok() && banners.size() < 8)){ - const std::string read = read_location_banner(screen); - if (!read.empty() && (banners.empty() || banners.back() != read)){ - banners.push_back(read); - } +bool banner_ok(const LegWalk& walk){ + return std::any_of(walk.banners.begin(), walk.banners.end(), [&](const std::string& read){ + return location_banner_matches(read, walk.map_after); + }); +} + +// Notes the location banners the walk passes, to check map changes. A wild +// battle or a second walk to the wall may follow the banner: any banner +// seen during the leg counts. +bool watch_banner(LegWalk& walk, const ImageViewRGB32& screen){ + if (walk.banners.empty() || (!walk.map_after.empty() && !banner_ok(walk) && walk.banners.size() < 8)){ + const std::string read = read_location_banner(screen); + if (!read.empty() && (walk.banners.empty() || walk.banners.back() != read)){ + walk.banners.push_back(read); } - return false; - } - DpadWalkOptions run_watching(){ - return {.arrived = [this](const ImageViewRGB32& screen){ return watch_banner(screen); }, .pace = DpadPace::RUN}; } -}; + return false; +} + +// Running on the D-pad, reading the banners on the way. +DpadWalkOptions run_watching(LegWalk& walk){ + return {.arrived = [&walk](const ImageViewRGB32& screen){ return watch_banner(walk, screen); }, .pace = DpadPace::RUN}; +} // walk_route(): plans, walks each leg, and plans again from where the walk // turned out to be when something stopped it. @@ -357,9 +357,9 @@ std::vector RouteWalker::arrive() const{ // Run (B held) on the D-pad: a leg to a wall ends at the wall however fast // we get there, and the D-pad keeps to the row. LegNext RouteWalker::wall_leg(LegWalk& walk){ - const TileLeg& leg = walk.leg(); + const TileLeg& leg = walk.planned.leg; const Milliseconds limit = wall_limit(leg); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); // A wild battle may end the walk early. Walking to a wall again ends in // the same place, so finish the leg. const bool interrupted = walk.result.wild_battles > 0; @@ -367,7 +367,7 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ (walk.result.stop == WalkStop::LIMIT || walk.result.stop == WalkStop::BLOCKED); again++ ){ m_stream.log("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again."); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); } if (interrupted){ return LegNext::NEXT_LEG; @@ -403,14 +403,14 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ // A walk to the wall that stopped well short of it: tap through a corner, // then wait for someone in the way. Returns the tiles walked by then. size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest){ - const Milliseconds per_tile = route_ms_per_tile(walk.leg().direction) / ROUTE_RUN_SPEEDUP; - const Milliseconds limit = wall_limit(walk.leg()); + const Milliseconds per_tile = route_ms_per_tile(walk.planned.leg.direction) / ROUTE_RUN_SPEEDUP; + const Milliseconds limit = wall_limit(walk.planned.leg); // A stick walk can catch a corner when we stand part of a tile off // centre, where a D-pad tap, which moves whole tiles, gets through: on // Route 203's stairs the walk west stopped nine times in a row and two // taps crossed (r225a, 2026-10-31). Tap twice, then walk on to the wall. if (walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ - const Direction direction = walk.leg().direction; + const Direction direction = walk.planned.leg.direction; const DpadPosition dpad = direction == Direction::NORTH ? DPAD_UP : direction == Direction::SOUTH ? DPAD_DOWN : @@ -421,7 +421,7 @@ size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t pbf_press_dpad(m_context, dpad, 160ms, 640ms); } m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); walked_tiles += tiles_moved(walk.result, per_tile); } // Someone walking about may stand in the way for a moment: wait and @@ -431,7 +431,7 @@ size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); pbf_wait(m_context, 2500ms); m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, walk.run_watching()); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); walked_tiles += tiles_moved(walk.result, per_tile); } return walked_tiles; @@ -443,7 +443,7 @@ LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ talk_to_blocker(walk.label); std::vector stopped; for (const TilePos& pos : m_from){ - const std::vector path = leg_path(m_grid, pos, walk.leg().direction, SIZE_MAX); + const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, SIZE_MAX); for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ stopped.push_back(k == 0 ? pos : path[k - 1]); } @@ -470,8 +470,8 @@ void RouteWalker::check_at_wall(LegWalk& walk){ route_status(m_stream, walk.label + ": a trainer stopped us on the way; battling."); m_trainers.fight("a trainer on the way"); m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.leg()), - walk.label + ": on to the wall", walk.run_watching()); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), + walk.label + ": on to the wall", run_watching(walk)); } if (check.stop != WalkStop::BLOCKED && !(check.stop == WalkStop::LIMIT && holding_against_wall(m_stream, m_context, walk.stick.x, walk.stick.y, walk.label + ": at the wall?")) @@ -486,8 +486,8 @@ void RouteWalker::check_at_wall(LegWalk& walk){ // however far it got, so walk on to it and carry on. void RouteWalker::on_to_wall(LegWalk& walk){ m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.leg()), - walk.label + ": on to the wall", walk.run_watching()); + walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), + walk.label + ": on to the wall", run_watching(walk)); m_from = walk.planned.after; log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, 0); } @@ -497,7 +497,7 @@ void RouteWalker::on_to_wall(LegWalk& walk){ // a stick hold timed for three moved one in the Ravaged Path (r235b) and the // belief drifted four rows. LegNext RouteWalker::open_leg(LegWalk& walk){ - const TileLeg& leg = walk.leg(); + const TileLeg& leg = walk.planned.leg; if (m_facing != leg.direction){ turn_tap(walk); } @@ -556,13 +556,13 @@ LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ talk_to_blocker(walk.label); std::vector stopped; for (const TilePos& pos : m_from){ - const std::vector path = leg_path(m_grid, pos, walk.leg().direction, moved); + const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, moved); stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); } stopped = sorted_unique(std::move(stopped)); mark_blocker(walk, stopped); return replan_from(walk, std::move(stopped), - "blocked after " + std::to_string(moved) + " of " + std::to_string(walk.leg().tiles) + " tiles"); + "blocked after " + std::to_string(moved) + " of " + std::to_string(walk.planned.leg.tiles) + " tiles"); } @@ -572,11 +572,11 @@ LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ void RouteWalker::check_banners(LegWalk& walk){ m_context.wait_for_all_requests(); if (VideoSnapshot screen = m_stream.video().snapshot()){ - walk.watch_banner(*screen.frame); + watch_banner(walk, *screen.frame); } const std::string banner_text = quoted_list(walk.banners); m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); - if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !walk.banner_ok()){ + if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + (banner_text.empty() ? "nothing" : banner_text) + ".", m_stream); @@ -595,7 +595,7 @@ void RouteWalker::check_banners(LegWalk& walk){ // A trainer who sees us freezes the player and walks over; the dialogue // may open after the walk has ended. LegNext RouteWalker::after_leg(LegWalk& walk){ - const TileLeg& leg = walk.leg(); + const TileLeg& leg = walk.planned.leg; std::vector> paths; for (const TilePos& pos : m_from){ paths.push_back(leg_path(m_grid, pos, leg.direction, leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); @@ -645,7 +645,7 @@ LegNext RouteWalker::unknown_trainer(LegWalk& walk, const std::vector by_taps; for (const TilePos& pos : m_from){ - const std::vector path = leg_path(m_grid, pos, walk.leg().direction, walk.leg().tiles); + const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, walk.planned.leg.tiles); const size_t k = std::min(taps, path.size()); by_taps.push_back(k == 0 ? pos : path[k - 1]); } @@ -715,15 +715,15 @@ void RouteWalker::mark_blocker(const LegWalk& walk, const std::vector& if (stopped.size() != 1){ return; } - const TilePos blocker = stopped[0].next(walk.leg().direction); + const TilePos blocker = stopped[0].next(walk.planned.leg.direction); m_grid.block(blocker); m_stream.log("[Route] " + walk.label + ": something the map lacks is at " + describe(m_maps, blocker) + "; marked it.", COLOR_ORANGE); } LegNext RouteWalker::replan_from(const LegWalk& walk, std::vector stopped, const std::string& why){ - m_stream.log(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.leg()), why, route_tiles(m_maps, stopped)), COLOR_ORANGE); - m_facing = walk.leg().direction; + m_stream.log(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.planned.leg), why, route_tiles(m_maps, stopped)), COLOR_ORANGE); + m_facing = walk.planned.leg.direction; m_from = std::move(stopped); return LegNext::REPLAN; } From aeb36c8aa633440b24f31185a0b7b9e477974145 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:23:06 -0800 Subject: [PATCH 12/39] BDSP walker: credit #1717 for the pieces adapted from it The transition watchers, the overworld detector and Walk.cpp's BattleDialogWatcher come from PokemonAutomation/Arduino-Source#1717 by Developer-Butters; their headers now say so. The transition watchers get the repo's header block, include guard and one namespace per line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_TransitionWatchers.cpp | 19 ++++++++++++-- .../PokemonBDSP_TransitionWatchers.h | 25 ++++++++++++++++--- ...okemonBDSP_AutoStory_OverworldDetector.cpp | 1 + .../PokemonBDSP_AutoStory_OverworldDetector.h | 2 ++ .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 2 ++ 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp index d774df0ddd..3cf98c2e72 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp @@ -1,5 +1,17 @@ +/* BDSP Transition Watchers + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + */ + #include "PokemonBDSP_TransitionWatchers.h" -namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + WhiteScreenOverWatcher::WhiteScreenOverWatcher(Color color, const ImageFloatBox& box) : VisualInferenceCallback("WhiteScreenOverWatcher") , m_on(color, box, WhiteScreenWatcher::FinderType::PRESENT, std::chrono::milliseconds(100)) @@ -27,4 +39,7 @@ bool WhiteScreenOverWatcher::white_is_over(const ImageViewRGB32& frame){ return m_white_is_over.load(std::memory_order_acquire); } -}}} + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h index 8b40e51455..9091f99fb0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h @@ -1,6 +1,20 @@ -#pragma once +/* BDSP Transition Watchers + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + */ + +#ifndef PokemonAutomation_PokemonBDSP_TransitionWatchers_H +#define PokemonAutomation_PokemonBDSP_TransitionWatchers_H + #include "CommonTools/VisualDetectors/BlackScreenDetector.h" -namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + // AutoStory-specific transition handling; shared watchers retain upstream behavior. // A white screen (RGB sum over 500, stddev sum under 10) in `box`, // present or gone for `duration`. @@ -29,4 +43,9 @@ class WhiteScreenOverWatcher : public VisualInferenceCallback{ bool m_has_been_white = false; std::atomic m_white_is_over = false; }; -}}} + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp index d9e759ec3a..855d3bbeae 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp @@ -2,6 +2,7 @@ * * From: https://github.com/PokemonAutomation/ * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. */ #include "CommonFramework/ImageTools/ImageStats.h" diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h index cf3d1ea2b6..f33988fcc3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h @@ -2,6 +2,8 @@ * * From: https://github.com/PokemonAutomation/ * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * * Detects that the player is in the free-roaming overworld state: * no dialog box, no battle menu, and no in-game menu open. * diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index 5cdc90dc3e..0f5d15fc9f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -2,6 +2,8 @@ * * From: https://github.com/PokemonAutomation/ * + * BattleDialogWatcher adapted from PokemonAutomation/Arduino-Source#1717 by + * Developer-Butters. */ #include From ba95847e16eda3ec725997b9ff90f01b8d426347 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 15:24:44 -0800 Subject: [PATCH 13/39] BDSP walker: new files in CRLF #129's new .cpp and .h files were LF; they are CRLF like the rest of the repo. Line endings only: git diff --ignore-cr-at-eol shows nothing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- ...okemonBDSP_AutoStory_OverworldDetector.cpp | 272 +-- .../PokemonBDSP_AutoStory_OverworldDetector.h | 200 +-- .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 450 ++--- .../PokemonBDSP_AutoStory_Walk_Tests.h | 54 +- .../Utils/PokemonBDSP_AutoStory_Indoors.cpp | 136 +- .../Utils/PokemonBDSP_AutoStory_Indoors.h | 66 +- .../Utils/PokemonBDSP_AutoStory_Location.cpp | 296 +-- .../Utils/PokemonBDSP_AutoStory_Location.h | 134 +- .../PokemonBDSP_AutoStory_OverlayLog.cpp | 84 +- .../Utils/PokemonBDSP_AutoStory_OverlayLog.h | 54 +- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 1598 ++++++++--------- .../PokemonBDSP_AutoStory_PlannedRoute.h | 214 +-- .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 588 +++--- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 300 ++-- .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 1122 ++++++------ .../Utils/PokemonBDSP_AutoStory_Walk.h | 378 ++-- 16 files changed, 2973 insertions(+), 2973 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp index 855d3bbeae..6b250862fd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp @@ -1,136 +1,136 @@ -/* BDSP AutoStory - Overworld Detector - * - * From: https://github.com/PokemonAutomation/ - * - * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. - */ - -#include "CommonFramework/ImageTools/ImageStats.h" -#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" - -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/Images/SolidColorTest.h" - -#include "PokemonBDSP_AutoStory_OverworldDetector.h" - -using namespace std::chrono_literals; - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// --------------------------------------------------------------------------- -// OverworldDetector -// --------------------------------------------------------------------------- - -OverworldDetector::OverworldDetector(Color color) - : m_color(color) - , m_battle_menu(BattleType::STANDARD, color){ -} - -void OverworldDetector::make_overlays(VideoOverlaySet& items) const { - m_dialog.make_overlays(items); - m_battle_menu.make_overlays(items); - m_menu.make_overlays(items); -} - -bool OverworldDetector::detect(const ImageViewRGB32& screen){ - // We are in the overworld when none of the overlay screens are present. - if (m_dialog.detect(screen)){ - return false; - } - if (m_battle_menu.detect(screen)){ - return false; - } - if (m_menu.detect(screen)){ - return false; - } - return true; -} - - -// --------------------------------------------------------------------------- -// OverworldWatcher -// --------------------------------------------------------------------------- - -OverworldWatcher::OverworldWatcher(Color color) - : OverworldDetector(color) - , VisualInferenceCallback("OverworldWatcher"){ -} - -void OverworldWatcher::make_overlays(VideoOverlaySet& items) const { - OverworldDetector::make_overlays(items); -} - -bool OverworldWatcher::process_frame(const ImageViewRGB32& frame, WallClock /*timestamp*/){ - return detect(frame); -} - - -// --------------------------------------------------------------------------- -// PoketchButtonDetector -// --------------------------------------------------------------------------- - -PoketchButtonDetector::PoketchButtonDetector(Color color) - : m_color(color) - , m_button(0.968, 0.170, 0.022, 0.120){ -} - -void PoketchButtonDetector::make_overlays(VideoOverlaySet& items) const { - items.add(m_color, m_button); -} - -bool PoketchButtonDetector::detect(const ImageViewRGB32& screen){ - ImageStats stats = image_stats(extract_box_reference(screen, m_button)); - // The button is a glossy red with a highlight, so allow a wide stddev. - return is_solid(stats, {0.74, 0.10, 0.16}, 0.12, 80); -} - -PoketchButtonWatcher::PoketchButtonWatcher(Color color) - : PoketchButtonDetector(color) - , VisualInferenceCallback("PoketchButtonWatcher"){ -} - -void PoketchButtonWatcher::make_overlays(VideoOverlaySet& items) const { - PoketchButtonDetector::make_overlays(items); -} - -bool PoketchButtonWatcher::process_frame(const ImageViewRGB32& frame, WallClock){ - return detect(frame); -} - - -static bool look_for_poketch_button(VideoStream& stream, ProControllerContext& context){ - context.wait_for_all_requests(); - PoketchButtonWatcher watcher; - return wait_until(stream, context, 2s, {{watcher}}) == 0; -} - -bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context){ - for (size_t attempt = 0; attempt < 3; attempt++){ - if (look_for_poketch_button(stream, context)){ - return true; - } - stream.log("[AutoStory] Poketch button not found. Attempt " + std::to_string(attempt + 1) + " of 3.", - COLOR_ORANGE); - - if (attempt == 0){ - // Most likely hidden: one tap shows it. - pbf_press_button(context, BUTTON_R, 205ms, 0ms); - }else{ - // Long press hides it, then a tap shows it at the normal size. - pbf_press_button(context, BUTTON_R, 1548ms, 0ms); - pbf_wait(context, 1418ms); - pbf_press_button(context, BUTTON_R, 143ms, 0ms); - } - } - - // Check the result of the last press sequence. - return look_for_poketch_button(stream, context); -} - - -} -} -} +/* BDSP AutoStory - Overworld Detector + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + */ + +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" + +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/Images/SolidColorTest.h" + +#include "PokemonBDSP_AutoStory_OverworldDetector.h" + +using namespace std::chrono_literals; + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// --------------------------------------------------------------------------- +// OverworldDetector +// --------------------------------------------------------------------------- + +OverworldDetector::OverworldDetector(Color color) + : m_color(color) + , m_battle_menu(BattleType::STANDARD, color){ +} + +void OverworldDetector::make_overlays(VideoOverlaySet& items) const { + m_dialog.make_overlays(items); + m_battle_menu.make_overlays(items); + m_menu.make_overlays(items); +} + +bool OverworldDetector::detect(const ImageViewRGB32& screen){ + // We are in the overworld when none of the overlay screens are present. + if (m_dialog.detect(screen)){ + return false; + } + if (m_battle_menu.detect(screen)){ + return false; + } + if (m_menu.detect(screen)){ + return false; + } + return true; +} + + +// --------------------------------------------------------------------------- +// OverworldWatcher +// --------------------------------------------------------------------------- + +OverworldWatcher::OverworldWatcher(Color color) + : OverworldDetector(color) + , VisualInferenceCallback("OverworldWatcher"){ +} + +void OverworldWatcher::make_overlays(VideoOverlaySet& items) const { + OverworldDetector::make_overlays(items); +} + +bool OverworldWatcher::process_frame(const ImageViewRGB32& frame, WallClock /*timestamp*/){ + return detect(frame); +} + + +// --------------------------------------------------------------------------- +// PoketchButtonDetector +// --------------------------------------------------------------------------- + +PoketchButtonDetector::PoketchButtonDetector(Color color) + : m_color(color) + , m_button(0.968, 0.170, 0.022, 0.120){ +} + +void PoketchButtonDetector::make_overlays(VideoOverlaySet& items) const { + items.add(m_color, m_button); +} + +bool PoketchButtonDetector::detect(const ImageViewRGB32& screen){ + ImageStats stats = image_stats(extract_box_reference(screen, m_button)); + // The button is a glossy red with a highlight, so allow a wide stddev. + return is_solid(stats, {0.74, 0.10, 0.16}, 0.12, 80); +} + +PoketchButtonWatcher::PoketchButtonWatcher(Color color) + : PoketchButtonDetector(color) + , VisualInferenceCallback("PoketchButtonWatcher"){ +} + +void PoketchButtonWatcher::make_overlays(VideoOverlaySet& items) const { + PoketchButtonDetector::make_overlays(items); +} + +bool PoketchButtonWatcher::process_frame(const ImageViewRGB32& frame, WallClock){ + return detect(frame); +} + + +static bool look_for_poketch_button(VideoStream& stream, ProControllerContext& context){ + context.wait_for_all_requests(); + PoketchButtonWatcher watcher; + return wait_until(stream, context, 2s, {{watcher}}) == 0; +} + +bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context){ + for (size_t attempt = 0; attempt < 3; attempt++){ + if (look_for_poketch_button(stream, context)){ + return true; + } + stream.log("[AutoStory] Poketch button not found. Attempt " + std::to_string(attempt + 1) + " of 3.", + COLOR_ORANGE); + + if (attempt == 0){ + // Most likely hidden: one tap shows it. + pbf_press_button(context, BUTTON_R, 205ms, 0ms); + }else{ + // Long press hides it, then a tap shows it at the normal size. + pbf_press_button(context, BUTTON_R, 1548ms, 0ms); + pbf_wait(context, 1418ms); + pbf_press_button(context, BUTTON_R, 143ms, 0ms); + } + } + + // Check the result of the last press sequence. + return look_for_poketch_button(stream, context); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h index f33988fcc3..ce36eb5f86 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h @@ -1,100 +1,100 @@ -/* BDSP AutoStory - Overworld Detector - * - * From: https://github.com/PokemonAutomation/ - * - * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. - * - * Detects that the player is in the free-roaming overworld state: - * no dialog box, no battle menu, and no in-game menu open. - * - * BDSP does not ship a standalone OverworldDetector in PokemonBDSP/Inference/, - * so this composite detector is defined here for AutoStory use. - * - * Reuses: - * - ShortDialogDetector (PokemonBDSP_DialogDetector.h) — absence required - * - BattleMenuDetector (PokemonBDSP_BattleMenuDetector.h) — absence required - * - MenuDetector (PokemonBDSP_MenuDetector.h) — absence required - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H -#define PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H - -#include "Common/Cpp/Color.h" - -#include "CommonFramework/Tools/VideoStream.h" - -#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" -#include "CommonTools/VisualDetector.h" - -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" - -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Static (single-frame) version — use when you already have a frame. -class OverworldDetector : public StaticScreenDetector { -public: - OverworldDetector(Color color = COLOR_CYAN); - - virtual void make_overlays(VideoOverlaySet& items) const override; - - // Returns true when none of: short dialog, battle menu, or in-game menu - // are present on the given frame. - virtual bool detect(const ImageViewRGB32& screen) override; - -private: - Color m_color; - ShortDialogDetector m_dialog; - BattleMenuDetector m_battle_menu; - MenuDetector m_menu; -}; - - -// Watcher (callback) version — use with run_until<> or wait_until(). -class OverworldWatcher : public OverworldDetector, public VisualInferenceCallback { -public: - OverworldWatcher(Color color = COLOR_CYAN); - - virtual void make_overlays(VideoOverlaySet& items) const override; - virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; -}; - - -// Detects the big red button on the right edge of the Poketch overlay. -// Present only while the Poketch is shown at its normal size. -class PoketchButtonDetector : public StaticScreenDetector { -public: - PoketchButtonDetector(Color color = COLOR_RED); - - virtual void make_overlays(VideoOverlaySet& items) const override; - virtual bool detect(const ImageViewRGB32& screen) override; - -private: - Color m_color; - ImageFloatBox m_button; -}; - -class PoketchButtonWatcher : public PoketchButtonDetector, public VisualInferenceCallback { -public: - PoketchButtonWatcher(Color color = COLOR_RED); - - virtual void make_overlays(VideoOverlaySet& items) const override; - virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; -}; - -// Tries up to 3 times to get the Poketch button on screen using R presses. -// Returns true if the button was seen. -bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context); - - -} -} -} -#endif +/* BDSP AutoStory - Overworld Detector + * + * From: https://github.com/PokemonAutomation/ + * + * Adapted from PokemonAutomation/Arduino-Source#1717 by Developer-Butters. + * + * Detects that the player is in the free-roaming overworld state: + * no dialog box, no battle menu, and no in-game menu open. + * + * BDSP does not ship a standalone OverworldDetector in PokemonBDSP/Inference/, + * so this composite detector is defined here for AutoStory use. + * + * Reuses: + * - ShortDialogDetector (PokemonBDSP_DialogDetector.h) — absence required + * - BattleMenuDetector (PokemonBDSP_BattleMenuDetector.h) — absence required + * - MenuDetector (PokemonBDSP_MenuDetector.h) — absence required + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H +#define PokemonAutomation_PokemonBDSP_AutoStory_OverworldDetector_H + +#include "Common/Cpp/Color.h" + +#include "CommonFramework/Tools/VideoStream.h" + +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "CommonTools/VisualDetector.h" + +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" + +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_MenuDetector.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Static (single-frame) version — use when you already have a frame. +class OverworldDetector : public StaticScreenDetector { +public: + OverworldDetector(Color color = COLOR_CYAN); + + virtual void make_overlays(VideoOverlaySet& items) const override; + + // Returns true when none of: short dialog, battle menu, or in-game menu + // are present on the given frame. + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ShortDialogDetector m_dialog; + BattleMenuDetector m_battle_menu; + MenuDetector m_menu; +}; + + +// Watcher (callback) version — use with run_until<> or wait_until(). +class OverworldWatcher : public OverworldDetector, public VisualInferenceCallback { +public: + OverworldWatcher(Color color = COLOR_CYAN); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; +}; + + +// Detects the big red button on the right edge of the Poketch overlay. +// Present only while the Poketch is shown at its normal size. +class PoketchButtonDetector : public StaticScreenDetector { +public: + PoketchButtonDetector(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool detect(const ImageViewRGB32& screen) override; + +private: + Color m_color; + ImageFloatBox m_button; +}; + +class PoketchButtonWatcher : public PoketchButtonDetector, public VisualInferenceCallback { +public: + PoketchButtonWatcher(Color color = COLOR_RED); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; +}; + +// Tries up to 3 times to get the Poketch button on screen using R presses. +// Returns true if the button was seen. +bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp index cb6ab3bb54..9357b6cee2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -1,225 +1,225 @@ -/* BDSP AutoStory Walk Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "Utils/PokemonBDSP_AutoStory_Indoors.h" -#include "Utils/PokemonBDSP_AutoStory_Location.h" -#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP_AutoStory_Walk_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -// indoors() decides when leave_door_south() is out: r257 walked on south -// past Sandgem's road into a house after leaving the Center, because the -// evening tint made the outside as warm as a room. Synthetic screens: the -// evening outside (warm all over), a room (warm, black along the bottom), -// a fade (black all over), and warm with a thin dark edge (a shadow). -struct IndoorsCase{ - std::string name; - uint32_t fill; - double dark_bottom; // how much of the screen's height, from the bottom, is black - bool indoors; -}; - -class Test_IndoorsAnyLight : public UnitTest{ -public: - Test_IndoorsAnyLight() - : UnitTest("PokemonBDSP::AutoStory - indoors() under the evening tint") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - const std::vector cases{ - {.name = "evening outside, orange all over", .fill = 0xffe0a040, .dark_bottom = 0, .indoors = false}, - {.name = "a room: orange, black below its floor", .fill = 0xffe0a040, .dark_bottom = 0.10, .indoors = true}, - {.name = "a fade: black all over", .fill = 0xff000000, .dark_bottom = 0, .indoors = false}, - {.name = "orange with a thin shadow at the bottom", .fill = 0xffe0a040, .dark_bottom = 0.01, .indoors = false}, - {.name = "daytime grass", .fill = 0xff60b040, .dark_bottom = 0.10, .indoors = false}, - }; - for (const IndoorsCase& item : cases){ - ImageRGB32 screen(1920, 1080); - const size_t dark_from = (size_t)(1080 * (1 - item.dark_bottom)); - for (size_t y = 0; y < 1080; y++){ - for (size_t x = 0; x < 1920; x++){ - screen.pixel(x, y) = y >= dark_from ? 0xff000000 : item.fill; - } - } - if (indoors(screen) != item.indoors){ - return item.name + (item.indoors ? ": read as outside." : ": read as inside."); - } - } - return true; - } -}; - -// door_fade_look(): out of a door is a dark look, then a bright one that is -// not a room. r259 turned round in Sandgem's lit Center, read "not a room" -// once at full brightness, and walked east inside it as if outside. -struct DoorLook{ - double brightness; - bool room; -}; -struct DoorCase{ - std::string name; - std::vector looks; - // The look (1-based) that reads out, or 0 for none. - size_t out_at; -}; - -class Test_DoorFade : public UnitTest{ -public: - Test_DoorFade() - : UnitTest("PokemonBDSP::AutoStory - out of a door needs its fade") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - const std::vector cases{ - {.name = "r259: a bright look in the Center that misses the room", .looks = {{458, true}, {440, false}, {457, true}}, .out_at = 0}, - {.name = "the fade, then the town", .looks = {{458, true}, {300, true}, {60, false}, {310, false}}, .out_at = 4}, - {.name = "the fade, then the room again", .looks = {{458, true}, {60, false}, {420, true}}, .out_at = 0}, - {.name = "the fade, still dim", .looks = {{60, false}, {150, false}, {320, false}}, .out_at = 3}, - }; - for (const DoorCase& item : cases){ - DoorFadeWatch watch; - size_t out_at = 0; - for (size_t i = 0; i < item.looks.size() && out_at == 0; i++){ - if (door_fade_look(watch, item.looks[i].brightness, item.looks[i].room)){ - out_at = i + 1; - } - } - if (out_at != item.out_at){ - return item.name + ": out at look " + std::to_string(out_at) + ", expected " + std::to_string(item.out_at) + "."; - } - } - return true; - } -}; - - -class Test_LocationBanner : public UnitTest{ -public: - Test_LocationBanner() - : UnitTest("PokemonBDSP::AutoStory - location banner match") - {} - - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - struct Case{ - const char* read; - const char* expected; - bool match; - }; - const Case cases[] = { - {"sandgemtown", "Sandgem Town", true}, - {"andgemltown", "Sandgem Town", true}, // r109's OCR - {"Route 202", "Route 202", true}, - {"route2o2", "Route 202", false}, // a digit read as a letter is not trusted - {"route201", "Route 202", false}, - {"jubilifecity", "Sandgem Town", false}, - {"twinleaftown", "Sandgem Town", false}, - {"", "Sandgem Town", false}, - }; - for (const Case& c : cases){ - if (location_banner_matches(c.read, c.expected) != c.match){ - return std::string("Banner \"") + c.read + "\" for " + c.expected + - (c.match ? " should match." : " should not match."); - } - } - return true; - } -}; - - -class Test_Indoors : public UnitTest{ -public: - Test_Indoors() - : UnitTest("PokemonBDSP::AutoStory - indoors") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - struct Case{ const char* file; bool inside; }; - const Case cases[] = { - {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", true}, - {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", true}, - {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", true}, - {"PokemonBDSP/AutoStory/SandgemCenterInside-20261030-053555.png", true}, - {"PokemonBDSP/AutoStory/JubilifeSchoolInside-20261030.png", true}, - {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", false}, - {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", false}, - {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep2-20261030.png", false}, - {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, - {"PokemonBDSP/AutoStory/SandgemCenterBelowSign-20261030-053527.png", false}, - {"PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", false}, - }; - for (const Case& c : cases){ - const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; - if (!Filesystem::is_regular_file(path)){ - return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); - } - const bool inside = indoors(ImageRGB32(path)); - logger.log(std::string(c.file) + ": " + (inside ? "indoors" : "outside")); - if (inside != c.inside){ - return std::string(c.file) + ": expected " + (c.inside ? "indoors" : "outside") + "."; - } - } - return true; - } -}; - - -class Test_LocationBannerShowing : public UnitTest{ -public: - Test_LocationBannerShowing() - : UnitTest("PokemonBDSP::AutoStory - location banner showing") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - struct Case{ const char* file; bool banner; }; - const Case cases[] = { - {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", true}, - {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", true}, - {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", false}, - {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", false}, - {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", false}, - {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, - }; - for (const Case& c : cases){ - const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; - if (!Filesystem::is_regular_file(path)){ - return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); - } - const bool banner = location_banner_showing(ImageRGB32(path)); - logger.log(std::string(c.file) + ": banner " + (banner ? "shows" : "none")); - if (banner != c.banner){ - return std::string(c.file) + ": expected " + (c.banner ? "a banner" : "no banner") + "."; - } - } - return true; - } -}; - - -} - - -void add_tests_AutoStory_Walk(UnitTestDatabase& database){ - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); -} - - - -} -} -} +/* BDSP AutoStory Walk Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "Utils/PokemonBDSP_AutoStory_Location.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Walk_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// indoors() decides when leave_door_south() is out: r257 walked on south +// past Sandgem's road into a house after leaving the Center, because the +// evening tint made the outside as warm as a room. Synthetic screens: the +// evening outside (warm all over), a room (warm, black along the bottom), +// a fade (black all over), and warm with a thin dark edge (a shadow). +struct IndoorsCase{ + std::string name; + uint32_t fill; + double dark_bottom; // how much of the screen's height, from the bottom, is black + bool indoors; +}; + +class Test_IndoorsAnyLight : public UnitTest{ +public: + Test_IndoorsAnyLight() + : UnitTest("PokemonBDSP::AutoStory - indoors() under the evening tint") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "evening outside, orange all over", .fill = 0xffe0a040, .dark_bottom = 0, .indoors = false}, + {.name = "a room: orange, black below its floor", .fill = 0xffe0a040, .dark_bottom = 0.10, .indoors = true}, + {.name = "a fade: black all over", .fill = 0xff000000, .dark_bottom = 0, .indoors = false}, + {.name = "orange with a thin shadow at the bottom", .fill = 0xffe0a040, .dark_bottom = 0.01, .indoors = false}, + {.name = "daytime grass", .fill = 0xff60b040, .dark_bottom = 0.10, .indoors = false}, + }; + for (const IndoorsCase& item : cases){ + ImageRGB32 screen(1920, 1080); + const size_t dark_from = (size_t)(1080 * (1 - item.dark_bottom)); + for (size_t y = 0; y < 1080; y++){ + for (size_t x = 0; x < 1920; x++){ + screen.pixel(x, y) = y >= dark_from ? 0xff000000 : item.fill; + } + } + if (indoors(screen) != item.indoors){ + return item.name + (item.indoors ? ": read as outside." : ": read as inside."); + } + } + return true; + } +}; + +// door_fade_look(): out of a door is a dark look, then a bright one that is +// not a room. r259 turned round in Sandgem's lit Center, read "not a room" +// once at full brightness, and walked east inside it as if outside. +struct DoorLook{ + double brightness; + bool room; +}; +struct DoorCase{ + std::string name; + std::vector looks; + // The look (1-based) that reads out, or 0 for none. + size_t out_at; +}; + +class Test_DoorFade : public UnitTest{ +public: + Test_DoorFade() + : UnitTest("PokemonBDSP::AutoStory - out of a door needs its fade") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "r259: a bright look in the Center that misses the room", .looks = {{458, true}, {440, false}, {457, true}}, .out_at = 0}, + {.name = "the fade, then the town", .looks = {{458, true}, {300, true}, {60, false}, {310, false}}, .out_at = 4}, + {.name = "the fade, then the room again", .looks = {{458, true}, {60, false}, {420, true}}, .out_at = 0}, + {.name = "the fade, still dim", .looks = {{60, false}, {150, false}, {320, false}}, .out_at = 3}, + }; + for (const DoorCase& item : cases){ + DoorFadeWatch watch; + size_t out_at = 0; + for (size_t i = 0; i < item.looks.size() && out_at == 0; i++){ + if (door_fade_look(watch, item.looks[i].brightness, item.looks[i].room)){ + out_at = i + 1; + } + } + if (out_at != item.out_at){ + return item.name + ": out at look " + std::to_string(out_at) + ", expected " + std::to_string(item.out_at) + "."; + } + } + return true; + } +}; + + +class Test_LocationBanner : public UnitTest{ +public: + Test_LocationBanner() + : UnitTest("PokemonBDSP::AutoStory - location banner match") + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + struct Case{ + const char* read; + const char* expected; + bool match; + }; + const Case cases[] = { + {"sandgemtown", "Sandgem Town", true}, + {"andgemltown", "Sandgem Town", true}, // r109's OCR + {"Route 202", "Route 202", true}, + {"route2o2", "Route 202", false}, // a digit read as a letter is not trusted + {"route201", "Route 202", false}, + {"jubilifecity", "Sandgem Town", false}, + {"twinleaftown", "Sandgem Town", false}, + {"", "Sandgem Town", false}, + }; + for (const Case& c : cases){ + if (location_banner_matches(c.read, c.expected) != c.match){ + return std::string("Banner \"") + c.read + "\" for " + c.expected + + (c.match ? " should match." : " should not match."); + } + } + return true; + } +}; + + +class Test_Indoors : public UnitTest{ +public: + Test_Indoors() + : UnitTest("PokemonBDSP::AutoStory - indoors") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* file; bool inside; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", true}, + {"PokemonBDSP/AutoStory/SandgemCenterInside-20261030-053555.png", true}, + {"PokemonBDSP/AutoStory/JubilifeSchoolInside-20261030.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", false}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", false}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep2-20261030.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, + {"PokemonBDSP/AutoStory/SandgemCenterBelowSign-20261030-053527.png", false}, + {"PokemonBDSP/AutoStory/SandgemMartDoorUpRight-20261030-023443.png", false}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const bool inside = indoors(ImageRGB32(path)); + logger.log(std::string(c.file) + ": " + (inside ? "indoors" : "outside")); + if (inside != c.inside){ + return std::string(c.file) + ": expected " + (c.inside ? "indoors" : "outside") + "."; + } + } + return true; + } +}; + + +class Test_LocationBannerShowing : public UnitTest{ +public: + Test_LocationBannerShowing() + : UnitTest("PokemonBDSP::AutoStory - location banner showing") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + struct Case{ const char* file; bool banner; }; + const Case cases[] = { + {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", true}, + {"PokemonBDSP/AutoStory/JubilifeSchoolDoorstep-20261030.png", true}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c4.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterInsideUpper-20261030-r151a.png", false}, + {"PokemonBDSP/AutoStory/JubilifeCenterFrontDoor-20261030-q2.png", false}, + }; + for (const Case& c : cases){ + const std::string path = UNIT_TEST_RESOURCE_PATH() + c.file; + if (!Filesystem::is_regular_file(path)){ + return UnitTestResult(UnitTestResult::SKIPPED, "Screenshot not found: " + path); + } + const bool banner = location_banner_showing(ImageRGB32(path)); + logger.log(std::string(c.file) + ": banner " + (banner ? "shows" : "none")); + if (banner != c.banner){ + return std::string(c.file) + ": expected " + (c.banner ? "a banner" : "no banner") + "."; + } + } + return true; + } +}; + + +} + + +void add_tests_AutoStory_Walk(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); + database.add(); + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h index ac120c6244..266dfa1c0d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h @@ -1,27 +1,27 @@ -/* BDSP AutoStory Walk Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walk_Tests_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Walk_Tests_H - -#include "Common/Cpp/TestRunners/UnitTest.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - - -// The walker's screen reads: indoors() on synthetic screens and -// screenshots, the door's fade, and the location banner. -void add_tests_AutoStory_Walk(UnitTestDatabase& database); - - - -} -} -} -#endif +/* BDSP AutoStory Walk Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walk_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Walk_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// The walker's screen reads: indoors() on synthetic screens and +// screenshots, the door's fade, and the location banner. +void add_tests_AutoStory_Walk(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp index 12baf6710f..c7e7a3458c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp @@ -1,68 +1,68 @@ -/* BDSP AutoStory - Indoors - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "Common/Cpp/Color.h" -#include "PokemonBDSP_AutoStory_Indoors.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -// Near-black pixels in one edge strip of the screen, out of all its pixels. -struct EdgeDark{ - size_t dark = 0; - size_t count = 0; -}; -void add_pixel(EdgeDark& edge, bool dark){ - edge.dark += dark; - edge.count++; -} -bool mostly_dark(const EdgeDark& edge){ - return edge.count > 0 && edge.dark * 100 >= edge.count * 15; -} - -} - -bool indoors(const ImageViewRGB32& screen){ - const size_t width = screen.width(), height = screen.height(); - const size_t strip_h = height * 12 / 100, strip_w = width * 12 / 100; - size_t warm = 0; - size_t count = 0; - EdgeDark top, bottom, left, right; - for (size_t y = 0; y < height; y += 8){ - for (size_t x = 0; x < width; x += 8){ - const Color pixel(screen.pixel(x, y)); - count++; - warm += pixel.red() > 150 && pixel.red() > pixel.blue() + 70 && pixel.green() > pixel.blue() + 20; - const bool dark = pixel.red() + pixel.green() + pixel.blue() < 60; - if (y < strip_h){ - add_pixel(top, dark); - } - if (y >= height - strip_h){ - add_pixel(bottom, dark); - } - if (x < strip_w){ - add_pixel(left, dark); - } - if (x >= width - strip_w){ - add_pixel(right, dark); - } - } - } - if (count == 0 || warm * 4 <= count){ - return false; - } - return mostly_dark(top) || mostly_dark(bottom) || mostly_dark(left) || mostly_dark(right); -} - - - -} -} -} +/* BDSP AutoStory - Indoors + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Color.h" +#include "PokemonBDSP_AutoStory_Indoors.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// Near-black pixels in one edge strip of the screen, out of all its pixels. +struct EdgeDark{ + size_t dark = 0; + size_t count = 0; +}; +void add_pixel(EdgeDark& edge, bool dark){ + edge.dark += dark; + edge.count++; +} +bool mostly_dark(const EdgeDark& edge){ + return edge.count > 0 && edge.dark * 100 >= edge.count * 15; +} + +} + +bool indoors(const ImageViewRGB32& screen){ + const size_t width = screen.width(), height = screen.height(); + const size_t strip_h = height * 12 / 100, strip_w = width * 12 / 100; + size_t warm = 0; + size_t count = 0; + EdgeDark top, bottom, left, right; + for (size_t y = 0; y < height; y += 8){ + for (size_t x = 0; x < width; x += 8){ + const Color pixel(screen.pixel(x, y)); + count++; + warm += pixel.red() > 150 && pixel.red() > pixel.blue() + 70 && pixel.green() > pixel.blue() + 20; + const bool dark = pixel.red() + pixel.green() + pixel.blue() < 60; + if (y < strip_h){ + add_pixel(top, dark); + } + if (y >= height - strip_h){ + add_pixel(bottom, dark); + } + if (x < strip_w){ + add_pixel(left, dark); + } + if (x >= width - strip_w){ + add_pixel(right, dark); + } + } + } + if (count == 0 || warm * 4 <= count){ + return false; + } + return mostly_dark(top) || mostly_dark(bottom) || mostly_dark(left) || mostly_dark(right); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h index 0d469836d0..9de7ba3159 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h @@ -1,33 +1,33 @@ -/* BDSP AutoStory - Indoors - * - * From: https://github.com/PokemonAutomation/ - * - * Whether the screen shows a room: the walker leaves a building by it. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Indoors_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Indoors_H - -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Whether the screen shows a building's inside: its warm walls and floor -// fill much of the view (Pokemon Centers, the Trainers' School: 42..65% of -// the screen), and the room floats in black, which shows along at least one -// edge (15% or more of a strip 12% deep). Warmth alone is not enough: the -// evening tint makes outside 50..72% warm (r256, r257, Sandgem), and -// Oreburgh's ground 58..69% by day. Of 1412 lab screenshots, the rule calls -// every outside one outside, at all hours, and the rooms rooms. A fade is -// dark everywhere but not warm, so it is not inside. -bool indoors(const ImageViewRGB32& screen); - - -} -} -} -#endif +/* BDSP AutoStory - Indoors + * + * From: https://github.com/PokemonAutomation/ + * + * Whether the screen shows a room: the walker leaves a building by it. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Indoors_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Indoors_H + +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Whether the screen shows a building's inside: its warm walls and floor +// fill much of the view (Pokemon Centers, the Trainers' School: 42..65% of +// the screen), and the room floats in black, which shows along at least one +// edge (15% or more of a strip 12% deep). Warmth alone is not enough: the +// evening tint makes outside 50..72% warm (r256, r257, Sandgem), and +// Oreburgh's ground 58..69% by day. Of 1412 lab screenshots, the rule calls +// every outside one outside, at all hours, and the rooms rooms. A fade is +// dark everywhere but not warm, so it is not inside. +bool indoors(const ImageViewRGB32& screen); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp index 6b63b5cf71..f79677bbe1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -1,148 +1,148 @@ -/* BDSP AutoStory - Location - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include "CommonFramework/ImageTools/ImageBoxes.h" -#include "CommonFramework/ImageTools/ImageStats.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/Language.h" -#include "CommonTools/Images/ImageFilter.h" -#include "CommonTools/OCR/OCR_RawTesseractOCR.h" -#include "PokemonBDSP_AutoStory_Location.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -// The location banner: a white box at the top left that names a map for a -// few seconds after the player enters it. Left of the centred name it is plain -// white (251 on console B, against 27-75 for the trees there otherwise). -const ImageFloatBox LOCATION_BANNER_WHITE{0.035, 0.035, 0.085, 0.050}; -const ImageFloatBox LOCATION_BANNER_TEXT{0.100, 0.025, 0.300, 0.080}; - -// The save card's header: the map's name in salmon (255, 152, 140) on near -// white (245), top left, right of the card's red border. Too faint for OCR -// as it is: the reddish pixels are made black on white first. -const ImageFloatBox SAVE_SCREEN_LOCATION{0.036, 0.045, 0.274, 0.070}; -const uint32_t SAVE_SCREEN_TEXT_MIN = 0xff960000; -const uint32_t SAVE_SCREEN_TEXT_MAX = 0xffffa9a9; - -std::string digits_of(const std::string& text){ - std::string out; - for (char ch : text){ - if (std::isdigit((unsigned char)ch)){ - out.push_back(ch); - } - } - return out; -} - -} - - -std::string normalize_location_name(const std::string& text){ - std::string out; - for (unsigned char ch : text){ - if (std::isalnum(ch)){ - out.push_back((char)std::tolower(ch)); - } - } - return out; -} - -namespace{ -Language& story_language_setting(){ - static Language language = Language::English; - return language; -} -} - -Language story_language(){ - return story_language_setting(); -} - -void set_story_language(Language language){ - story_language_setting() = language; -} - -bool story_reads_text(){ - return story_reads_text(story_language()); -} - -bool story_reads_text(Language language){ - return language == Language::English; -} - -bool location_banner_showing(const ImageViewRGB32& screen){ - const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); - return white.average.sum() >= 690 && white.stddev.sum() <= 30; -} - -std::string location_banner_stats(const ImageViewRGB32& screen){ - const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); - return "mean " + std::to_string((int)white.average.sum()) + ", spread " + std::to_string((int)white.stddev.sum()); -} - -std::string read_location_banner(const ImageViewRGB32& screen){ - if (!location_banner_showing(screen)){ - return ""; - } - if (!story_reads_text()){ - return UNREAD_LOCATION; - } - return normalize_location_name(OCR::tesseract_ocr_read(Language::English, - extract_box_reference(screen, LOCATION_BANNER_TEXT))); -} - -std::string read_save_screen_location(const ImageViewRGB32& screen){ - if (!story_reads_text()){ - return UNREAD_LOCATION; - } - const ImageRGB32 text = to_blackwhite_rgb32_range( - extract_box_reference(screen, SAVE_SCREEN_LOCATION), true, SAVE_SCREEN_TEXT_MIN, SAVE_SCREEN_TEXT_MAX); - return normalize_location_name(OCR::tesseract_ocr_read(Language::English, text)); -} - -bool location_banner_matches(const std::string& read, const std::string& expected){ - // No reader for the game's language: a banner (or the save card) that - // showed is all there is; where we stand is the walk's to say. - if (read == UNREAD_LOCATION){ - return true; - } - const std::string a = normalize_location_name(read); - const std::string b = normalize_location_name(expected); - if (a.empty() || b.empty()){ - return false; - } - if (digits_of(a) != digits_of(b)){ - return false; - } - // Levenshtein distance. - std::vector row(b.size() + 1); - for (size_t j = 0; j <= b.size(); j++){ - row[j] = j; - } - for (size_t i = 1; i <= a.size(); i++){ - size_t diagonal = row[0]; - row[0] = i; - for (size_t j = 1; j <= b.size(); j++){ - const size_t above = row[j]; - row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] == b[j - 1] ? 0 : 1)}); - diagonal = above; - } - } - return row[b.size()] <= std::max(1, b.size() / 4); -} - - -} -} -} +/* BDSP AutoStory - Location + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "CommonFramework/ImageTools/ImageBoxes.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/Language.h" +#include "CommonTools/Images/ImageFilter.h" +#include "CommonTools/OCR/OCR_RawTesseractOCR.h" +#include "PokemonBDSP_AutoStory_Location.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// The location banner: a white box at the top left that names a map for a +// few seconds after the player enters it. Left of the centred name it is plain +// white (251 on console B, against 27-75 for the trees there otherwise). +const ImageFloatBox LOCATION_BANNER_WHITE{0.035, 0.035, 0.085, 0.050}; +const ImageFloatBox LOCATION_BANNER_TEXT{0.100, 0.025, 0.300, 0.080}; + +// The save card's header: the map's name in salmon (255, 152, 140) on near +// white (245), top left, right of the card's red border. Too faint for OCR +// as it is: the reddish pixels are made black on white first. +const ImageFloatBox SAVE_SCREEN_LOCATION{0.036, 0.045, 0.274, 0.070}; +const uint32_t SAVE_SCREEN_TEXT_MIN = 0xff960000; +const uint32_t SAVE_SCREEN_TEXT_MAX = 0xffffa9a9; + +std::string digits_of(const std::string& text){ + std::string out; + for (char ch : text){ + if (std::isdigit((unsigned char)ch)){ + out.push_back(ch); + } + } + return out; +} + +} + + +std::string normalize_location_name(const std::string& text){ + std::string out; + for (unsigned char ch : text){ + if (std::isalnum(ch)){ + out.push_back((char)std::tolower(ch)); + } + } + return out; +} + +namespace{ +Language& story_language_setting(){ + static Language language = Language::English; + return language; +} +} + +Language story_language(){ + return story_language_setting(); +} + +void set_story_language(Language language){ + story_language_setting() = language; +} + +bool story_reads_text(){ + return story_reads_text(story_language()); +} + +bool story_reads_text(Language language){ + return language == Language::English; +} + +bool location_banner_showing(const ImageViewRGB32& screen){ + const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); + return white.average.sum() >= 690 && white.stddev.sum() <= 30; +} + +std::string location_banner_stats(const ImageViewRGB32& screen){ + const ImageStats white = image_stats(extract_box_reference(screen, LOCATION_BANNER_WHITE)); + return "mean " + std::to_string((int)white.average.sum()) + ", spread " + std::to_string((int)white.stddev.sum()); +} + +std::string read_location_banner(const ImageViewRGB32& screen){ + if (!location_banner_showing(screen)){ + return ""; + } + if (!story_reads_text()){ + return UNREAD_LOCATION; + } + return normalize_location_name(OCR::tesseract_ocr_read(Language::English, + extract_box_reference(screen, LOCATION_BANNER_TEXT))); +} + +std::string read_save_screen_location(const ImageViewRGB32& screen){ + if (!story_reads_text()){ + return UNREAD_LOCATION; + } + const ImageRGB32 text = to_blackwhite_rgb32_range( + extract_box_reference(screen, SAVE_SCREEN_LOCATION), true, SAVE_SCREEN_TEXT_MIN, SAVE_SCREEN_TEXT_MAX); + return normalize_location_name(OCR::tesseract_ocr_read(Language::English, text)); +} + +bool location_banner_matches(const std::string& read, const std::string& expected){ + // No reader for the game's language: a banner (or the save card) that + // showed is all there is; where we stand is the walk's to say. + if (read == UNREAD_LOCATION){ + return true; + } + const std::string a = normalize_location_name(read); + const std::string b = normalize_location_name(expected); + if (a.empty() || b.empty()){ + return false; + } + if (digits_of(a) != digits_of(b)){ + return false; + } + // Levenshtein distance. + std::vector row(b.size() + 1); + for (size_t j = 0; j <= b.size(); j++){ + row[j] = j; + } + for (size_t i = 1; i <= a.size(); i++){ + size_t diagonal = row[0]; + row[0] = i; + for (size_t j = 1; j <= b.size(); j++){ + const size_t above = row[j]; + row[j] = std::min({row[j] + 1, row[j - 1] + 1, diagonal + (a[i - 1] == b[j - 1] ? 0 : 1)}); + diagonal = above; + } + } + return row[b.size()] <= std::max(1, b.size() / 4); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h index 24bdc2ecc0..683874d64c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h @@ -1,67 +1,67 @@ -/* BDSP AutoStory - Location - * - * From: https://github.com/PokemonAutomation/ - * - * Where the game says the player is: the location banner shown on entering - * a map, and the map named on the save screen's card. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Location_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Location_H - -#include -#include "CommonFramework/Language.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Lower-case letters and digits only: "Route 202" and "ROUTE 2O2 " compare -// by "route202". -std::string normalize_location_name(const std::string& text); - -// The game's language, for the reads that still use text (item names, the -// location banner, a sign's or talk's first box), set once from the -// program's options (StoryRunOptions) before the story runs; English until -// then. story_reads_text() says whether those reads have a model and -// dictionary for it (English only, so far); when not, callers use -// language-free checks: the banner's box showing, a sign's box opening, the -// first row of a Bag pocket. -Language story_language(); -void set_story_language(Language language); -bool story_reads_text(); -bool story_reads_text(Language language); - -// What the reads below return for a banner or save card that showed, when -// there is no text reader for the game's language (story_reads_text()): -// location_banner_matches() takes it as a match. -inline const std::string UNREAD_LOCATION = "(unread)"; - -// The location banner (the white box at the top left that names a map for a -// few seconds after the player enters it), read by OCR and normalized; -// empty when no banner shows. -std::string read_location_banner(const ImageViewRGB32& screen); -// Whether the location banner's white box shows, without reading it. -bool location_banner_showing(const ImageViewRGB32& screen); -// The banner box's mean and spread, for the log. -std::string location_banner_stats(const ImageViewRGB32& screen); - -// The map named at the top left of the save screen's card ("Sandgem Town", -// "Route 202"), read by OCR and normalized; empty if nothing reads. -std::string read_save_screen_location(const ImageViewRGB32& screen); - -// Whether the location banner's OCR `read` names the map `expected`. Both -// are compared as lower-case letters and digits. OCR drops or swaps letters -// ("andgemltown" for Sandgem Town on r109), so up to a quarter of the name's -// letters may differ; the digits must match exactly, so Route 201 is never -// read as Route 202. -bool location_banner_matches(const std::string& read, const std::string& expected); - - -} -} -} -#endif +/* BDSP AutoStory - Location + * + * From: https://github.com/PokemonAutomation/ + * + * Where the game says the player is: the location banner shown on entering + * a map, and the map named on the save screen's card. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Location_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Location_H + +#include +#include "CommonFramework/Language.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Lower-case letters and digits only: "Route 202" and "ROUTE 2O2 " compare +// by "route202". +std::string normalize_location_name(const std::string& text); + +// The game's language, for the reads that still use text (item names, the +// location banner, a sign's or talk's first box), set once from the +// program's options (StoryRunOptions) before the story runs; English until +// then. story_reads_text() says whether those reads have a model and +// dictionary for it (English only, so far); when not, callers use +// language-free checks: the banner's box showing, a sign's box opening, the +// first row of a Bag pocket. +Language story_language(); +void set_story_language(Language language); +bool story_reads_text(); +bool story_reads_text(Language language); + +// What the reads below return for a banner or save card that showed, when +// there is no text reader for the game's language (story_reads_text()): +// location_banner_matches() takes it as a match. +inline const std::string UNREAD_LOCATION = "(unread)"; + +// The location banner (the white box at the top left that names a map for a +// few seconds after the player enters it), read by OCR and normalized; +// empty when no banner shows. +std::string read_location_banner(const ImageViewRGB32& screen); +// Whether the location banner's white box shows, without reading it. +bool location_banner_showing(const ImageViewRGB32& screen); +// The banner box's mean and spread, for the log. +std::string location_banner_stats(const ImageViewRGB32& screen); + +// The map named at the top left of the save screen's card ("Sandgem Town", +// "Route 202"), read by OCR and normalized; empty if nothing reads. +std::string read_save_screen_location(const ImageViewRGB32& screen); + +// Whether the location banner's OCR `read` names the map `expected`. Both +// are compared as lower-case letters and digits. OCR drops or swaps letters +// ("andgemltown" for Sandgem Town on r109), so up to a quarter of the name's +// letters may differ; the digits must match exactly, so Route 201 is never +// read as Route 202. +bool location_banner_matches(const std::string& read, const std::string& expected); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp index 7ac01390db..eab60111f3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp @@ -1,42 +1,42 @@ -/* BDSP AutoStory - Overlay Log - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include "PokemonBDSP_AutoStory_OverlayLog.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color){ - // The log box is 35% of the video wide at a font 4% of its height: about - // 34 characters. - constexpr size_t WIDTH = 34; - std::string line; - size_t start = 0; - while (start < text.size()){ - size_t end = text.find(' ', start); - if (end == std::string::npos){ - end = text.size(); - } - const std::string word = text.substr(start, end - start); - if (!line.empty() && line.size() + 1 + word.size() > WIDTH){ - overlay.add_log(line, color); - line = " "; - } - line += (line.empty() || line == " " ? "" : " ") + word; - start = end + 1; - } - if (!line.empty()){ - overlay.add_log(line, color); - } -} - - - -} -} -} +/* BDSP AutoStory - Overlay Log + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "PokemonBDSP_AutoStory_OverlayLog.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color){ + // The log box is 35% of the video wide at a font 4% of its height: about + // 34 characters. + constexpr size_t WIDTH = 34; + std::string line; + size_t start = 0; + while (start < text.size()){ + size_t end = text.find(' ', start); + if (end == std::string::npos){ + end = text.size(); + } + const std::string word = text.substr(start, end - start); + if (!line.empty() && line.size() + 1 + word.size() > WIDTH){ + overlay.add_log(line, color); + line = " "; + } + line += (line.empty() || line == " " ? "" : " ") + word; + start = end + 1; + } + if (!line.empty()){ + overlay.add_log(line, color); + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h index f030b54f1c..803b9bc4aa 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h @@ -1,27 +1,27 @@ -/* BDSP AutoStory - Overlay Log - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_OverlayLog_H -#define PokemonAutomation_PokemonBDSP_AutoStory_OverlayLog_H - -#include -#include "Common/Cpp/Color.h" -#include "CommonFramework/VideoPipeline/VideoOverlay.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Adds `text` to the video overlay's log, split at spaces into lines short -// enough for the log box, which draws each line on one row and cuts it off. -void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color); - - -} -} -} -#endif +/* BDSP AutoStory - Overlay Log + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_OverlayLog_H +#define PokemonAutomation_PokemonBDSP_AutoStory_OverlayLog_H + +#include +#include "Common/Cpp/Color.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Adds `text` to the video overlay's log, split at spaces into lines short +// enough for the log box, which draws each line on one row and cuts it off. +void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index f4ed4a1bb1..da8af82032 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -1,799 +1,799 @@ -/* BDSP AutoStory - Planned Route - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include "Common/Cpp/PrettyPrint.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "Pokemon/Navigation/Pokemon_RouteLog.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP_AutoStory_Location.h" -#include "PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP_AutoStory_PlannedRoute.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; -using namespace Pokemon; - - -namespace{ - -// World tiles as tiles of the named maps, for the shared route log -// (Pokemon_RouteLog.h); a tile no map holds is a "world" tile. -std::vector route_tiles(const StoryWorld& maps, const std::vector& tiles){ - std::vector out; - for (const TilePos& pos : tiles){ - const std::optional local = maps.to_local(pos); - out.push_back(local ? RouteTile{local->map, local->pos} : RouteTile{"world", pos}); - } - return out; -} - -std::string describe(const StoryWorld& maps, const std::vector& tiles){ - return Pokemon::describe(route_tiles(maps, tiles)); -} - -RouteLogLeg route_log_leg(const TileLeg& leg){ - return {.action = RouteLegAction::WALK, .direction = leg.direction, .tiles = leg.tiles, - .end = leg.ends_at_wall ? RouteLegEnd::WALL : RouteLegEnd::OPEN}; -} - -PlannedRouteLeg planned_route_leg(const StoryWorld& maps, const PlannedLeg& leg){ - return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; -} - -void log_plan(VideoStream& stream, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ - std::vector logged; - for (const PlannedLeg& leg : legs){ - logged.push_back(planned_route_leg(maps, leg)); - } - stream.log(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged)); -} - -// Logs a leg that ended on its planned tiles, and how that is known: an -// open leg counts its taps through a wild battle, so its end is assumed; a -// leg to a wall is walked again to its wall. -void log_leg_done(VideoStream& stream, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ - const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; - stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), - assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); -} - -// The tiles the player passes when walking `tiles` tiles from `from`, the -// first one after `from` first. Stops early where the player is stopped. -std::vector leg_path(const StoryWorld& grid, TilePos from, Direction direction, size_t tiles){ - std::vector path; - size_t travelled = 0; - while (travelled < tiles){ - const std::optional next = step(grid, from, direction); - if (!next){ - break; - } - travelled += std::abs(next->x - from.x) + std::abs(next->y - from.y); - from = *next; - path.push_back(from); - } - return path; -} - -// Holding toward a wall with people or Pokemon moving beside us never reads -// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking -// scrolls the view far more (35 to 90). True if holding toward (x, y) for -// 1.6 s changes the screen less than walking would: we stand at a wall. -bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ - constexpr double WALKING_CHANGE = 25.0; - context.wait_for_all_requests(); - VideoSnapshot before = stream.video().snapshot(); - double most = 0; - for (size_t chunk = 0; chunk < 4 && before; chunk++){ - pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); - context.wait_for_all_requests(); - VideoSnapshot after = stream.video().snapshot(); - if (!after){ - return false; - } - most = std::max(most, walk_screen_change(*before.frame, *after.frame)); - before = std::move(after); - } - stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + - (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); - return before && most < WALKING_CHANGE; -} - -// The map all of `tiles` are on, or empty if they are on several or none. -std::string map_of(const StoryWorld& maps, const std::vector& tiles){ - std::string name; - for (const TilePos& pos : tiles){ - const std::optional tile = maps.to_local(pos); - if (!tile || (!name.empty() && tile->map != name)){ - return ""; - } - name = tile->map; - } - return name; -} - -} - - -namespace{ - -// Sorted by row, then column, without repeats. -std::vector sorted_unique(std::vector tiles){ - std::sort(tiles.begin(), tiles.end(), [](const TilePos& a, const TilePos& b){ - return a.y != b.y ? a.y < b.y : a.x < b.x; - }); - tiles.erase(std::unique(tiles.begin(), tiles.end()), tiles.end()); - return tiles; -} - -std::string quoted_list(const std::vector& reads){ - std::string text; - for (const std::string& read : reads){ - text += (text.empty() ? "\"" : ", \"") + read + "\""; - } - return text; -} - -// Long enough to reach the wall from the farthest tile, with room for a -// slow start; past the wall the player stands still. -Milliseconds wall_limit(const TileLeg& leg){ - const Milliseconds per_tile = route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; - return ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; -} - -// Tiles a walk to the wall moved, from the time it walked. -size_t tiles_moved(const WalkResult& walk, Milliseconds per_tile){ - const Milliseconds moving = walk.walked - ROUTE_START_DELAY - - (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); - return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); -} - -// Whether a leg's walk goes on to the next leg or the route plans again. -enum class LegNext{ - NEXT_LEG, - REPLAN, -}; - -// One planned leg as it is walked: where it goes, and the location -// banners read on the way. -struct LegWalk{ - const PlannedLeg& planned; - std::string label; - JoystickPosition stick; - std::string map_before; - std::string map_after; - std::vector banners = {}; - WalkResult result{WalkStop::LIMIT, 0ms, 0}; - // Tiles the first tap of an open leg moved (see open_leg()). - size_t turn_moved = 0; -}; - -bool banner_ok(const LegWalk& walk){ - return std::any_of(walk.banners.begin(), walk.banners.end(), [&](const std::string& read){ - return location_banner_matches(read, walk.map_after); - }); -} - -// Notes the location banners the walk passes, to check map changes. A wild -// battle or a second walk to the wall may follow the banner: any banner -// seen during the leg counts. -bool watch_banner(LegWalk& walk, const ImageViewRGB32& screen){ - if (walk.banners.empty() || (!walk.map_after.empty() && !banner_ok(walk) && walk.banners.size() < 8)){ - const std::string read = read_location_banner(screen); - if (!read.empty() && (walk.banners.empty() || walk.banners.back() != read)){ - walk.banners.push_back(read); - } - } - return false; -} - -// Running on the D-pad, reading the banners on the way. -DpadWalkOptions run_watching(LegWalk& walk){ - return {.arrived = [&walk](const ImageViewRGB32& screen){ return watch_banner(walk, screen); }, .pace = DpadPace::RUN}; -} - -// walk_route(): plans, walks each leg, and plans again from where the walk -// turned out to be when something stopped it. -class RouteWalker{ -public: - RouteWalker( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, RouteTrainers& trainers, const RouteWalkPlan& plan - ) - : m_stream(stream) - , m_context(context) - , m_escape(escape) - , m_maps(maps) - , m_grid(maps) - , m_trainers(trainers) - , m_plan(plan) - {} - - std::vector run(std::vector from); - -private: - std::vector plan_legs() const; - LegNext walk_leg(const PlannedLeg& planned, size_t number, size_t count); - std::vector arrive() const; - - // Legs to a wall. - LegNext wall_leg(LegWalk& walk); - size_t unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest); - LegNext stopped_short_of_wall(LegWalk& walk, size_t walked_tiles); - void check_at_wall(LegWalk& walk); - void on_to_wall(LegWalk& walk); - - // Open legs, tapped. - LegNext open_leg(LegWalk& walk); - void turn_tap(LegWalk& walk); - LegNext blocked_open_leg(LegWalk& walk, size_t moved); - - // After each leg. - void check_banners(LegWalk& walk); - LegNext after_leg(LegWalk& walk); - std::vector watching_trainers(const std::vector>& paths) const; - LegNext unknown_trainer(LegWalk& walk, const std::vector>& paths); - LegNext known_trainer(LegWalk& walk, const std::vector>& paths, const std::vector& watching); - - void talk_to_blocker(const std::string& leg_label); - LegNext replan_from(const LegWalk& walk, std::vector stopped, const std::string& why); - void mark_blocker(const LegWalk& walk, const std::vector& stopped); - - VideoStream& m_stream; - ProControllerContext& m_context; - const WildBattleEscape& m_escape; - const StoryWorld& m_maps; - // Our own copy, so things found in the way can be added to it. - StoryWorld m_grid; - RouteTrainers& m_trainers; - const RouteWalkPlan& m_plan; - std::vector m_from; - // Which way the player faces, once a leg has set it. - std::optional m_facing; - // Every banner read on the way, to check the map we arrive on against. - std::vector m_all_banners; - std::string m_map_start; -}; - -std::vector RouteWalker::run(std::vector from){ - // Tiles to avoid are ground the game lets us walk on (a trainer's line - // of sight), so they are off the map, not walls: a leg must not end - // "at" one, or the walk would run on through it. - for (const TilePos& pos : m_plan.avoid){ - if (std::find(from.begin(), from.end(), pos) == from.end()){ - m_grid.uncover(pos); - } - } - m_from = std::move(from); - m_map_start = map_of(m_maps, m_from); - for (size_t plans = 0; plans < 8; plans++){ - const std::vector legs = plan_legs(); - route_status(m_stream, m_plan.label + ": " + std::to_string(legs.size()) + " legs planned"); - log_plan(m_stream, m_maps, m_plan.label, m_from, legs); - bool replanned = false; - for (size_t number = 1; number <= legs.size() && !replanned; number++){ - replanned = walk_leg(legs[number - 1], number, legs.size()) == LegNext::REPLAN; - } - if (!replanned){ - return arrive(); - } - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": too many plans.", m_stream); -} - -std::vector RouteWalker::plan_legs() const{ - std::optional> legs = plan_robust_route(m_grid, m_from, m_plan.goal, m_plan.costs); - if (!legs){ - // From a few possible tiles the exact-open-leg costs found no - // plan where the default ones did (r240a, three tiles on Route - // 204's row 27); the default plan is longer but sound. - m_stream.log("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + - "; trying the default costs.", COLOR_ORANGE); - legs = plan_robust_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); - } - if (!legs){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": no plan from " + describe(m_maps, m_from) + ".", m_stream); - } - return std::move(*legs); -} - -LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t count){ - route_status(m_stream, route_leg_line(number, count, planned_route_leg(m_maps, planned))); - LegWalk walk{ - .planned = planned, - .label = m_plan.label + ": " + to_string(planned.leg), - .stick = route_stick(planned.leg.direction), - .map_before = map_of(m_maps, m_from), - .map_after = map_of(m_maps, planned.after), - }; - const LegNext next = planned.leg.ends_at_wall ? wall_leg(walk) : open_leg(walk); - if (next == LegNext::REPLAN){ - return next; - } - m_facing = planned.leg.direction; - check_banners(walk); - if (walk.result.stop == WalkStop::TRANSITION){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": unexpected transition.", m_stream); - } - return after_leg(walk); -} - -// Legs whose ends span two maps skip the per-leg check; the walk as a -// whole must still have seen the banner of the map it ends on. -std::vector RouteWalker::arrive() const{ - const std::string map_end = map_of(m_maps, m_from); - const bool changed_map = !m_map_start.empty() && !map_end.empty() && map_end != m_map_start; - const bool seen = std::any_of(m_all_banners.begin(), m_all_banners.end(), [&](const std::string& read){ - return location_banner_matches(read, map_end); - }); - if (changed_map && !seen){ - const std::string reads = quoted_list(m_all_banners); - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + - "'s location banner never showed (read " + (reads.empty() ? "nothing" : reads) + ").", m_stream); - } - m_stream.log(ROUTE_LOG_TAG + route_arrived_line(m_plan.label, route_tiles(m_maps, m_from))); - return m_from; -} - - -// Run (B held) on the D-pad: a leg to a wall ends at the wall however fast -// we get there, and the D-pad keeps to the row. -LegNext RouteWalker::wall_leg(LegWalk& walk){ - const TileLeg& leg = walk.planned.leg; - const Milliseconds limit = wall_limit(leg); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); - // A wild battle may end the walk early. Walking to a wall again ends in - // the same place, so finish the leg. - const bool interrupted = walk.result.wild_battles > 0; - for (size_t again = 0; again < 3 && walk.result.wild_battles > 0 && - (walk.result.stop == WalkStop::LIMIT || walk.result.stop == WalkStop::BLOCKED); again++ - ){ - m_stream.log("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again."); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); - } - if (interrupted){ - return LegNext::NEXT_LEG; - } - // A wall well before the nearest one the map has: something the map - // does not know is in the way. Work out where we stopped from the time - // walked, put it on our map and plan again from there. - size_t nearest = SIZE_MAX; - for (const TilePos& pos : m_from){ - nearest = std::min(nearest, slide_to_wall(m_grid, pos, leg.direction).tiles); - } - size_t walked_tiles = tiles_moved(walk.result, route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP); - walked_tiles = unstick_wall_leg(walk, walked_tiles, nearest); - // Any stop short of the wall's distance: someone who stays put is - // talked to, and the leg is planned again from where we are. Within the - // time count's slack this may be the wall itself, where A does nothing - // and the new plan walks no further (r239a: a wanderer 3 tiles short - // passed as "about 8 of 10", and every later leg was off by those tiles). - if (walk.result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ - return stopped_short_of_wall(walk, walked_tiles); - } - // A leg to the wall that ran out of time is not at its wall: the open - // leg before it stopped short, and it ran along another row (r127a went - // west along Sandgem's row 13 for row 14, and the next leg north walked - // into the lab). Out of tall grass a stop reads, so walk on: a player - // still moving is not where the plan has him. - if (walk.result.stop == WalkStop::LIMIT && route_wall_readable(m_grid, walk.planned.after)){ - check_at_wall(walk); - } - return LegNext::NEXT_LEG; -} - -// A walk to the wall that stopped well short of it: tap through a corner, -// then wait for someone in the way. Returns the tiles walked by then. -size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest){ - const Milliseconds per_tile = route_ms_per_tile(walk.planned.leg.direction) / ROUTE_RUN_SPEEDUP; - const Milliseconds limit = wall_limit(walk.planned.leg); - // A stick walk can catch a corner when we stand part of a tile off - // centre, where a D-pad tap, which moves whole tiles, gets through: on - // Route 203's stairs the walk west stopped nine times in a row and two - // taps crossed (r225a, 2026-10-31). Tap twice, then walk on to the wall. - if (walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ - const Direction direction = walk.planned.leg.direction; - const DpadPosition dpad = - direction == Direction::NORTH ? DPAD_UP : - direction == Direction::SOUTH ? DPAD_DOWN : - direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); - for (size_t tap = 0; tap < 2; tap++){ - pbf_press_dpad(m_context, dpad, 160ms, 640ms); - } - m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); - walked_tiles += tiles_moved(walk.result, per_tile); - } - // Someone walking about may stand in the way for a moment: wait and - // walk to the wall again before calling it an obstacle. - for (size_t again = 0; again < 2 && walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++){ - m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(m_context, 2500ms); - m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); - walked_tiles += tiles_moved(walk.result, per_tile); - } - return walked_tiles; -} - -LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ - // Someone who stays put: talk to them. A trainer we walked up to from - // the side starts the battle this way. - talk_to_blocker(walk.label); - std::vector stopped; - for (const TilePos& pos : m_from){ - const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, SIZE_MAX); - for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ - stopped.push_back(k == 0 ? pos : path[k - 1]); - } - } - stopped = sorted_unique(std::move(stopped)); - if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": stopped early, and no tile fits.", m_stream); - } - mark_blocker(walk, stopped); - return replan_from(walk, std::move(stopped), "stopped early after about " + std::to_string(walked_tiles) + " tiles"); -} - -void RouteWalker::check_at_wall(LegWalk& walk){ - const WalkResult check = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, 2000ms, - walk.label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); - if (check.stop == WalkStop::DIALOG){ - // A trainer who saw us froze the walk (the "!") until its time ran - // out, then walked over (r241a: Aroma Lady Taylor's line crosses - // Route 204's corridor). Battle, then walk on to the wall: where - // they saw us from is not reliable (BDSP's trainers stand a few - // tiles from Platinum's, r242a), and a leg to a wall ends at its - // wall however far it got. - route_status(m_stream, walk.label + ": a trainer stopped us on the way; battling."); - m_trainers.fight("a trainer on the way"); - m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), - walk.label + ": on to the wall", run_watching(walk)); - } - if (check.stop != WalkStop::BLOCKED && - !(check.stop == WalkStop::LIMIT && holding_against_wall(m_stream, m_context, walk.stick.x, walk.stick.y, walk.label + ": at the wall?")) - ){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": ran out of time short of the wall, and walking on gets " + - walk_stop_name(check.stop) + ": not where the plan has us.", m_stream); - } -} - -// After a battle on a leg to a wall: a leg to a wall ends at its wall -// however far it got, so walk on to it and carry on. -void RouteWalker::on_to_wall(LegWalk& walk){ - m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), - walk.label + ": on to the wall", run_watching(walk)); - m_from = walk.planned.after; - log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, 0); -} - - -// Whole tiles on the D-pad, a tap each: a tap moves exactly one tile, while -// a stick hold timed for three moved one in the Ravaged Path (r235b) and the -// belief drifted four rows. -LegNext RouteWalker::open_leg(LegWalk& walk){ - const TileLeg& leg = walk.planned.leg; - if (m_facing != leg.direction){ - turn_tap(walk); - } - if (leg.tiles > walk.turn_moved){ - walk.result = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - walk.turn_moved, walk.label); - } - // A wild battle starts once the step onto the grass is done, so the - // taps still count; the walk went on after it. - if (walk.result.wild_battles > 0){ - m_stream.log("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + - " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); - } - // Taps that moved nothing: someone stands in the way (the Youngster - // below the Ravaged Path wanders across the row under the cliff, r239a, - // r240a). Two still taps end the walk, so the tiles moved are the taps - // less those two. Wait once for them to move on and tap the rest (r246a - // lost 20 s per block on three waits); someone who stays is talked to, - // put on the map, and the leg planned again from the tile we stand on, - // which the taps give exactly. - size_t moved = std::min(leg.tiles, walk.result.moved_chunks + walk.turn_moved); - if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ - m_stream.log("[Route] " + walk.label + ": blocked after " + std::to_string(moved) + " of " + - std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(m_context, 2500ms); - m_context.wait_for_all_requests(); - const WalkResult rest = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - moved, walk.label + ": the rest"); - moved = std::min(leg.tiles, moved + rest.moved_chunks); - walk.result.stop = rest.stop; - walk.result.walked += rest.walked; - walk.result.wild_battles += rest.wild_battles; - } - if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ - return blocked_open_leg(walk, moved); - } - return LegNext::NEXT_LEG; -} - -// The first tap of a leg was meant to turn, but a 160 ms D-pad tap turns -// and steps whichever way the player faced (r248a: all seven did; r246a: -// "south 1" out of the cave went two tiles, onto the Youngster's row), so -// the tap is measured like any other and a step counts for the leg. -void RouteWalker::turn_tap(LegWalk& walk){ - const DpadPosition dpad = walk.stick.x < 0 ? DPAD_LEFT : walk.stick.x > 0 ? DPAD_RIGHT : walk.stick.y > 0 ? DPAD_UP : DPAD_DOWN; - m_context.wait_for_all_requests(); - VideoSnapshot before = m_stream.video().snapshot(); - pbf_press_dpad(m_context, dpad, 160ms, 700ms); - m_context.wait_for_all_requests(); - VideoSnapshot after = m_stream.video().snapshot(); - if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ - walk.turn_moved = 1; - m_stream.log("[Route] " + walk.label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); - } -} - -LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ - talk_to_blocker(walk.label); - std::vector stopped; - for (const TilePos& pos : m_from){ - const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, moved); - stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); - } - stopped = sorted_unique(std::move(stopped)); - mark_blocker(walk, stopped); - return replan_from(walk, std::move(stopped), - "blocked after " + std::to_string(moved) + " of " + std::to_string(walk.planned.leg.tiles) + " tiles"); -} - - -// Entering another map shows its name; a missing or wrong name means the -// walk did not end where the plan says. From tiles on several maps (a leg -// not confirmed) the banner may already have shown. -void RouteWalker::check_banners(LegWalk& walk){ - m_context.wait_for_all_requests(); - if (VideoSnapshot screen = m_stream.video().snapshot()){ - watch_banner(walk, *screen.frame); - } - const std::string banner_text = quoted_list(walk.banners); - m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); - if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + - (banner_text.empty() ? "nothing" : banner_text) + ".", m_stream); - } - if (!banner_text.empty()){ - m_stream.log("[Route] " + walk.label + ": location banner " + banner_text + "."); - } - // Where each leg ended, to compare with the plan after a run. - if (VideoSnapshot screen = m_stream.video().snapshot()){ - const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; - Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(walk.label) + ".png"); - } -} - -// A trainer who sees us freezes the player and walks over; the dialogue -// may open after the walk has ended. -LegNext RouteWalker::after_leg(LegWalk& walk){ - const TileLeg& leg = walk.planned.leg; - std::vector> paths; - for (const TilePos& pos : m_from){ - paths.push_back(leg_path(m_grid, pos, leg.direction, leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); - } - const std::vector watching = watching_trainers(paths); - bool talking = walk.result.stop == WalkStop::DIALOG; - if (!talking){ - ShortDialogWatcher dialog(COLOR_YELLOW); - const Milliseconds wait = watching.empty() ? 500ms : 4000ms; - talking = wait_until(m_stream, m_context, wait, {{dialog}}) == 0; - } - if (!talking){ - m_from = walk.planned.after; - log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, walk.result.wild_battles); - return LegNext::NEXT_LEG; - } - if (watching.empty()){ - return unknown_trainer(walk, paths); - } - return known_trainer(walk, paths, watching); -} - -// The trainers whose sight one of the leg's possible paths enters. -std::vector RouteWalker::watching_trainers(const std::vector>& paths) const{ - std::vector watching; - for (size_t t = 0; t < m_trainers.sights.size(); t++){ - const std::vector sight = sight_tiles(m_grid, m_trainers.sights[t]); - const bool seen = std::any_of(paths.begin(), paths.end(), [&](const std::vector& path){ - return std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ - return std::find(sight.begin(), sight.end(), pos) != sight.end(); - }); - }); - if (seen){ - watching.push_back(t); - } - } - return watching; -} - -// A trainer the map does not have. Fight, then plan again from anywhere -// the leg could have stopped. -LegNext RouteWalker::unknown_trainer(LegWalk& walk, const std::vector>& paths){ - std::vector anywhere = m_from; - for (const std::vector& path : paths){ - anywhere.insert(anywhere.end(), path.begin(), path.end()); - } - anywhere = sorted_unique(std::move(anywhere)); - route_status(m_stream, m_plan.label + ": a trainer the map lacks stopped us; fighting."); - m_trainers.fight("unknown trainer"); - if (walk.planned.leg.ends_at_wall){ - on_to_wall(walk); - return LegNext::NEXT_LEG; - } - m_from = std::move(anywhere); - return LegNext::REPLAN; -} - -LegNext RouteWalker::known_trainer(LegWalk& walk, const std::vector>& paths, const std::vector& watching){ - // Each possible start tile was stopped on the first tile of its path - // inside a watching trainer's sight. - const TrainerSight trainer = m_trainers.sights[watching[0]]; - std::vector stopped; - for (const std::vector& path : paths){ - const auto first_seen = std::find_if(path.begin(), path.end(), [&](const TilePos& pos){ - return std::any_of(watching.begin(), watching.end(), [&](size_t t){ - const std::vector sight = sight_tiles(m_grid, m_trainers.sights[t]); - return std::find(sight.begin(), sight.end(), pos) != sight.end(); - }); - }); - if (first_seen != path.end()){ - stopped.push_back(*first_seen); - } - } - if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": " + trainer.name + " talked, but no path enters their sight.", m_stream); - } - m_stream.log(ROUTE_LOG_TAG + route_trainer_line(m_plan.label, trainer.name, route_tiles(m_maps, stopped))); - route_status(m_stream, "Fighting " + trainer.name + "."); - m_trainers.fight(trainer.name); - m_trainers.sights.erase(m_trainers.sights.begin() + watching[0]); - // Their sight line only says roughly where: BDSP's trainers stand a few - // tiles from Platinum's (r242a). A leg to a wall walks on to its wall. - if (walk.planned.leg.ends_at_wall){ - on_to_wall(walk); - return LegNext::NEXT_LEG; - } - // An open leg counts only the taps that moved: the trainer freezes the - // player on the tile they saw, and the taps pressed while they walk - // over move nobody (r248a: "4 taps" were 2 steps; the belief of two - // tiles then cost a 12-tile run to a wall and 5 back). - const size_t taps = std::min(walk.planned.leg.tiles, walk.result.moved_chunks + walk.turn_moved); - std::vector by_taps; - for (const TilePos& pos : m_from){ - const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, walk.planned.leg.tiles); - const size_t k = std::min(taps, path.size()); - by_taps.push_back(k == 0 ? pos : path[k - 1]); - } - return replan_from(walk, sorted_unique(std::move(by_taps)), std::to_string(taps) + " taps before the battle"); -} - - -// Presses A at whoever stopped the walk; a trainer we walked up to from -// the side starts the battle this way. -void RouteWalker::talk_to_blocker(const std::string& leg_label){ - pbf_press_button(m_context, BUTTON_A, 80ms, 200ms); - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(m_stream, m_context, 2000ms, {{dialog}}) == 0){ - route_status(m_stream, leg_label + ": someone is in the way; talking to them."); - m_trainers.fight("someone in the way"); - } -} - -// One tile we may stand on: what stopped us is on the next tile, and goes -// on our map. -void RouteWalker::mark_blocker(const LegWalk& walk, const std::vector& stopped){ - if (stopped.size() != 1){ - return; - } - const TilePos blocker = stopped[0].next(walk.planned.leg.direction); - m_grid.block(blocker); - m_stream.log("[Route] " + walk.label + ": something the map lacks is at " + - describe(m_maps, blocker) + "; marked it.", COLOR_ORANGE); -} - -LegNext RouteWalker::replan_from(const LegWalk& walk, std::vector stopped, const std::string& why){ - m_stream.log(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.planned.leg), why, route_tiles(m_maps, stopped)), COLOR_ORANGE); - m_facing = walk.planned.leg.direction; - m_from = std::move(stopped); - return LegNext::REPLAN; -} - -} - - -std::vector walk_route( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, std::vector from, - RouteTrainers& trainers, const RouteWalkPlan& plan -){ - return RouteWalker(stream, context, escape, maps, trainers, plan).run(std::move(from)); -} - -std::vector walk_waypoints( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, std::vector from, - RouteTrainers& trainers, const WaypointWalkPlan& plan -){ - for (size_t i = 0; i < plan.waypoints.size(); i++){ - const TilePos goal = plan.waypoints[i]; - from = walk_route(stream, context, escape, maps, from, trainers, { - .goal = [goal](const TilePos& pos){ return pos == goal; }, - .label = plan.label + " (" + std::to_string(i + 1) + "/" + std::to_string(plan.waypoints.size()) + - ", to " + describe(maps, goal) + ")", - .avoid = {}, - .costs = plan.costs, - }); - } - return from; -} - -void talk_to_npc( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, std::vector& at, - RouteTrainers& trainers, const NpcTalk& talk -){ - for (Direction side : talk.sides){ - const TilePos stand = talk.npc.next(side); - if (!standable(maps, stand)){ - continue; - } - at = walk_route(stream, context, escape, maps, at, trainers, { - .goal = [stand](const TilePos& pos){ return pos == stand; }, - .label = talk.label, - .avoid = {}, - .costs = route_plan_costs(), - }); - // Face the NPC: the opposite of the side we stand on. - const Direction face = - side == Direction::NORTH ? Direction::SOUTH : - side == Direction::SOUTH ? Direction::NORTH : - side == Direction::EAST ? Direction::WEST : Direction::EAST; - route_turn(stream, context, face); - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, talk.label + ": talking from " + describe(maps, stand) + "."); - return; - } - route_status(stream, talk.label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue.", stream); -} - - - -} -} -} +/* BDSP AutoStory - Planned Route + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Navigation/Pokemon_RouteLog.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_PlannedRoute.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +// World tiles as tiles of the named maps, for the shared route log +// (Pokemon_RouteLog.h); a tile no map holds is a "world" tile. +std::vector route_tiles(const StoryWorld& maps, const std::vector& tiles){ + std::vector out; + for (const TilePos& pos : tiles){ + const std::optional local = maps.to_local(pos); + out.push_back(local ? RouteTile{local->map, local->pos} : RouteTile{"world", pos}); + } + return out; +} + +std::string describe(const StoryWorld& maps, const std::vector& tiles){ + return Pokemon::describe(route_tiles(maps, tiles)); +} + +RouteLogLeg route_log_leg(const TileLeg& leg){ + return {.action = RouteLegAction::WALK, .direction = leg.direction, .tiles = leg.tiles, + .end = leg.ends_at_wall ? RouteLegEnd::WALL : RouteLegEnd::OPEN}; +} + +PlannedRouteLeg planned_route_leg(const StoryWorld& maps, const PlannedLeg& leg){ + return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; +} + +void log_plan(VideoStream& stream, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ + std::vector logged; + for (const PlannedLeg& leg : legs){ + logged.push_back(planned_route_leg(maps, leg)); + } + stream.log(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged)); +} + +// Logs a leg that ended on its planned tiles, and how that is known: an +// open leg counts its taps through a wild battle, so its end is assumed; a +// leg to a wall is walked again to its wall. +void log_leg_done(VideoStream& stream, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ + const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; + stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), + assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); +} + +// The tiles the player passes when walking `tiles` tiles from `from`, the +// first one after `from` first. Stops early where the player is stopped. +std::vector leg_path(const StoryWorld& grid, TilePos from, Direction direction, size_t tiles){ + std::vector path; + size_t travelled = 0; + while (travelled < tiles){ + const std::optional next = step(grid, from, direction); + if (!next){ + break; + } + travelled += std::abs(next->x - from.x) + std::abs(next->y - from.y); + from = *next; + path.push_back(from); + } + return path; +} + +// Holding toward a wall with people or Pokemon moving beside us never reads +// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking +// scrolls the view far more (35 to 90). True if holding toward (x, y) for +// 1.6 s changes the screen less than walking would: we stand at a wall. +bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ + constexpr double WALKING_CHANGE = 25.0; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + double most = 0; + for (size_t chunk = 0; chunk < 4 && before; chunk++){ + pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (!after){ + return false; + } + most = std::max(most, walk_screen_change(*before.frame, *after.frame)); + before = std::move(after); + } + stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + + (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); + return before && most < WALKING_CHANGE; +} + +// The map all of `tiles` are on, or empty if they are on several or none. +std::string map_of(const StoryWorld& maps, const std::vector& tiles){ + std::string name; + for (const TilePos& pos : tiles){ + const std::optional tile = maps.to_local(pos); + if (!tile || (!name.empty() && tile->map != name)){ + return ""; + } + name = tile->map; + } + return name; +} + +} + + +namespace{ + +// Sorted by row, then column, without repeats. +std::vector sorted_unique(std::vector tiles){ + std::sort(tiles.begin(), tiles.end(), [](const TilePos& a, const TilePos& b){ + return a.y != b.y ? a.y < b.y : a.x < b.x; + }); + tiles.erase(std::unique(tiles.begin(), tiles.end()), tiles.end()); + return tiles; +} + +std::string quoted_list(const std::vector& reads){ + std::string text; + for (const std::string& read : reads){ + text += (text.empty() ? "\"" : ", \"") + read + "\""; + } + return text; +} + +// Long enough to reach the wall from the farthest tile, with room for a +// slow start; past the wall the player stands still. +Milliseconds wall_limit(const TileLeg& leg){ + const Milliseconds per_tile = route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; + return ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; +} + +// Tiles a walk to the wall moved, from the time it walked. +size_t tiles_moved(const WalkResult& walk, Milliseconds per_tile){ + const Milliseconds moving = walk.walked - ROUTE_START_DELAY - + (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); + return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); +} + +// Whether a leg's walk goes on to the next leg or the route plans again. +enum class LegNext{ + NEXT_LEG, + REPLAN, +}; + +// One planned leg as it is walked: where it goes, and the location +// banners read on the way. +struct LegWalk{ + const PlannedLeg& planned; + std::string label; + JoystickPosition stick; + std::string map_before; + std::string map_after; + std::vector banners = {}; + WalkResult result{WalkStop::LIMIT, 0ms, 0}; + // Tiles the first tap of an open leg moved (see open_leg()). + size_t turn_moved = 0; +}; + +bool banner_ok(const LegWalk& walk){ + return std::any_of(walk.banners.begin(), walk.banners.end(), [&](const std::string& read){ + return location_banner_matches(read, walk.map_after); + }); +} + +// Notes the location banners the walk passes, to check map changes. A wild +// battle or a second walk to the wall may follow the banner: any banner +// seen during the leg counts. +bool watch_banner(LegWalk& walk, const ImageViewRGB32& screen){ + if (walk.banners.empty() || (!walk.map_after.empty() && !banner_ok(walk) && walk.banners.size() < 8)){ + const std::string read = read_location_banner(screen); + if (!read.empty() && (walk.banners.empty() || walk.banners.back() != read)){ + walk.banners.push_back(read); + } + } + return false; +} + +// Running on the D-pad, reading the banners on the way. +DpadWalkOptions run_watching(LegWalk& walk){ + return {.arrived = [&walk](const ImageViewRGB32& screen){ return watch_banner(walk, screen); }, .pace = DpadPace::RUN}; +} + +// walk_route(): plans, walks each leg, and plans again from where the walk +// turned out to be when something stopped it. +class RouteWalker{ +public: + RouteWalker( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, RouteTrainers& trainers, const RouteWalkPlan& plan + ) + : m_stream(stream) + , m_context(context) + , m_escape(escape) + , m_maps(maps) + , m_grid(maps) + , m_trainers(trainers) + , m_plan(plan) + {} + + std::vector run(std::vector from); + +private: + std::vector plan_legs() const; + LegNext walk_leg(const PlannedLeg& planned, size_t number, size_t count); + std::vector arrive() const; + + // Legs to a wall. + LegNext wall_leg(LegWalk& walk); + size_t unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest); + LegNext stopped_short_of_wall(LegWalk& walk, size_t walked_tiles); + void check_at_wall(LegWalk& walk); + void on_to_wall(LegWalk& walk); + + // Open legs, tapped. + LegNext open_leg(LegWalk& walk); + void turn_tap(LegWalk& walk); + LegNext blocked_open_leg(LegWalk& walk, size_t moved); + + // After each leg. + void check_banners(LegWalk& walk); + LegNext after_leg(LegWalk& walk); + std::vector watching_trainers(const std::vector>& paths) const; + LegNext unknown_trainer(LegWalk& walk, const std::vector>& paths); + LegNext known_trainer(LegWalk& walk, const std::vector>& paths, const std::vector& watching); + + void talk_to_blocker(const std::string& leg_label); + LegNext replan_from(const LegWalk& walk, std::vector stopped, const std::string& why); + void mark_blocker(const LegWalk& walk, const std::vector& stopped); + + VideoStream& m_stream; + ProControllerContext& m_context; + const WildBattleEscape& m_escape; + const StoryWorld& m_maps; + // Our own copy, so things found in the way can be added to it. + StoryWorld m_grid; + RouteTrainers& m_trainers; + const RouteWalkPlan& m_plan; + std::vector m_from; + // Which way the player faces, once a leg has set it. + std::optional m_facing; + // Every banner read on the way, to check the map we arrive on against. + std::vector m_all_banners; + std::string m_map_start; +}; + +std::vector RouteWalker::run(std::vector from){ + // Tiles to avoid are ground the game lets us walk on (a trainer's line + // of sight), so they are off the map, not walls: a leg must not end + // "at" one, or the walk would run on through it. + for (const TilePos& pos : m_plan.avoid){ + if (std::find(from.begin(), from.end(), pos) == from.end()){ + m_grid.uncover(pos); + } + } + m_from = std::move(from); + m_map_start = map_of(m_maps, m_from); + for (size_t plans = 0; plans < 8; plans++){ + const std::vector legs = plan_legs(); + route_status(m_stream, m_plan.label + ": " + std::to_string(legs.size()) + " legs planned"); + log_plan(m_stream, m_maps, m_plan.label, m_from, legs); + bool replanned = false; + for (size_t number = 1; number <= legs.size() && !replanned; number++){ + replanned = walk_leg(legs[number - 1], number, legs.size()) == LegNext::REPLAN; + } + if (!replanned){ + return arrive(); + } + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + m_plan.label + ": too many plans.", m_stream); +} + +std::vector RouteWalker::plan_legs() const{ + std::optional> legs = plan_robust_route(m_grid, m_from, m_plan.goal, m_plan.costs); + if (!legs){ + // From a few possible tiles the exact-open-leg costs found no + // plan where the default ones did (r240a, three tiles on Route + // 204's row 27); the default plan is longer but sound. + m_stream.log("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + + "; trying the default costs.", COLOR_ORANGE); + legs = plan_robust_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); + } + if (!legs){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + m_plan.label + ": no plan from " + describe(m_maps, m_from) + ".", m_stream); + } + return std::move(*legs); +} + +LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t count){ + route_status(m_stream, route_leg_line(number, count, planned_route_leg(m_maps, planned))); + LegWalk walk{ + .planned = planned, + .label = m_plan.label + ": " + to_string(planned.leg), + .stick = route_stick(planned.leg.direction), + .map_before = map_of(m_maps, m_from), + .map_after = map_of(m_maps, planned.after), + }; + const LegNext next = planned.leg.ends_at_wall ? wall_leg(walk) : open_leg(walk); + if (next == LegNext::REPLAN){ + return next; + } + m_facing = planned.leg.direction; + check_banners(walk); + if (walk.result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": unexpected transition.", m_stream); + } + return after_leg(walk); +} + +// Legs whose ends span two maps skip the per-leg check; the walk as a +// whole must still have seen the banner of the map it ends on. +std::vector RouteWalker::arrive() const{ + const std::string map_end = map_of(m_maps, m_from); + const bool changed_map = !m_map_start.empty() && !map_end.empty() && map_end != m_map_start; + const bool seen = std::any_of(m_all_banners.begin(), m_all_banners.end(), [&](const std::string& read){ + return location_banner_matches(read, map_end); + }); + if (changed_map && !seen){ + const std::string reads = quoted_list(m_all_banners); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + + "'s location banner never showed (read " + (reads.empty() ? "nothing" : reads) + ").", m_stream); + } + m_stream.log(ROUTE_LOG_TAG + route_arrived_line(m_plan.label, route_tiles(m_maps, m_from))); + return m_from; +} + + +// Run (B held) on the D-pad: a leg to a wall ends at the wall however fast +// we get there, and the D-pad keeps to the row. +LegNext RouteWalker::wall_leg(LegWalk& walk){ + const TileLeg& leg = walk.planned.leg; + const Milliseconds limit = wall_limit(leg); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + // A wild battle may end the walk early. Walking to a wall again ends in + // the same place, so finish the leg. + const bool interrupted = walk.result.wild_battles > 0; + for (size_t again = 0; again < 3 && walk.result.wild_battles > 0 && + (walk.result.stop == WalkStop::LIMIT || walk.result.stop == WalkStop::BLOCKED); again++ + ){ + m_stream.log("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again."); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + } + if (interrupted){ + return LegNext::NEXT_LEG; + } + // A wall well before the nearest one the map has: something the map + // does not know is in the way. Work out where we stopped from the time + // walked, put it on our map and plan again from there. + size_t nearest = SIZE_MAX; + for (const TilePos& pos : m_from){ + nearest = std::min(nearest, slide_to_wall(m_grid, pos, leg.direction).tiles); + } + size_t walked_tiles = tiles_moved(walk.result, route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP); + walked_tiles = unstick_wall_leg(walk, walked_tiles, nearest); + // Any stop short of the wall's distance: someone who stays put is + // talked to, and the leg is planned again from where we are. Within the + // time count's slack this may be the wall itself, where A does nothing + // and the new plan walks no further (r239a: a wanderer 3 tiles short + // passed as "about 8 of 10", and every later leg was off by those tiles). + if (walk.result.stop == WalkStop::BLOCKED && walked_tiles < nearest){ + return stopped_short_of_wall(walk, walked_tiles); + } + // A leg to the wall that ran out of time is not at its wall: the open + // leg before it stopped short, and it ran along another row (r127a went + // west along Sandgem's row 13 for row 14, and the next leg north walked + // into the lab). Out of tall grass a stop reads, so walk on: a player + // still moving is not where the plan has him. + if (walk.result.stop == WalkStop::LIMIT && route_wall_readable(m_grid, walk.planned.after)){ + check_at_wall(walk); + } + return LegNext::NEXT_LEG; +} + +// A walk to the wall that stopped well short of it: tap through a corner, +// then wait for someone in the way. Returns the tiles walked by then. +size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest){ + const Milliseconds per_tile = route_ms_per_tile(walk.planned.leg.direction) / ROUTE_RUN_SPEEDUP; + const Milliseconds limit = wall_limit(walk.planned.leg); + // A stick walk can catch a corner when we stand part of a tile off + // centre, where a D-pad tap, which moves whole tiles, gets through: on + // Route 203's stairs the walk west stopped nine times in a row and two + // taps crossed (r225a, 2026-10-31). Tap twice, then walk on to the wall. + if (walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ + const Direction direction = walk.planned.leg.direction; + const DpadPosition dpad = + direction == Direction::NORTH ? DPAD_UP : + direction == Direction::SOUTH ? DPAD_DOWN : + direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); + for (size_t tap = 0; tap < 2; tap++){ + pbf_press_dpad(m_context, dpad, 160ms, 640ms); + } + m_context.wait_for_all_requests(); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + walked_tiles += tiles_moved(walk.result, per_tile); + } + // Someone walking about may stand in the way for a moment: wait and + // walk to the wall again before calling it an obstacle. + for (size_t again = 0; again < 2 && walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++){ + m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(m_context, 2500ms); + m_context.wait_for_all_requests(); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + walked_tiles += tiles_moved(walk.result, per_tile); + } + return walked_tiles; +} + +LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ + // Someone who stays put: talk to them. A trainer we walked up to from + // the side starts the battle this way. + talk_to_blocker(walk.label); + std::vector stopped; + for (const TilePos& pos : m_from){ + const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, SIZE_MAX); + for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ + stopped.push_back(k == 0 ? pos : path[k - 1]); + } + } + stopped = sorted_unique(std::move(stopped)); + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": stopped early, and no tile fits.", m_stream); + } + mark_blocker(walk, stopped); + return replan_from(walk, std::move(stopped), "stopped early after about " + std::to_string(walked_tiles) + " tiles"); +} + +void RouteWalker::check_at_wall(LegWalk& walk){ + const WalkResult check = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, 2000ms, + walk.label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); + if (check.stop == WalkStop::DIALOG){ + // A trainer who saw us froze the walk (the "!") until its time ran + // out, then walked over (r241a: Aroma Lady Taylor's line crosses + // Route 204's corridor). Battle, then walk on to the wall: where + // they saw us from is not reliable (BDSP's trainers stand a few + // tiles from Platinum's, r242a), and a leg to a wall ends at its + // wall however far it got. + route_status(m_stream, walk.label + ": a trainer stopped us on the way; battling."); + m_trainers.fight("a trainer on the way"); + m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); + walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), + walk.label + ": on to the wall", run_watching(walk)); + } + if (check.stop != WalkStop::BLOCKED && + !(check.stop == WalkStop::LIMIT && holding_against_wall(m_stream, m_context, walk.stick.x, walk.stick.y, walk.label + ": at the wall?")) + ){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": ran out of time short of the wall, and walking on gets " + + walk_stop_name(check.stop) + ": not where the plan has us.", m_stream); + } +} + +// After a battle on a leg to a wall: a leg to a wall ends at its wall +// however far it got, so walk on to it and carry on. +void RouteWalker::on_to_wall(LegWalk& walk){ + m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); + walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), + walk.label + ": on to the wall", run_watching(walk)); + m_from = walk.planned.after; + log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, 0); +} + + +// Whole tiles on the D-pad, a tap each: a tap moves exactly one tile, while +// a stick hold timed for three moved one in the Ravaged Path (r235b) and the +// belief drifted four rows. +LegNext RouteWalker::open_leg(LegWalk& walk){ + const TileLeg& leg = walk.planned.leg; + if (m_facing != leg.direction){ + turn_tap(walk); + } + if (leg.tiles > walk.turn_moved){ + walk.result = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - walk.turn_moved, walk.label); + } + // A wild battle starts once the step onto the grass is done, so the + // taps still count; the walk went on after it. + if (walk.result.wild_battles > 0){ + m_stream.log("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + + " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); + } + // Taps that moved nothing: someone stands in the way (the Youngster + // below the Ravaged Path wanders across the row under the cliff, r239a, + // r240a). Two still taps end the walk, so the tiles moved are the taps + // less those two. Wait once for them to move on and tap the rest (r246a + // lost 20 s per block on three waits); someone who stays is talked to, + // put on the map, and the leg planned again from the tile we stand on, + // which the taps give exactly. + size_t moved = std::min(leg.tiles, walk.result.moved_chunks + walk.turn_moved); + if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ + m_stream.log("[Route] " + walk.label + ": blocked after " + std::to_string(moved) + " of " + + std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); + pbf_wait(m_context, 2500ms); + m_context.wait_for_all_requests(); + const WalkResult rest = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - moved, walk.label + ": the rest"); + moved = std::min(leg.tiles, moved + rest.moved_chunks); + walk.result.stop = rest.stop; + walk.result.walked += rest.walked; + walk.result.wild_battles += rest.wild_battles; + } + if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ + return blocked_open_leg(walk, moved); + } + return LegNext::NEXT_LEG; +} + +// The first tap of a leg was meant to turn, but a 160 ms D-pad tap turns +// and steps whichever way the player faced (r248a: all seven did; r246a: +// "south 1" out of the cave went two tiles, onto the Youngster's row), so +// the tap is measured like any other and a step counts for the leg. +void RouteWalker::turn_tap(LegWalk& walk){ + const DpadPosition dpad = walk.stick.x < 0 ? DPAD_LEFT : walk.stick.x > 0 ? DPAD_RIGHT : walk.stick.y > 0 ? DPAD_UP : DPAD_DOWN; + m_context.wait_for_all_requests(); + VideoSnapshot before = m_stream.video().snapshot(); + pbf_press_dpad(m_context, dpad, 160ms, 700ms); + m_context.wait_for_all_requests(); + VideoSnapshot after = m_stream.video().snapshot(); + if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ + walk.turn_moved = 1; + m_stream.log("[Route] " + walk.label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); + } +} + +LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ + talk_to_blocker(walk.label); + std::vector stopped; + for (const TilePos& pos : m_from){ + const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, moved); + stopped.push_back(moved == 0 || path.empty() ? pos : path[std::min(moved, path.size()) - 1]); + } + stopped = sorted_unique(std::move(stopped)); + mark_blocker(walk, stopped); + return replan_from(walk, std::move(stopped), + "blocked after " + std::to_string(moved) + " of " + std::to_string(walk.planned.leg.tiles) + " tiles"); +} + + +// Entering another map shows its name; a missing or wrong name means the +// walk did not end where the plan says. From tiles on several maps (a leg +// not confirmed) the banner may already have shown. +void RouteWalker::check_banners(LegWalk& walk){ + m_context.wait_for_all_requests(); + if (VideoSnapshot screen = m_stream.video().snapshot()){ + watch_banner(walk, *screen.frame); + } + const std::string banner_text = quoted_list(walk.banners); + m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); + if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + + (banner_text.empty() ? "nothing" : banner_text) + ".", m_stream); + } + if (!banner_text.empty()){ + m_stream.log("[Route] " + walk.label + ": location banner " + banner_text + "."); + } + // Where each leg ended, to compare with the plan after a run. + if (VideoSnapshot screen = m_stream.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; + Filesystem::create_directories(Filesystem::Path(folder)); + screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(walk.label) + ".png"); + } +} + +// A trainer who sees us freezes the player and walks over; the dialogue +// may open after the walk has ended. +LegNext RouteWalker::after_leg(LegWalk& walk){ + const TileLeg& leg = walk.planned.leg; + std::vector> paths; + for (const TilePos& pos : m_from){ + paths.push_back(leg_path(m_grid, pos, leg.direction, leg.ends_at_wall ? SIZE_MAX : leg.tiles + 1)); + } + const std::vector watching = watching_trainers(paths); + bool talking = walk.result.stop == WalkStop::DIALOG; + if (!talking){ + ShortDialogWatcher dialog(COLOR_YELLOW); + const Milliseconds wait = watching.empty() ? 500ms : 4000ms; + talking = wait_until(m_stream, m_context, wait, {{dialog}}) == 0; + } + if (!talking){ + m_from = walk.planned.after; + log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, walk.result.wild_battles); + return LegNext::NEXT_LEG; + } + if (watching.empty()){ + return unknown_trainer(walk, paths); + } + return known_trainer(walk, paths, watching); +} + +// The trainers whose sight one of the leg's possible paths enters. +std::vector RouteWalker::watching_trainers(const std::vector>& paths) const{ + std::vector watching; + for (size_t t = 0; t < m_trainers.sights.size(); t++){ + const std::vector sight = sight_tiles(m_grid, m_trainers.sights[t]); + const bool seen = std::any_of(paths.begin(), paths.end(), [&](const std::vector& path){ + return std::any_of(path.begin(), path.end(), [&](const TilePos& pos){ + return std::find(sight.begin(), sight.end(), pos) != sight.end(); + }); + }); + if (seen){ + watching.push_back(t); + } + } + return watching; +} + +// A trainer the map does not have. Fight, then plan again from anywhere +// the leg could have stopped. +LegNext RouteWalker::unknown_trainer(LegWalk& walk, const std::vector>& paths){ + std::vector anywhere = m_from; + for (const std::vector& path : paths){ + anywhere.insert(anywhere.end(), path.begin(), path.end()); + } + anywhere = sorted_unique(std::move(anywhere)); + route_status(m_stream, m_plan.label + ": a trainer the map lacks stopped us; fighting."); + m_trainers.fight("unknown trainer"); + if (walk.planned.leg.ends_at_wall){ + on_to_wall(walk); + return LegNext::NEXT_LEG; + } + m_from = std::move(anywhere); + return LegNext::REPLAN; +} + +LegNext RouteWalker::known_trainer(LegWalk& walk, const std::vector>& paths, const std::vector& watching){ + // Each possible start tile was stopped on the first tile of its path + // inside a watching trainer's sight. + const TrainerSight trainer = m_trainers.sights[watching[0]]; + std::vector stopped; + for (const std::vector& path : paths){ + const auto first_seen = std::find_if(path.begin(), path.end(), [&](const TilePos& pos){ + return std::any_of(watching.begin(), watching.end(), [&](size_t t){ + const std::vector sight = sight_tiles(m_grid, m_trainers.sights[t]); + return std::find(sight.begin(), sight.end(), pos) != sight.end(); + }); + }); + if (first_seen != path.end()){ + stopped.push_back(*first_seen); + } + } + if (stopped.empty()){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": " + trainer.name + " talked, but no path enters their sight.", m_stream); + } + m_stream.log(ROUTE_LOG_TAG + route_trainer_line(m_plan.label, trainer.name, route_tiles(m_maps, stopped))); + route_status(m_stream, "Fighting " + trainer.name + "."); + m_trainers.fight(trainer.name); + m_trainers.sights.erase(m_trainers.sights.begin() + watching[0]); + // Their sight line only says roughly where: BDSP's trainers stand a few + // tiles from Platinum's (r242a). A leg to a wall walks on to its wall. + if (walk.planned.leg.ends_at_wall){ + on_to_wall(walk); + return LegNext::NEXT_LEG; + } + // An open leg counts only the taps that moved: the trainer freezes the + // player on the tile they saw, and the taps pressed while they walk + // over move nobody (r248a: "4 taps" were 2 steps; the belief of two + // tiles then cost a 12-tile run to a wall and 5 back). + const size_t taps = std::min(walk.planned.leg.tiles, walk.result.moved_chunks + walk.turn_moved); + std::vector by_taps; + for (const TilePos& pos : m_from){ + const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, walk.planned.leg.tiles); + const size_t k = std::min(taps, path.size()); + by_taps.push_back(k == 0 ? pos : path[k - 1]); + } + return replan_from(walk, sorted_unique(std::move(by_taps)), std::to_string(taps) + " taps before the battle"); +} + + +// Presses A at whoever stopped the walk; a trainer we walked up to from +// the side starts the battle this way. +void RouteWalker::talk_to_blocker(const std::string& leg_label){ + pbf_press_button(m_context, BUTTON_A, 80ms, 200ms); + ShortDialogWatcher dialog(COLOR_YELLOW); + if (wait_until(m_stream, m_context, 2000ms, {{dialog}}) == 0){ + route_status(m_stream, leg_label + ": someone is in the way; talking to them."); + m_trainers.fight("someone in the way"); + } +} + +// One tile we may stand on: what stopped us is on the next tile, and goes +// on our map. +void RouteWalker::mark_blocker(const LegWalk& walk, const std::vector& stopped){ + if (stopped.size() != 1){ + return; + } + const TilePos blocker = stopped[0].next(walk.planned.leg.direction); + m_grid.block(blocker); + m_stream.log("[Route] " + walk.label + ": something the map lacks is at " + + describe(m_maps, blocker) + "; marked it.", COLOR_ORANGE); +} + +LegNext RouteWalker::replan_from(const LegWalk& walk, std::vector stopped, const std::string& why){ + m_stream.log(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.planned.leg), why, route_tiles(m_maps, stopped)), COLOR_ORANGE); + m_facing = walk.planned.leg.direction; + m_from = std::move(stopped); + return LegNext::REPLAN; +} + +} + + +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +){ + return RouteWalker(stream, context, escape, maps, trainers, plan).run(std::move(from)); +} + +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const WaypointWalkPlan& plan +){ + for (size_t i = 0; i < plan.waypoints.size(); i++){ + const TilePos goal = plan.waypoints[i]; + from = walk_route(stream, context, escape, maps, from, trainers, { + .goal = [goal](const TilePos& pos){ return pos == goal; }, + .label = plan.label + " (" + std::to_string(i + 1) + "/" + std::to_string(plan.waypoints.size()) + + ", to " + describe(maps, goal) + ")", + .avoid = {}, + .costs = plan.costs, + }); + } + return from; +} + +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk +){ + for (Direction side : talk.sides){ + const TilePos stand = talk.npc.next(side); + if (!standable(maps, stand)){ + continue; + } + at = walk_route(stream, context, escape, maps, at, trainers, { + .goal = [stand](const TilePos& pos){ return pos == stand; }, + .label = talk.label, + .avoid = {}, + .costs = route_plan_costs(), + }); + // Face the NPC: the opposite of the side we stand on. + const Direction face = + side == Direction::NORTH ? Direction::SOUTH : + side == Direction::SOUTH ? Direction::NORTH : + side == Direction::EAST ? Direction::WEST : Direction::EAST; + route_turn(stream, context, face); + pbf_press_button(context, BUTTON_A, 80ms, 200ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ + route_status(stream, talk.label + ": talking from " + describe(maps, stand) + "."); + return; + } + route_status(stream, talk.label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue.", stream); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h index e6d38f1b5f..845b8977d7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -1,107 +1,107 @@ -/* BDSP AutoStory - Planned Route - * - * From: https://github.com/PokemonAutomation/ - * - * Walks a plan made on the story maps: legs to a wall are held until the - * player stops, open legs are tapped a tile at a time. Tracks every tile - * the player may be on, and plans again after something stops the walk. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_H -#define PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_H - -#include -#include -#include -#include "CommonFramework/Tools/VideoStream.h" -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" -#include "PokemonBDSP_AutoStory_Walk.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Called when a trainer's dialogue has opened, or when someone standing in -// the way was talked to. Fights the battle if one starts, and returns to the -// overworld either way. -using RouteTrainerFight = std::function; - -// The trainers a route walk may meet, and how to fight them. A trainer who -// is fought is taken off `sights`. -struct RouteTrainers{ - std::vector sights; - RouteTrainerFight fight; -}; - -// Where walk_route() goes: until every tile the player may be on is -// `goal`. Tiles in `avoid` (where someone wanders, a trainer's sight) are -// planned as off the map. -struct RouteWalkPlan{ - std::function goal; - std::string label; - std::vector avoid = {}; - RobustPlanCosts costs = route_plan_costs(); -}; - -// Walks from any of the world tiles `from` of `maps` until every tile the -// player may be on is `plan.goal`, and returns those tiles. Trainers who -// see the player are fought and taken off `trainers`. Fails with a -// screenshot on a transition, a dialogue no trainer explains, or when no -// plan exists. A walk from one map to another must see the second map's -// location banner on the way: a walk whose legs all ended on walls can -// still end in the wrong place (r124a saved in Sandgem Town after -// "arriving" on Route 202). -std::vector walk_route( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, std::vector from, - RouteTrainers& trainers, const RouteWalkPlan& plan -); - -// The corners of a line a human walked, and the costs to plan each -// stretch with. -struct WaypointWalkPlan{ - std::vector waypoints; - std::string label; - RobustPlanCosts costs = waypoint_plan_costs(); -}; - -// Walks `from` to each of `plan.waypoints` in turn, with walk_route() for -// every stretch, and returns where the last one ended: the plan is the -// line, with the walker's handling of trainers and blocks on each stretch -// (Route 204 North, 2026-10-31). -std::vector walk_waypoints( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, std::vector from, - RouteTrainers& trainers, const WaypointWalkPlan& plan -); - -// Whom talk_to_npc() talks to: the NPC on world tile `npc`, from the sides -// in `sides` (the side of the NPC to stand on), in order. -struct NpcTalk{ - Pokemon::TilePos npc; - std::vector sides; - std::string label; -}; - -// Walks next to `talk.npc`, faces them and presses A, trying each side -// until a dialogue opens. `at` is where the player may be, and is updated. -// Fails with a screenshot if no side starts a dialogue. A press from a -// diagonal tile, or from a tile a row off, talks to nobody, so each side is -// tried in turn. -void talk_to_npc( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, std::vector& at, - RouteTrainers& trainers, const NpcTalk& talk -); - - - -} -} -} -#endif +/* BDSP AutoStory - Planned Route + * + * From: https://github.com/PokemonAutomation/ + * + * Walks a plan made on the story maps: legs to a wall are held until the + * player stops, open legs are tapped a tile at a time. Tracks every tile + * the player may be on, and plans again after something stops the walk. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_H +#define PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_H + +#include +#include +#include +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Called when a trainer's dialogue has opened, or when someone standing in +// the way was talked to. Fights the battle if one starts, and returns to the +// overworld either way. +using RouteTrainerFight = std::function; + +// The trainers a route walk may meet, and how to fight them. A trainer who +// is fought is taken off `sights`. +struct RouteTrainers{ + std::vector sights; + RouteTrainerFight fight; +}; + +// Where walk_route() goes: until every tile the player may be on is +// `goal`. Tiles in `avoid` (where someone wanders, a trainer's sight) are +// planned as off the map. +struct RouteWalkPlan{ + std::function goal; + std::string label; + std::vector avoid = {}; + RobustPlanCosts costs = route_plan_costs(); +}; + +// Walks from any of the world tiles `from` of `maps` until every tile the +// player may be on is `plan.goal`, and returns those tiles. Trainers who +// see the player are fought and taken off `trainers`. Fails with a +// screenshot on a transition, a dialogue no trainer explains, or when no +// plan exists. A walk from one map to another must see the second map's +// location banner on the way: a walk whose legs all ended on walls can +// still end in the wrong place (r124a saved in Sandgem Town after +// "arriving" on Route 202). +std::vector walk_route( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +); + +// The corners of a line a human walked, and the costs to plan each +// stretch with. +struct WaypointWalkPlan{ + std::vector waypoints; + std::string label; + RobustPlanCosts costs = waypoint_plan_costs(); +}; + +// Walks `from` to each of `plan.waypoints` in turn, with walk_route() for +// every stretch, and returns where the last one ended: the plan is the +// line, with the walker's handling of trainers and blocks on each stretch +// (Route 204 North, 2026-10-31). +std::vector walk_waypoints( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const WaypointWalkPlan& plan +); + +// Whom talk_to_npc() talks to: the NPC on world tile `npc`, from the sides +// in `sides` (the side of the NPC to stand on), in order. +struct NpcTalk{ + Pokemon::TilePos npc; + std::vector sides; + std::string label; +}; + +// Walks next to `talk.npc`, faces them and presses A, trying each side +// until a dialogue opens. `at` is where the player may be, and is updated. +// Fails with a screenshot if no side starts a dialogue. A press from a +// diagonal tile, or from a tile a row off, talks to nobody, so each side is +// tried in turn. +void talk_to_npc( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index d7d6367e9c..803019b456 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -1,294 +1,294 @@ -/* BDSP AutoStory - Route Walk - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include "Common/Cpp/PrettyPrint.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/ImageTools/ImageStats.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "Pokemon/Navigation/Pokemon_RouteLog.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" -#include "PokemonBDSP_AutoStory_Location.h" -#include "PokemonBDSP_AutoStory_Walk.h" -#include "PokemonBDSP_AutoStory_Indoors.h" -#include "PokemonBDSP_AutoStory_OverlayLog.h" -#include "PokemonBDSP_AutoStory_RouteWalk.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; -using namespace Pokemon; - - -namespace{ - -bool across(Direction direction){ - return direction == Direction::EAST || direction == Direction::WEST; -} - -} - -Milliseconds route_ms_per_tile(Direction direction){ - return across(direction) ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN; -} - -JoystickPosition route_stick(Direction direction){ - switch (direction){ - case Direction::NORTH: - return {0, 1}; - case Direction::EAST: - return {1, 0}; - case Direction::SOUTH: - return {0, -1}; - case Direction::WEST: - return {-1, 0}; - } - return {0, 0}; -} - - -bool route_wall_readable(const StoryWorld& grid, const std::vector& after){ - return std::none_of(after.begin(), after.end(), [&](const TilePos& pos){ - return grid.tile(pos) == TileKind::TALL_GRASS; - }); -} - -void route_status(VideoStream& stream, const std::string& text){ - stream.log("[Route] " + text, COLOR_BLUE); - add_wrapped_log(stream.overlay(), text, COLOR_WHITE); -} - -Milliseconds route_open_hold(Direction direction, size_t tiles){ - const Milliseconds step = route_ms_per_tile(direction); - return step * tiles - step / 2; -} - -void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ - stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); - pbf_move_left_joystick(context, route_stick(direction), ROUTE_TURN_TAP, 300ms); - context.wait_for_all_requests(); -} - -std::optional talk_to_adjacent_npc( - VideoStream& stream, ProControllerContext& context, - const std::vector& directions, - const std::string& label, Milliseconds wait -){ - for (Direction direction : directions){ - route_turn(stream, context, direction); - pbf_press_button(context, BUTTON_A, 80ms, 0ms); - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream, context, wait, {{dialog}}) == 0){ - stream.log("[Route] " + label + ": answered from the " + direction_name(direction) + "."); - return direction; - } - } - stream.log("[Route] " + label + ": nobody answered next to us."); - return std::nullopt; -} - -bool talk_with_nudges( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - Direction facing, const std::string& label, Milliseconds wait, - const std::function& accept -){ - const bool vertical = facing == Direction::NORTH || facing == Direction::SOUTH; - const Direction minus = vertical ? Direction::WEST : Direction::NORTH; - const Direction plus = vertical ? Direction::EAST : Direction::SOUTH; - int at = 0; - for (int offset : {0, -1, 1, -2, 2}){ - const Direction side = offset < at ? minus : plus; - for (; at != offset; at += offset < at ? -1 : 1){ - pbf_move_left_joystick(context, route_stick(side), ROUTE_NUDGE_TAP, 300ms); - } - if (offset != 0){ - route_turn(stream, context, facing); - // A tap in a cave or in grass can start a wild battle, after - // which we are not where the nudges think: let the caller line up again. - if (run_from_late_encounter(stream, context, escape, label + ": nudge")){ - return false; - } - } - context.wait_for_all_requests(); - const VideoSnapshot before = stream.video().snapshot(); - pbf_press_button(context, BUTTON_A, 80ms, 0ms); - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream, context, wait, {{dialog}}) == 0){ - const VideoSnapshot after = stream.video().snapshot(); - if (!accept || (before && after && accept(*before.frame, *after.frame))){ - stream.log("[Route] " + label + ": answered " + std::to_string(offset) + " nudge(s) from where we stood."); - return true; - } - stream.log("[Route] " + label + ": something else answered " + std::to_string(offset) + " nudge(s) from where we stood; closing it."); - for (size_t press = 0; press < 3; press++){ - pbf_press_button(context, BUTTON_B, 80ms, 700ms); - } - context.wait_for_all_requests(); - } - } - stream.log("[Route] " + label + ": nobody answered within two nudges either side."); - return false; -} - - -void finish_dialogue( - VideoStream& stream, ProControllerContext& context, - Button button, const std::string& label, Milliseconds timeout -){ - // No line counts: talks differ in length (r115a: 8 A left the Poke Mart - // lady's Potions talk open). The box blinks off between lines and during - // "obtained" jingles, so only a stable absence ends the talk. - const WallClock none = WallClock::max(); - ShortDialogDetector dialog; - OverworldDetector overworld; - const WallClock deadline = current_time() + timeout; - WallClock quiet_since = none; - WallClock unknown_since = none; - while (current_time() < deadline){ - context.wait_for_all_requests(); - const auto frame = stream.video().snapshot(); - const WallClock now = current_time(); - const bool talking = frame && dialog.detect(frame); - if (frame && !talking && overworld.detect(frame)){ - unknown_since = none; - if (quiet_since == none){ - quiet_since = now; - }else if (now - quiet_since >= 1500ms){ - return; - } - context.wait_for(200ms); - continue; - } - quiet_since = none; - // Press only on a box (an A on the overworld talks again), or on a - // screen that is neither for 3 s. - if (talking || (unknown_since != none && now - unknown_since >= 3000ms)){ - unknown_since = none; - pbf_press_button(context, button, 80ms, 1200ms); - continue; - } - if (unknown_since == none){ - unknown_since = now; - } - context.wait_for(200ms); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": the dialogue did not end within " + - std::to_string(std::chrono::duration_cast(timeout).count()) + " s.", stream); -} - -void enter_door_north(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ - // Sideways steps to try after a blocked walk: east, then back west and - // one more. - const Direction steps[] = {Direction::EAST, Direction::WEST, Direction::WEST}; - for (size_t attempt = 0; attempt <= 3; attempt++){ - const WalkResult walk = walk_until(stream, context, escape, {0, 1}, 2500ms, label); - if (walk.stop == WalkStop::TRANSITION){ - context.wait_for(1500ms); - return; - } - if (attempt == 3){ - break; - } - route_status(stream, label + ": the door frame stopped us; one tile " + - (steps[attempt] == Direction::EAST ? "east." : "west.")); - route_turn(stream, context, steps[attempt]); - const double x = steps[attempt] == Direction::EAST ? 1 : -1; - pbf_move_left_joystick(context, {x, 0}, route_open_hold(steps[attempt], 1), 200ms); - context.wait_for_all_requests(); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": could not go through the door.", stream); -} - -bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room){ - if (brightness < DOOR_FADE_DARK){ - watch.saw_dark = true; - } - return watch.saw_dark && !room && brightness >= DOOR_OUTSIDE_BRIGHT; -} - -bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room){ - // Both are asked: the fade watch keeps what it has seen. - const bool faded = door_fade_look(watch, brightness, room); - return faded || !room; -} - -namespace{ - -// Three looks half a second apart, none a room: outside, whatever the fade -// did. A look of a lit Center can miss its black edge (r259 at 800 ms), -// three in a row standing still do not. -bool outside_for_three_looks(VideoStream& stream, ProControllerContext& context, const std::string& label){ - for (size_t look = 0; look < 3; look++){ - context.wait_for_all_requests(); - VideoSnapshot now = stream.video().snapshot(); - const bool room = !now || indoors(*now.frame); - stream.log("[Route] " + label + ": standing look " + std::to_string(look + 1) + ": " + (room ? "a room" : "not a room") + "."); - if (room){ - return false; - } - pbf_wait(context, 500ms); - } - return true; -} - -} - -void leave_door_south(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ - // A last line of talk may still be up, and it would stop the walk. - pbf_mash_button(context, BUTTON_B, 2000ms); - context.wait_for_all_requests(); - // Stop the walk at the fade or at the first look that is not a room: a - // stick held on walks on outside (r124a ten rows south of the Mart's - // door, r257 into a house, r261 three rows). Standing looks then confirm. - DoorFadeWatch watch; - auto out_of_door = [&](const ImageViewRGB32& screen){ - const double brightness = image_stats(screen).average.sum(); - const bool room = indoors(screen); - const bool out = door_walk_stops(watch, brightness, room); - stream.log(std::format("[Route] {}: look: brightness {:.0f}, {}, fade {}.", label, brightness, - room ? "a room" : "not a room", watch.saw_dark ? "seen" : "not seen")); - return out; - }; - bool out = false; - for (size_t tries = 0; tries < 5 && !out; tries++){ - const WalkResult walk = walk_until(stream, context, escape, {0, -1}, 6000ms, label, out_of_door); - if (walk.stop == WalkStop::DIALOG){ - pbf_mash_button(context, BUTTON_B, 2000ms); - } - // Let the fade-in finish, then look again standing still. - pbf_wait(context, 1500ms); - out = walk.stop == WalkStop::TRANSITION || outside_for_three_looks(stream, context, label); - if (!out){ - stream.log("[Route] " + label + ": still indoors; walking on."); - } - } - if (!out){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": did not get back out.", stream); - } - // Let the fade-in finish. - pbf_wait(context, 1500ms); -} - - - -} -} -} +/* BDSP AutoStory - Route Walk + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/PrettyPrint.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "Pokemon/Navigation/Pokemon_RouteLog.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_Indoors.h" +#include "PokemonBDSP_AutoStory_OverlayLog.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +bool across(Direction direction){ + return direction == Direction::EAST || direction == Direction::WEST; +} + +} + +Milliseconds route_ms_per_tile(Direction direction){ + return across(direction) ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN; +} + +JoystickPosition route_stick(Direction direction){ + switch (direction){ + case Direction::NORTH: + return {0, 1}; + case Direction::EAST: + return {1, 0}; + case Direction::SOUTH: + return {0, -1}; + case Direction::WEST: + return {-1, 0}; + } + return {0, 0}; +} + + +bool route_wall_readable(const StoryWorld& grid, const std::vector& after){ + return std::none_of(after.begin(), after.end(), [&](const TilePos& pos){ + return grid.tile(pos) == TileKind::TALL_GRASS; + }); +} + +void route_status(VideoStream& stream, const std::string& text){ + stream.log("[Route] " + text, COLOR_BLUE); + add_wrapped_log(stream.overlay(), text, COLOR_WHITE); +} + +Milliseconds route_open_hold(Direction direction, size_t tiles){ + const Milliseconds step = route_ms_per_tile(direction); + return step * tiles - step / 2; +} + +void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ + stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); + pbf_move_left_joystick(context, route_stick(direction), ROUTE_TURN_TAP, 300ms); + context.wait_for_all_requests(); +} + +std::optional talk_to_adjacent_npc( + VideoStream& stream, ProControllerContext& context, + const std::vector& directions, + const std::string& label, Milliseconds wait +){ + for (Direction direction : directions){ + route_turn(stream, context, direction); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, wait, {{dialog}}) == 0){ + stream.log("[Route] " + label + ": answered from the " + direction_name(direction) + "."); + return direction; + } + } + stream.log("[Route] " + label + ": nobody answered next to us."); + return std::nullopt; +} + +bool talk_with_nudges( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + Direction facing, const std::string& label, Milliseconds wait, + const std::function& accept +){ + const bool vertical = facing == Direction::NORTH || facing == Direction::SOUTH; + const Direction minus = vertical ? Direction::WEST : Direction::NORTH; + const Direction plus = vertical ? Direction::EAST : Direction::SOUTH; + int at = 0; + for (int offset : {0, -1, 1, -2, 2}){ + const Direction side = offset < at ? minus : plus; + for (; at != offset; at += offset < at ? -1 : 1){ + pbf_move_left_joystick(context, route_stick(side), ROUTE_NUDGE_TAP, 300ms); + } + if (offset != 0){ + route_turn(stream, context, facing); + // A tap in a cave or in grass can start a wild battle, after + // which we are not where the nudges think: let the caller line up again. + if (run_from_late_encounter(stream, context, escape, label + ": nudge")){ + return false; + } + } + context.wait_for_all_requests(); + const VideoSnapshot before = stream.video().snapshot(); + pbf_press_button(context, BUTTON_A, 80ms, 0ms); + context.wait_for_all_requests(); + ShortDialogWatcher dialog(COLOR_CYAN); + if (wait_until(stream, context, wait, {{dialog}}) == 0){ + const VideoSnapshot after = stream.video().snapshot(); + if (!accept || (before && after && accept(*before.frame, *after.frame))){ + stream.log("[Route] " + label + ": answered " + std::to_string(offset) + " nudge(s) from where we stood."); + return true; + } + stream.log("[Route] " + label + ": something else answered " + std::to_string(offset) + " nudge(s) from where we stood; closing it."); + for (size_t press = 0; press < 3; press++){ + pbf_press_button(context, BUTTON_B, 80ms, 700ms); + } + context.wait_for_all_requests(); + } + } + stream.log("[Route] " + label + ": nobody answered within two nudges either side."); + return false; +} + + +void finish_dialogue( + VideoStream& stream, ProControllerContext& context, + Button button, const std::string& label, Milliseconds timeout +){ + // No line counts: talks differ in length (r115a: 8 A left the Poke Mart + // lady's Potions talk open). The box blinks off between lines and during + // "obtained" jingles, so only a stable absence ends the talk. + const WallClock none = WallClock::max(); + ShortDialogDetector dialog; + OverworldDetector overworld; + const WallClock deadline = current_time() + timeout; + WallClock quiet_since = none; + WallClock unknown_since = none; + while (current_time() < deadline){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const WallClock now = current_time(); + const bool talking = frame && dialog.detect(frame); + if (frame && !talking && overworld.detect(frame)){ + unknown_since = none; + if (quiet_since == none){ + quiet_since = now; + }else if (now - quiet_since >= 1500ms){ + return; + } + context.wait_for(200ms); + continue; + } + quiet_since = none; + // Press only on a box (an A on the overworld talks again), or on a + // screen that is neither for 3 s. + if (talking || (unknown_since != none && now - unknown_since >= 3000ms)){ + unknown_since = none; + pbf_press_button(context, button, 80ms, 1200ms); + continue; + } + if (unknown_since == none){ + unknown_since = now; + } + context.wait_for(200ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": the dialogue did not end within " + + std::to_string(std::chrono::duration_cast(timeout).count()) + " s.", stream); +} + +void enter_door_north(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ + // Sideways steps to try after a blocked walk: east, then back west and + // one more. + const Direction steps[] = {Direction::EAST, Direction::WEST, Direction::WEST}; + for (size_t attempt = 0; attempt <= 3; attempt++){ + const WalkResult walk = walk_until(stream, context, escape, {0, 1}, 2500ms, label); + if (walk.stop == WalkStop::TRANSITION){ + context.wait_for(1500ms); + return; + } + if (attempt == 3){ + break; + } + route_status(stream, label + ": the door frame stopped us; one tile " + + (steps[attempt] == Direction::EAST ? "east." : "west.")); + route_turn(stream, context, steps[attempt]); + const double x = steps[attempt] == Direction::EAST ? 1 : -1; + pbf_move_left_joystick(context, {x, 0}, route_open_hold(steps[attempt], 1), 200ms); + context.wait_for_all_requests(); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": could not go through the door.", stream); +} + +bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room){ + if (brightness < DOOR_FADE_DARK){ + watch.saw_dark = true; + } + return watch.saw_dark && !room && brightness >= DOOR_OUTSIDE_BRIGHT; +} + +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room){ + // Both are asked: the fade watch keeps what it has seen. + const bool faded = door_fade_look(watch, brightness, room); + return faded || !room; +} + +namespace{ + +// Three looks half a second apart, none a room: outside, whatever the fade +// did. A look of a lit Center can miss its black edge (r259 at 800 ms), +// three in a row standing still do not. +bool outside_for_three_looks(VideoStream& stream, ProControllerContext& context, const std::string& label){ + for (size_t look = 0; look < 3; look++){ + context.wait_for_all_requests(); + VideoSnapshot now = stream.video().snapshot(); + const bool room = !now || indoors(*now.frame); + stream.log("[Route] " + label + ": standing look " + std::to_string(look + 1) + ": " + (room ? "a room" : "not a room") + "."); + if (room){ + return false; + } + pbf_wait(context, 500ms); + } + return true; +} + +} + +void leave_door_south(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ + // A last line of talk may still be up, and it would stop the walk. + pbf_mash_button(context, BUTTON_B, 2000ms); + context.wait_for_all_requests(); + // Stop the walk at the fade or at the first look that is not a room: a + // stick held on walks on outside (r124a ten rows south of the Mart's + // door, r257 into a house, r261 three rows). Standing looks then confirm. + DoorFadeWatch watch; + auto out_of_door = [&](const ImageViewRGB32& screen){ + const double brightness = image_stats(screen).average.sum(); + const bool room = indoors(screen); + const bool out = door_walk_stops(watch, brightness, room); + stream.log(std::format("[Route] {}: look: brightness {:.0f}, {}, fade {}.", label, brightness, + room ? "a room" : "not a room", watch.saw_dark ? "seen" : "not seen")); + return out; + }; + bool out = false; + for (size_t tries = 0; tries < 5 && !out; tries++){ + const WalkResult walk = walk_until(stream, context, escape, {0, -1}, 6000ms, label, out_of_door); + if (walk.stop == WalkStop::DIALOG){ + pbf_mash_button(context, BUTTON_B, 2000ms); + } + // Let the fade-in finish, then look again standing still. + pbf_wait(context, 1500ms); + out = walk.stop == WalkStop::TRANSITION || outside_for_three_looks(stream, context, label); + if (!out){ + stream.log("[Route] " + label + ": still indoors; walking on."); + } + } + if (!out){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": did not get back out.", stream); + } + // Let the fade-in finish. + pbf_wait(context, 1500ms); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index 6654a93379..97bda06d6e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -1,150 +1,150 @@ -/* BDSP AutoStory - Route Walk - * - * From: https://github.com/PokemonAutomation/ - * - * What walking on the story maps is made of: walking times per tile, the - * stick for a direction, turning, talking to someone next to us, finishing - * a dialogue, and going through doors. PlannedRoute walks whole plans. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H -#define PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H - -#include -#include -#include -#include -#include "Common/Cpp/Time.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" -#include "CommonFramework/Tools/VideoStream.h" -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" -#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" -#include "PokemonBDSP_AutoStory_Walk.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Walking time per tile, measured on the console from the post-catch save: -// a row is deeper than a column is wide under the tilted camera. -constexpr Milliseconds ROUTE_MS_PER_TILE_ACROSS = Milliseconds(160); // east or west -constexpr Milliseconds ROUTE_MS_PER_TILE_UP_DOWN = Milliseconds(240); // north or south -// From pressing the stick to the first step, when standing still. -constexpr Milliseconds ROUTE_START_DELAY = Milliseconds(250); -// Running (B held) covers a tile in this fraction of the walking time. B -// held with the D-pad did not run: r238a walked the Ravaged Path's corridor -// at 160 ms a tile, and a limit set for twice that speed cut a wall leg -// short by two tiles. 1 until running is measured to work. -constexpr int ROUTE_RUN_SPEEDUP = 1; - -// Walking time per tile in `direction`. -Milliseconds route_ms_per_tile(Pokemon::Direction direction); -// The left stick for a direction. Row 0 is the top of a map, and the -// stick's y is positive upward. -JoystickPosition route_stick(Pokemon::Direction direction); - -// How long to hold the stick for an open leg of `tiles` tiles, for a player -// already facing that way: a step once started is always finished, so stop -// half a tile early. On the console a 1290ms hold west went 9 tiles and an -// 800ms hold south 4: the player starts at once and finishes the step. -Milliseconds route_open_hold(Pokemon::Direction direction, size_t tiles); - -// A tap this short turns a player who faces another way, without a step: on -// the console a 150ms tap south after walking north only turned the player. -constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(100); - -// Turns the player to face `direction` without moving. Only for a player who -// faces another way: one already facing it may take a step. -void route_turn(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction); - -// Talks to someone on a tile next to ours, without trusting the tile we -// counted ourselves onto: for each of `directions` in order, turns that way, -// presses A and waits up to `wait` for a dialogue. Returns the direction that -// answered, its dialogue open, or none if nobody did. Turns are taps (see -// route_turn()), so do not list the way the player already faces first. -std::optional talk_to_adjacent_npc( - VideoStream& stream, ProControllerContext& context, - const std::vector& directions, - const std::string& label, Milliseconds wait = Milliseconds(1500) -); - -// Talks to someone straight ahead in `facing`, where we may stand a little -// off their tile: BDSP moves by any distance, and a sideways tap moves about -// a third of a tile (a 50ms tap 0.2 to 0.45 tiles, a 100ms turn tap 0.4, on -// the console 2026-10-31). Presses A, and while nobody answers shifts by one -// such tap at a time to -1, +1, -2, +2 taps from the start and tries again. -// Never turns away and back between tries: each turn is a tap that moves us -// (s03e2 drifted east of Roark a third of a tile per try). Returns whether a -// dialogue opened, left open; false also after a wild battle on a nudge, -// which leaves us off the place we were. -// `accept`, if given, looks at the screen before A and once the dialogue -// is open, and says whether it is the one wanted: a rock or a sign next to -// someone answers A too (r187a read "It's a rugged rock" beside Roark). A -// dialogue it turns down is closed with B and the nudges go on. -bool talk_with_nudges( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - Pokemon::Direction facing, const std::string& label, - Milliseconds wait = Milliseconds(3000), - const std::function& accept = nullptr -); -constexpr Milliseconds ROUTE_NUDGE_TAP = Milliseconds(50); - -// Whether a walk to the wall that ends on any of `after` can tell it has -// stopped: not in tall grass, whose sway keeps the screen changing at a -// wall. Elsewhere, a walk to the wall that stops confirms where it ended. -bool route_wall_readable(const StoryWorld& grid, const std::vector& after); - -// Shows what the route walk is doing in the log and the video overlay. -void route_status(VideoStream& stream, const std::string& text); - -// Finishes a dialogue that has opened, however many lines it has: presses -// `button` while a dialogue box shows, and returns once the overworld has -// shown with no box for 1.5 s (the box blinks off between lines and during -// "obtained" jingles). Use A where the talk asks Yes/No or gives an item (A -// answers Yes and closes "obtained" lines; B would answer No), B elsewhere -// (B never starts a new talk). Fails with a screenshot after `timeout`. -void finish_dialogue( - VideoStream& stream, ProControllerContext& context, - Button button, const std::string& label, - Milliseconds timeout = std::chrono::seconds(40) -); - -// For a player on the tile below a door: walks north through it. If a wall -// stops the walk (the door frame, a tile off the opening), steps a tile east, -// then west, and tries again. Fails with a screenshot if no step goes in. -void enter_door_north(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); - -// For a player inside a building whose door is straight south: closes any -// talk still up and walks south out of it, stopping at the door's fade or -// the first look that is not a room, so it ends a tile or so outside. Out is -// then three standing looks in a row that are not a room: one look of a lit -// Center can miss its black edge (r259). Fails with a screenshot if five -// walks south get nowhere. -void leave_door_south(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); - -// Brightness (mean r + g + b) below which a look is the door's fade, and -// above which a look that is not a room is outside. -inline constexpr double DOOR_FADE_DARK = 120.0; -inline constexpr double DOOR_OUTSIDE_BRIGHT = 200.0; - -// What leave_door_south() has seen of the door's fade so far. -struct DoorFadeWatch{ - bool saw_dark = false; -}; -// Takes one look (its brightness, and whether indoors() read a room) and -// returns whether we are out: a dark look seen, and now a bright one that -// is not a room. -bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room); -// Whether leave_door_south()'s walk stops at this look: at the door's fade -// out (door_fade_look()), or at the first look that is not a room, so a -// held stick does not walk on outside (r261 three rows). -bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room); - - - -} -} -} -#endif +/* BDSP AutoStory - Route Walk + * + * From: https://github.com/PokemonAutomation/ + * + * What walking on the story maps is made of: walking times per tile, the + * stick for a direction, turning, talking to someone next to us, finishing + * a dialogue, and going through doors. PlannedRoute walks whole plans. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H +#define PokemonAutomation_PokemonBDSP_AutoStory_RouteWalk_H + +#include +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Walking time per tile, measured on the console from the post-catch save: +// a row is deeper than a column is wide under the tilted camera. +constexpr Milliseconds ROUTE_MS_PER_TILE_ACROSS = Milliseconds(160); // east or west +constexpr Milliseconds ROUTE_MS_PER_TILE_UP_DOWN = Milliseconds(240); // north or south +// From pressing the stick to the first step, when standing still. +constexpr Milliseconds ROUTE_START_DELAY = Milliseconds(250); +// Running (B held) covers a tile in this fraction of the walking time. B +// held with the D-pad did not run: r238a walked the Ravaged Path's corridor +// at 160 ms a tile, and a limit set for twice that speed cut a wall leg +// short by two tiles. 1 until running is measured to work. +constexpr int ROUTE_RUN_SPEEDUP = 1; + +// Walking time per tile in `direction`. +Milliseconds route_ms_per_tile(Pokemon::Direction direction); +// The left stick for a direction. Row 0 is the top of a map, and the +// stick's y is positive upward. +JoystickPosition route_stick(Pokemon::Direction direction); + +// How long to hold the stick for an open leg of `tiles` tiles, for a player +// already facing that way: a step once started is always finished, so stop +// half a tile early. On the console a 1290ms hold west went 9 tiles and an +// 800ms hold south 4: the player starts at once and finishes the step. +Milliseconds route_open_hold(Pokemon::Direction direction, size_t tiles); + +// A tap this short turns a player who faces another way, without a step: on +// the console a 150ms tap south after walking north only turned the player. +constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(100); + +// Turns the player to face `direction` without moving. Only for a player who +// faces another way: one already facing it may take a step. +void route_turn(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction); + +// Talks to someone on a tile next to ours, without trusting the tile we +// counted ourselves onto: for each of `directions` in order, turns that way, +// presses A and waits up to `wait` for a dialogue. Returns the direction that +// answered, its dialogue open, or none if nobody did. Turns are taps (see +// route_turn()), so do not list the way the player already faces first. +std::optional talk_to_adjacent_npc( + VideoStream& stream, ProControllerContext& context, + const std::vector& directions, + const std::string& label, Milliseconds wait = Milliseconds(1500) +); + +// Talks to someone straight ahead in `facing`, where we may stand a little +// off their tile: BDSP moves by any distance, and a sideways tap moves about +// a third of a tile (a 50ms tap 0.2 to 0.45 tiles, a 100ms turn tap 0.4, on +// the console 2026-10-31). Presses A, and while nobody answers shifts by one +// such tap at a time to -1, +1, -2, +2 taps from the start and tries again. +// Never turns away and back between tries: each turn is a tap that moves us +// (s03e2 drifted east of Roark a third of a tile per try). Returns whether a +// dialogue opened, left open; false also after a wild battle on a nudge, +// which leaves us off the place we were. +// `accept`, if given, looks at the screen before A and once the dialogue +// is open, and says whether it is the one wanted: a rock or a sign next to +// someone answers A too (r187a read "It's a rugged rock" beside Roark). A +// dialogue it turns down is closed with B and the nudges go on. +bool talk_with_nudges( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + Pokemon::Direction facing, const std::string& label, + Milliseconds wait = Milliseconds(3000), + const std::function& accept = nullptr +); +constexpr Milliseconds ROUTE_NUDGE_TAP = Milliseconds(50); + +// Whether a walk to the wall that ends on any of `after` can tell it has +// stopped: not in tall grass, whose sway keeps the screen changing at a +// wall. Elsewhere, a walk to the wall that stops confirms where it ended. +bool route_wall_readable(const StoryWorld& grid, const std::vector& after); + +// Shows what the route walk is doing in the log and the video overlay. +void route_status(VideoStream& stream, const std::string& text); + +// Finishes a dialogue that has opened, however many lines it has: presses +// `button` while a dialogue box shows, and returns once the overworld has +// shown with no box for 1.5 s (the box blinks off between lines and during +// "obtained" jingles). Use A where the talk asks Yes/No or gives an item (A +// answers Yes and closes "obtained" lines; B would answer No), B elsewhere +// (B never starts a new talk). Fails with a screenshot after `timeout`. +void finish_dialogue( + VideoStream& stream, ProControllerContext& context, + Button button, const std::string& label, + Milliseconds timeout = std::chrono::seconds(40) +); + +// For a player on the tile below a door: walks north through it. If a wall +// stops the walk (the door frame, a tile off the opening), steps a tile east, +// then west, and tries again. Fails with a screenshot if no step goes in. +void enter_door_north(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); + +// For a player inside a building whose door is straight south: closes any +// talk still up and walks south out of it, stopping at the door's fade or +// the first look that is not a room, so it ends a tile or so outside. Out is +// then three standing looks in a row that are not a room: one look of a lit +// Center can miss its black edge (r259). Fails with a screenshot if five +// walks south get nowhere. +void leave_door_south(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); + +// Brightness (mean r + g + b) below which a look is the door's fade, and +// above which a look that is not a room is outside. +inline constexpr double DOOR_FADE_DARK = 120.0; +inline constexpr double DOOR_OUTSIDE_BRIGHT = 200.0; + +// What leave_door_south() has seen of the door's fade so far. +struct DoorFadeWatch{ + bool saw_dark = false; +}; +// Takes one look (its brightness, and whether indoors() read a room) and +// returns whether we are out: a dark look seen, and now a bright one that +// is not a room. +bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room); +// Whether leave_door_south()'s walk stops at this look: at the door's fade +// out (door_fade_look()), or at the first look that is not a room, so a +// held stick does not walk on outside (r261 three rows). +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index 0f5d15fc9f..e0e3104d80 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -1,561 +1,561 @@ -/* BDSP AutoStory - Walk - * - * From: https://github.com/PokemonAutomation/ - * - * BattleDialogWatcher adapted from PokemonAutomation/Arduino-Source#1717 by - * Developer-Butters. - */ - -#include -#include "Common/Cpp/Exceptions.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/ImageTools/ImageDiff.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" -#include "CommonTools/VisualDetectors/BlackScreenDetector.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" -#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" -#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" -#include "PokemonBDSP_AutoStory_Walk.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using namespace std::chrono_literals; - - -const char* walk_stop_name(WalkStop stop){ - switch (stop){ - case WalkStop::ARRIVED: return "arrived"; - case WalkStop::DIALOG: return "dialog"; - case WalkStop::TRANSITION: return "transition"; - case WalkStop::BLOCKED: return "blocked"; - case WalkStop::LIMIT: return "limit"; - } - return "?"; -} - -double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after){ - // Small copies: only the whole-view scroll matters, not detail. - return ImageMatch::pixel_RMSD(before.scale_to(192, 108), after.scale_to(192, 108)); -} - - -namespace{ - -// The battle's text box, held for 250ms. -class BattleDialogWatcher : public DetectorToFinder{ -public: - explicit BattleDialogWatcher(Color color) - : DetectorToFinder("BattleDialogWatcher", std::chrono::milliseconds(250), color) - {} -}; - -VideoSnapshot snapshot_or_fail(VideoStream& stream, const std::string& label){ - VideoSnapshot frame = stream.video().snapshot(); - if (!frame){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": No video frame.", stream); - } - return frame; -} - -// After a black screen: a wild battle if a battle screen shows up, else a transition. -bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context){ - BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 100, 10, - BlackScreenWatcher::FinderType::GONE, 500ms); - const int ret = wait_until(stream, context, 10s, {{battle_menu}, {battle_dialog}, {gone}}); - if (ret == 2){ - // The battle intro also starts from black; give its first screen a moment. - BattleMenuWatcher late_menu(BattleType::STANDARD, COLOR_YELLOW); - BattleDialogWatcher late_dialog(COLOR_YELLOW); - return wait_until(stream, context, 2s, {{late_menu}, {late_dialog}}) >= 0; - } - return ret >= 0; -} - -// A grass encounter opens with a leaf animation, neither white nor black, and -// shows the battle's text box a few seconds later. After a change of the -// whole screen that walking cannot make, wait for the battle to show. -bool battle_follows(VideoStream& stream, ProControllerContext& context){ - BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - return wait_until(stream, context, 5s, {{battle_menu}, {battle_dialog}}) >= 0; -} - -} - -// A step in tall grass that was started before the stick was released still -// finishes, and its encounter starts after the walk has ended. Watch a little -// longer, and run from it. Returns whether there was one. -bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ - VideoSnapshot before = stream.video().snapshot(); - WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BlackScreenWatcher black(COLOR_RED); - BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - const int ret = wait_until(stream, context, 1200ms, {{encounter_flash}, {black}, {battle_menu}, {battle_dialog}}); - bool battle = ret == 0 || ret == 2 || ret == 3 || (ret == 1 && black_screen_was_battle(stream, context)); - if (ret < 0){ - VideoSnapshot after = stream.video().snapshot(); - battle = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE && - battle_follows(stream, context); - } - if (!battle){ - return false; - } - stream.log("[Walk] " + label + ": wild battle after the walk; running."); - escape(stream, context); - context.wait_for_all_requests(); - return true; -} -namespace{ - -// Samples the screen every 100ms for probe_walk() and stops it once the view -// has been still for 800ms. -class WalkProbeSampler : public VisualInferenceCallback{ -public: - WalkProbeSampler(WalkProbe& result, double moving_change, WallClock start) - : VisualInferenceCallback("WalkProbeSampler") - , m_result(result) - , m_moving_change(moving_change) - , m_start(start) - {} - virtual void make_overlays(VideoOverlaySet&) const override{} - virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override{ - if (m_has_last && timestamp - m_last_time < 100ms){ - return false; - } - const Milliseconds at = std::chrono::duration_cast(timestamp - m_start); - ImageRGB32 small = frame.scale_to(192, 108); - if (m_has_last){ - const double change = ImageMatch::pixel_RMSD(m_last, small); - m_result.samples.push_back({at, change}); - if (change >= m_moving_change){ - if (m_result.first_move < 0ms){ - m_result.first_move = at; - } - m_result.last_move = at; - } - } - m_last = std::move(small); - m_last_time = timestamp; - m_has_last = true; - const Milliseconds still_since = m_result.last_move < 0ms ? 0ms : m_result.last_move; - return at - still_since >= 800ms; - } - -private: - WalkProbe& m_result; - double m_moving_change; - WallClock m_start; - ImageRGB32 m_last; - WallClock m_last_time; - bool m_has_last = false; -}; - -} - - -namespace{ - -// What a walk presses. -enum class WalkInput{ - STICK, - DPAD, - DPAD_RUN, // the D-pad with B held - TAPS, // a D-pad tap a tile, 800 ms each -}; -// What a battle that starts on the way is. -enum class WildPolicy{ - RUN_FROM, // a wild battle: run, and walk on - STOP_AT, // no wild Pokemon here: a trainer's, so the walk stops with DIALOG -}; - -struct WalkSpec{ - JoystickPosition stick; - Milliseconds limit = 0ms; - std::string label; - WalkArrived arrived = nullptr; - WalkInput input = WalkInput::STICK; - WildPolicy wild = WildPolicy::RUN_FROM; -}; - -// What a chunk of the walk decided: stop with `stop`, walk the next chunk, -// or (after the watchers) look at how the screen changed. -enum class ChunkNext{ - STOP, - NEXT_CHUNK, - LOOK, -}; -struct ChunkVerdict{ - ChunkNext next = ChunkNext::LOOK; - WalkStop stop = WalkStop::LIMIT; -}; - -// walk_until() and its variants: holds the input a chunk at a time, and -// after each chunk reads the watchers, then the screen. -class WalkUntil{ -public: - WalkUntil(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const WalkSpec& spec) - : m_stream(stream) - , m_context(context) - , m_escape(escape) - , m_spec(spec) - , m_dpad(spec.stick.x < 0 ? DPAD_LEFT : spec.stick.x > 0 ? DPAD_RIGHT : spec.stick.y > 0 ? DPAD_UP : DPAD_DOWN) - {} - - WalkResult run(){ - // A tile a tap: 160 ms held, the rest released, so each chunk is one tile. - const Milliseconds CHUNK = m_spec.input == WalkInput::TAPS ? 800ms : 400ms; - m_context.wait_for_all_requests(); - m_before = snapshot_or_fail(m_stream, m_spec.label); - while (true){ - if (m_spec.arrived && m_spec.arrived(*m_before.frame)){ - return finish(WalkStop::ARRIVED); - } - if (m_result.walked >= m_spec.limit){ - return finish(WalkStop::LIMIT); - } - const Milliseconds chunk = std::min(CHUNK, m_spec.limit - m_result.walked); - const int ret = hold(chunk); - m_result.walked += chunk; - ChunkVerdict verdict = after_watchers(ret); - if (verdict.next == ChunkNext::LOOK){ - verdict = look(); - } - if (verdict.next == ChunkNext::STOP){ - return finish(verdict.stop); - } - } - } - -private: - int hold(Milliseconds chunk){ - WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BlackScreenWatcher black(COLOR_RED); - BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); - ShortDialogWatcher dialog(COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - return run_until( - m_stream, m_context, - [&](ProControllerContext& context){ - press(context, chunk); - }, - {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} - ); - } - - void press(ProControllerContext& context, Milliseconds chunk) const{ - switch (m_spec.input){ - case WalkInput::TAPS: - pbf_press_dpad(context, m_dpad, 160ms, chunk - 160ms); - return; - case WalkInput::DPAD_RUN: - ssf_press_button(context, BUTTON_B, 0ms, chunk, 0ms); - ssf_press_dpad(context, m_dpad, chunk, chunk, 0ms); - return; - case WalkInput::DPAD: - pbf_press_dpad(context, m_dpad, chunk, 0ms); - return; - case WalkInput::STICK: - pbf_move_left_joystick(context, m_spec.stick, chunk, 0ms); - return; - } - } - - // The watchers: 0 the encounter flash, 1 black, 2 the battle menu, 3 a - // dialogue, 4 the battle's text box. - ChunkVerdict after_watchers(int ret){ - bool battle = ret == 0 || ret == 2 || ret == 4; - if (ret == 1){ - battle = black_screen_was_battle(m_stream, m_context); - if (!battle){ - return {ChunkNext::STOP, WalkStop::TRANSITION}; - } - } - if (battle && m_spec.wild == WildPolicy::STOP_AT){ - m_stream.log("[Walk] " + m_spec.label + ": a battle started; a trainer's here."); - return {ChunkNext::STOP, WalkStop::DIALOG}; - } - if (battle){ - escape_battle("wild battle; running."); - return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; - } - if (ret == 3){ - return {ChunkNext::STOP, WalkStop::DIALOG}; - } - return {ChunkNext::LOOK, WalkStop::LIMIT}; - } - - ChunkVerdict look(){ - m_context.wait_for_all_requests(); - VideoSnapshot after = snapshot_or_fail(m_stream, m_spec.label); - const double change = walk_screen_change(*m_before.frame, *after.frame); - m_stream.log("[Walk] " + m_spec.label + ": " + std::to_string(m_result.walked.count()) + - "ms, screen change " + std::to_string(change) + "."); - if (change > WALK_SCENE_CHANGE && m_spec.wild == WildPolicy::STOP_AT && battle_follows(m_stream, m_context)){ - m_stream.log("[Walk] " + m_spec.label + ": a battle started; a trainer's here."); - return {ChunkNext::STOP, WalkStop::DIALOG}; - } - if (change > WALK_SCENE_CHANGE && battle_follows(m_stream, m_context)){ - escape_battle("wild battle (leaf opening); running."); - return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; - } - // Two still chunks in a row, so one dropped frame does not stop us. A - // frame exactly the same is a stalled video or a game hitch, not a wall: - // standing at a wall still animates. - if (change == 0){ - m_before = std::move(after); - return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; - } - const bool still = still_chunk(change); - if (!still){ - m_result.moved_chunks++; - } - m_unchanged = still ? m_unchanged + 1 : 0; - if (m_unchanged >= 2){ - return {ChunkNext::STOP, WalkStop::BLOCKED}; - } - m_before = std::move(after); - return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; - } - - // The largest change a chunk of this walk made is a step. A bump into an - // NPC measured 17.2 after steps of 63 to 65 (r250a, Floaroma's old - // woman), over the fixed line of 16, so for taps "still" is also - // anything under 40% of the walk's own steps. - bool still_chunk(double change){ - m_largest = std::max(m_largest, change); - const double line = m_spec.input == WalkInput::TAPS - ? std::max(WALK_TAP_BLOCKED_CHANGE, 0.4 * m_largest) - : WALK_BLOCKED_CHANGE; - return change < line; - } - - void escape_battle(const std::string& what){ - m_result.wild_battles++; - m_stream.log("[Walk] " + m_spec.label + ": " + what); - m_escape(m_stream, m_context); - m_context.wait_for_all_requests(); - m_before = snapshot_or_fail(m_stream, m_spec.label); - m_unchanged = 0; - } - - WalkResult finish(WalkStop stop){ - m_result.stop = stop; - if (m_spec.wild == WildPolicy::RUN_FROM && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && - run_from_late_encounter(m_stream, m_context, m_escape, m_spec.label) - ){ - m_result.wild_battles++; - } - m_stream.log("[Walk] " + m_spec.label + ": " + walk_stop_name(stop) + " after " + - std::to_string(m_result.walked.count()) + "ms, " + - std::to_string(m_result.wild_battles) + " wild battle(s)."); - return m_result; - } - - VideoStream& m_stream; - ProControllerContext& m_context; - const WildBattleEscape& m_escape; - const WalkSpec& m_spec; - const DpadPosition m_dpad; - WalkResult m_result{WalkStop::LIMIT, 0ms, 0}; - VideoSnapshot m_before; - size_t m_unchanged = 0; - double m_largest = 0; -}; - -WalkResult walk_until_spec( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const WalkSpec& spec -){ - return WalkUntil(stream, context, escape, spec).run(); -} - -} - -WalkResult walk_until( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label, - const WalkArrived& arrived -){ - return walk_until_spec(stream, context, escape, { - .stick = stick, .limit = limit, .label = label, .arrived = arrived, - .input = WalkInput::STICK, .wild = WildPolicy::RUN_FROM, - }); -} - -WalkResult walk_until_dpad( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label, - const DpadWalkOptions& options -){ - return walk_until_spec(stream, context, escape, { - .stick = stick, .limit = limit, .label = label, .arrived = options.arrived, - .input = options.pace == DpadPace::RUN ? WalkInput::DPAD_RUN : WalkInput::DPAD, - .wild = WildPolicy::RUN_FROM, - }); -} - -WalkResult walk_tiles_dpad( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, size_t tiles, const std::string& label -){ - return walk_until_spec(stream, context, escape, { - .stick = stick, .limit = 800ms * tiles, .label = label, .arrived = nullptr, - .input = WalkInput::TAPS, .wild = WildPolicy::RUN_FROM, - }); -} - -WalkResult walk_until_no_wild( - VideoStream& stream, ProControllerContext& context, - JoystickPosition stick, Milliseconds limit, const std::string& label -){ - // With no wild Pokemon any battle is a trainer's, and the walk stops at - // it (DIALOG) without running. - const WildBattleEscape no_wild = [](VideoStream& stream, ProControllerContext&){ - throw InternalProgramError(&stream.logger(), PA_CURRENT_FUNCTION, "walk_until_no_wild() ran from a battle."); - }; - return walk_until_spec(stream, context, no_wild, { - .stick = stick, .limit = limit, .label = label, .arrived = nullptr, - .input = WalkInput::STICK, .wild = WildPolicy::STOP_AT, - }); -} - -WalkResult walk_for( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds duration, const std::string& label -){ - WalkResult result{WalkStop::LIMIT, duration, 0}; - context.wait_for_all_requests(); - WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BlackScreenWatcher black(COLOR_RED); - BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); - ShortDialogWatcher dialog(COLOR_YELLOW); - BattleDialogWatcher battle_dialog(COLOR_YELLOW); - const int ret = run_until( - stream, context, - [&](ProControllerContext& context){ - pbf_move_left_joystick(context, stick, duration, 0ms); - }, - {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} - ); - bool battle = ret == 0 || ret == 2 || ret == 4; - if (ret == 1){ - battle = black_screen_was_battle(stream, context); - if (!battle){ - result.stop = WalkStop::TRANSITION; - } - } - if (battle){ - result.wild_battles = 1; - stream.log("[Walk] " + label + ": wild battle; running."); - escape(stream, context); - context.wait_for_all_requests(); - } - if (ret == 3){ - result.stop = WalkStop::DIALOG; - } - // An encounter after the hold leaves us where the walk ended, so it does - // not count as cutting the walk short. - if (ret < 0){ - run_from_late_encounter(stream, context, escape, label); - } - stream.log("[Walk] " + label + ": " + walk_stop_name(result.stop) + " after a " + - std::to_string(duration.count()) + "ms hold, " + std::to_string(result.wild_battles) + " wild battle(s)."); - return result; -} - -WalkResult walk_to_talk( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label -){ - WalkResult total{WalkStop::LIMIT, 0ms, 0}; - while (total.walked < limit){ - const WalkResult step = walk_until(stream, context, escape, stick, - std::min(400ms, limit - total.walked), label); - total.walked += step.walked; - total.wild_battles += step.wild_battles; - if (step.stop == WalkStop::DIALOG || step.stop == WalkStop::TRANSITION){ - total.stop = step.stop; - return total; - } - ShortDialogWatcher dialog(COLOR_YELLOW); - const int ret = run_until( - stream, context, - [](ProControllerContext& context){ - pbf_press_button(context, BUTTON_A, 80ms, 720ms); - }, - {{dialog}} - ); - if (ret == 0){ - total.stop = WalkStop::DIALOG; - stream.log("[Walk] " + label + ": talking after " + std::to_string(total.walked.count()) + "ms."); - return total; - } - } - stream.log("[Walk] " + label + ": nobody to talk to within " + std::to_string(limit.count()) + "ms."); - return total; -} - -WalkProbe probe_walk( - VideoStream& stream, ProControllerContext& context, - JoystickPosition stick, Milliseconds limit, - double moving_change, const std::string& label -){ - WalkProbe result; - context.wait_for_all_requests(); - WalkProbeSampler sampler(result, moving_change, current_time()); - run_until( - stream, context, - [&](ProControllerContext& context){ - pbf_move_left_joystick(context, stick, limit, 0ms); - }, - {{sampler}} - ); - context.wait_for_all_requests(); - stream.log("[Walk] " + label + ": probe moved from " + std::to_string(result.first_move.count()) + - "ms to " + std::to_string(result.last_move.count()) + "ms."); - return result; -} - -WalkResult walk_to_wall( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label -){ - const WalkResult result = walk_until(stream, context, escape, stick, limit, label); - if (result.stop == WalkStop::DIALOG || result.stop == WalkStop::TRANSITION){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": " + walk_stop_name(result.stop) + " instead of a wall.", stream); - } - if (result.stop == WalkStop::LIMIT){ - stream.log("[Walk] " + label + ": held for the whole limit without seeing the stop; taking the wall as reached."); - } - return result; -} - -WalkResult walk_expecting( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label, - const WalkExpectation& expected -){ - const WalkResult result = walk_until(stream, context, escape, stick, limit, label, expected.arrived); - if (result.stop != expected.stop){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - label + ": expected " + walk_stop_name(expected.stop) + ", got " + - walk_stop_name(result.stop) + ".", stream); - } - return result; -} - - -} -} -} +/* BDSP AutoStory - Walk + * + * From: https://github.com/PokemonAutomation/ + * + * BattleDialogWatcher adapted from PokemonAutomation/Arduino-Source#1717 by + * Developer-Butters. + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageDiff.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" +#include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; + + +const char* walk_stop_name(WalkStop stop){ + switch (stop){ + case WalkStop::ARRIVED: return "arrived"; + case WalkStop::DIALOG: return "dialog"; + case WalkStop::TRANSITION: return "transition"; + case WalkStop::BLOCKED: return "blocked"; + case WalkStop::LIMIT: return "limit"; + } + return "?"; +} + +double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after){ + // Small copies: only the whole-view scroll matters, not detail. + return ImageMatch::pixel_RMSD(before.scale_to(192, 108), after.scale_to(192, 108)); +} + + +namespace{ + +// The battle's text box, held for 250ms. +class BattleDialogWatcher : public DetectorToFinder{ +public: + explicit BattleDialogWatcher(Color color) + : DetectorToFinder("BattleDialogWatcher", std::chrono::milliseconds(250), color) + {} +}; + +VideoSnapshot snapshot_or_fail(VideoStream& stream, const std::string& label){ + VideoSnapshot frame = stream.video().snapshot(); + if (!frame){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": No video frame.", stream); + } + return frame; +} + +// After a black screen: a wild battle if a battle screen shows up, else a transition. +bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context){ + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 100, 10, + BlackScreenWatcher::FinderType::GONE, 500ms); + const int ret = wait_until(stream, context, 10s, {{battle_menu}, {battle_dialog}, {gone}}); + if (ret == 2){ + // The battle intro also starts from black; give its first screen a moment. + BattleMenuWatcher late_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher late_dialog(COLOR_YELLOW); + return wait_until(stream, context, 2s, {{late_menu}, {late_dialog}}) >= 0; + } + return ret >= 0; +} + +// A grass encounter opens with a leaf animation, neither white nor black, and +// shows the battle's text box a few seconds later. After a change of the +// whole screen that walking cannot make, wait for the battle to show. +bool battle_follows(VideoStream& stream, ProControllerContext& context){ + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + return wait_until(stream, context, 5s, {{battle_menu}, {battle_dialog}}) >= 0; +} + +} + +// A step in tall grass that was started before the stick was released still +// finishes, and its encounter starts after the walk has ended. Watch a little +// longer, and run from it. Returns whether there was one. +bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ + VideoSnapshot before = stream.video().snapshot(); + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = wait_until(stream, context, 1200ms, {{encounter_flash}, {black}, {battle_menu}, {battle_dialog}}); + bool battle = ret == 0 || ret == 2 || ret == 3 || (ret == 1 && black_screen_was_battle(stream, context)); + if (ret < 0){ + VideoSnapshot after = stream.video().snapshot(); + battle = before && after && walk_screen_change(*before.frame, *after.frame) > WALK_SCENE_CHANGE && + battle_follows(stream, context); + } + if (!battle){ + return false; + } + stream.log("[Walk] " + label + ": wild battle after the walk; running."); + escape(stream, context); + context.wait_for_all_requests(); + return true; +} +namespace{ + +// Samples the screen every 100ms for probe_walk() and stops it once the view +// has been still for 800ms. +class WalkProbeSampler : public VisualInferenceCallback{ +public: + WalkProbeSampler(WalkProbe& result, double moving_change, WallClock start) + : VisualInferenceCallback("WalkProbeSampler") + , m_result(result) + , m_moving_change(moving_change) + , m_start(start) + {} + virtual void make_overlays(VideoOverlaySet&) const override{} + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override{ + if (m_has_last && timestamp - m_last_time < 100ms){ + return false; + } + const Milliseconds at = std::chrono::duration_cast(timestamp - m_start); + ImageRGB32 small = frame.scale_to(192, 108); + if (m_has_last){ + const double change = ImageMatch::pixel_RMSD(m_last, small); + m_result.samples.push_back({at, change}); + if (change >= m_moving_change){ + if (m_result.first_move < 0ms){ + m_result.first_move = at; + } + m_result.last_move = at; + } + } + m_last = std::move(small); + m_last_time = timestamp; + m_has_last = true; + const Milliseconds still_since = m_result.last_move < 0ms ? 0ms : m_result.last_move; + return at - still_since >= 800ms; + } + +private: + WalkProbe& m_result; + double m_moving_change; + WallClock m_start; + ImageRGB32 m_last; + WallClock m_last_time; + bool m_has_last = false; +}; + +} + + +namespace{ + +// What a walk presses. +enum class WalkInput{ + STICK, + DPAD, + DPAD_RUN, // the D-pad with B held + TAPS, // a D-pad tap a tile, 800 ms each +}; +// What a battle that starts on the way is. +enum class WildPolicy{ + RUN_FROM, // a wild battle: run, and walk on + STOP_AT, // no wild Pokemon here: a trainer's, so the walk stops with DIALOG +}; + +struct WalkSpec{ + JoystickPosition stick; + Milliseconds limit = 0ms; + std::string label; + WalkArrived arrived = nullptr; + WalkInput input = WalkInput::STICK; + WildPolicy wild = WildPolicy::RUN_FROM; +}; + +// What a chunk of the walk decided: stop with `stop`, walk the next chunk, +// or (after the watchers) look at how the screen changed. +enum class ChunkNext{ + STOP, + NEXT_CHUNK, + LOOK, +}; +struct ChunkVerdict{ + ChunkNext next = ChunkNext::LOOK; + WalkStop stop = WalkStop::LIMIT; +}; + +// walk_until() and its variants: holds the input a chunk at a time, and +// after each chunk reads the watchers, then the screen. +class WalkUntil{ +public: + WalkUntil(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const WalkSpec& spec) + : m_stream(stream) + , m_context(context) + , m_escape(escape) + , m_spec(spec) + , m_dpad(spec.stick.x < 0 ? DPAD_LEFT : spec.stick.x > 0 ? DPAD_RIGHT : spec.stick.y > 0 ? DPAD_UP : DPAD_DOWN) + {} + + WalkResult run(){ + // A tile a tap: 160 ms held, the rest released, so each chunk is one tile. + const Milliseconds CHUNK = m_spec.input == WalkInput::TAPS ? 800ms : 400ms; + m_context.wait_for_all_requests(); + m_before = snapshot_or_fail(m_stream, m_spec.label); + while (true){ + if (m_spec.arrived && m_spec.arrived(*m_before.frame)){ + return finish(WalkStop::ARRIVED); + } + if (m_result.walked >= m_spec.limit){ + return finish(WalkStop::LIMIT); + } + const Milliseconds chunk = std::min(CHUNK, m_spec.limit - m_result.walked); + const int ret = hold(chunk); + m_result.walked += chunk; + ChunkVerdict verdict = after_watchers(ret); + if (verdict.next == ChunkNext::LOOK){ + verdict = look(); + } + if (verdict.next == ChunkNext::STOP){ + return finish(verdict.stop); + } + } + } + +private: + int hold(Milliseconds chunk){ + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + ShortDialogWatcher dialog(COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + return run_until( + m_stream, m_context, + [&](ProControllerContext& context){ + press(context, chunk); + }, + {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} + ); + } + + void press(ProControllerContext& context, Milliseconds chunk) const{ + switch (m_spec.input){ + case WalkInput::TAPS: + pbf_press_dpad(context, m_dpad, 160ms, chunk - 160ms); + return; + case WalkInput::DPAD_RUN: + ssf_press_button(context, BUTTON_B, 0ms, chunk, 0ms); + ssf_press_dpad(context, m_dpad, chunk, chunk, 0ms); + return; + case WalkInput::DPAD: + pbf_press_dpad(context, m_dpad, chunk, 0ms); + return; + case WalkInput::STICK: + pbf_move_left_joystick(context, m_spec.stick, chunk, 0ms); + return; + } + } + + // The watchers: 0 the encounter flash, 1 black, 2 the battle menu, 3 a + // dialogue, 4 the battle's text box. + ChunkVerdict after_watchers(int ret){ + bool battle = ret == 0 || ret == 2 || ret == 4; + if (ret == 1){ + battle = black_screen_was_battle(m_stream, m_context); + if (!battle){ + return {ChunkNext::STOP, WalkStop::TRANSITION}; + } + } + if (battle && m_spec.wild == WildPolicy::STOP_AT){ + m_stream.log("[Walk] " + m_spec.label + ": a battle started; a trainer's here."); + return {ChunkNext::STOP, WalkStop::DIALOG}; + } + if (battle){ + escape_battle("wild battle; running."); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; + } + if (ret == 3){ + return {ChunkNext::STOP, WalkStop::DIALOG}; + } + return {ChunkNext::LOOK, WalkStop::LIMIT}; + } + + ChunkVerdict look(){ + m_context.wait_for_all_requests(); + VideoSnapshot after = snapshot_or_fail(m_stream, m_spec.label); + const double change = walk_screen_change(*m_before.frame, *after.frame); + m_stream.log("[Walk] " + m_spec.label + ": " + std::to_string(m_result.walked.count()) + + "ms, screen change " + std::to_string(change) + "."); + if (change > WALK_SCENE_CHANGE && m_spec.wild == WildPolicy::STOP_AT && battle_follows(m_stream, m_context)){ + m_stream.log("[Walk] " + m_spec.label + ": a battle started; a trainer's here."); + return {ChunkNext::STOP, WalkStop::DIALOG}; + } + if (change > WALK_SCENE_CHANGE && battle_follows(m_stream, m_context)){ + escape_battle("wild battle (leaf opening); running."); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; + } + // Two still chunks in a row, so one dropped frame does not stop us. A + // frame exactly the same is a stalled video or a game hitch, not a wall: + // standing at a wall still animates. + if (change == 0){ + m_before = std::move(after); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; + } + const bool still = still_chunk(change); + if (!still){ + m_result.moved_chunks++; + } + m_unchanged = still ? m_unchanged + 1 : 0; + if (m_unchanged >= 2){ + return {ChunkNext::STOP, WalkStop::BLOCKED}; + } + m_before = std::move(after); + return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; + } + + // The largest change a chunk of this walk made is a step. A bump into an + // NPC measured 17.2 after steps of 63 to 65 (r250a, Floaroma's old + // woman), over the fixed line of 16, so for taps "still" is also + // anything under 40% of the walk's own steps. + bool still_chunk(double change){ + m_largest = std::max(m_largest, change); + const double line = m_spec.input == WalkInput::TAPS + ? std::max(WALK_TAP_BLOCKED_CHANGE, 0.4 * m_largest) + : WALK_BLOCKED_CHANGE; + return change < line; + } + + void escape_battle(const std::string& what){ + m_result.wild_battles++; + m_stream.log("[Walk] " + m_spec.label + ": " + what); + m_escape(m_stream, m_context); + m_context.wait_for_all_requests(); + m_before = snapshot_or_fail(m_stream, m_spec.label); + m_unchanged = 0; + } + + WalkResult finish(WalkStop stop){ + m_result.stop = stop; + if (m_spec.wild == WildPolicy::RUN_FROM && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && + run_from_late_encounter(m_stream, m_context, m_escape, m_spec.label) + ){ + m_result.wild_battles++; + } + m_stream.log("[Walk] " + m_spec.label + ": " + walk_stop_name(stop) + " after " + + std::to_string(m_result.walked.count()) + "ms, " + + std::to_string(m_result.wild_battles) + " wild battle(s)."); + return m_result; + } + + VideoStream& m_stream; + ProControllerContext& m_context; + const WildBattleEscape& m_escape; + const WalkSpec& m_spec; + const DpadPosition m_dpad; + WalkResult m_result{WalkStop::LIMIT, 0ms, 0}; + VideoSnapshot m_before; + size_t m_unchanged = 0; + double m_largest = 0; +}; + +WalkResult walk_until_spec( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const WalkSpec& spec +){ + return WalkUntil(stream, context, escape, spec).run(); +} + +} + +WalkResult walk_until( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkArrived& arrived +){ + return walk_until_spec(stream, context, escape, { + .stick = stick, .limit = limit, .label = label, .arrived = arrived, + .input = WalkInput::STICK, .wild = WildPolicy::RUN_FROM, + }); +} + +WalkResult walk_until_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const DpadWalkOptions& options +){ + return walk_until_spec(stream, context, escape, { + .stick = stick, .limit = limit, .label = label, .arrived = options.arrived, + .input = options.pace == DpadPace::RUN ? WalkInput::DPAD_RUN : WalkInput::DPAD, + .wild = WildPolicy::RUN_FROM, + }); +} + +WalkResult walk_tiles_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, size_t tiles, const std::string& label +){ + return walk_until_spec(stream, context, escape, { + .stick = stick, .limit = 800ms * tiles, .label = label, .arrived = nullptr, + .input = WalkInput::TAPS, .wild = WildPolicy::RUN_FROM, + }); +} + +WalkResult walk_until_no_wild( + VideoStream& stream, ProControllerContext& context, + JoystickPosition stick, Milliseconds limit, const std::string& label +){ + // With no wild Pokemon any battle is a trainer's, and the walk stops at + // it (DIALOG) without running. + const WildBattleEscape no_wild = [](VideoStream& stream, ProControllerContext&){ + throw InternalProgramError(&stream.logger(), PA_CURRENT_FUNCTION, "walk_until_no_wild() ran from a battle."); + }; + return walk_until_spec(stream, context, no_wild, { + .stick = stick, .limit = limit, .label = label, .arrived = nullptr, + .input = WalkInput::STICK, .wild = WildPolicy::STOP_AT, + }); +} + +WalkResult walk_for( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds duration, const std::string& label +){ + WalkResult result{WalkStop::LIMIT, duration, 0}; + context.wait_for_all_requests(); + WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); + BlackScreenWatcher black(COLOR_RED); + BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); + ShortDialogWatcher dialog(COLOR_YELLOW); + BattleDialogWatcher battle_dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [&](ProControllerContext& context){ + pbf_move_left_joystick(context, stick, duration, 0ms); + }, + {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} + ); + bool battle = ret == 0 || ret == 2 || ret == 4; + if (ret == 1){ + battle = black_screen_was_battle(stream, context); + if (!battle){ + result.stop = WalkStop::TRANSITION; + } + } + if (battle){ + result.wild_battles = 1; + stream.log("[Walk] " + label + ": wild battle; running."); + escape(stream, context); + context.wait_for_all_requests(); + } + if (ret == 3){ + result.stop = WalkStop::DIALOG; + } + // An encounter after the hold leaves us where the walk ended, so it does + // not count as cutting the walk short. + if (ret < 0){ + run_from_late_encounter(stream, context, escape, label); + } + stream.log("[Walk] " + label + ": " + walk_stop_name(result.stop) + " after a " + + std::to_string(duration.count()) + "ms hold, " + std::to_string(result.wild_battles) + " wild battle(s)."); + return result; +} + +WalkResult walk_to_talk( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label +){ + WalkResult total{WalkStop::LIMIT, 0ms, 0}; + while (total.walked < limit){ + const WalkResult step = walk_until(stream, context, escape, stick, + std::min(400ms, limit - total.walked), label); + total.walked += step.walked; + total.wild_battles += step.wild_battles; + if (step.stop == WalkStop::DIALOG || step.stop == WalkStop::TRANSITION){ + total.stop = step.stop; + return total; + } + ShortDialogWatcher dialog(COLOR_YELLOW); + const int ret = run_until( + stream, context, + [](ProControllerContext& context){ + pbf_press_button(context, BUTTON_A, 80ms, 720ms); + }, + {{dialog}} + ); + if (ret == 0){ + total.stop = WalkStop::DIALOG; + stream.log("[Walk] " + label + ": talking after " + std::to_string(total.walked.count()) + "ms."); + return total; + } + } + stream.log("[Walk] " + label + ": nobody to talk to within " + std::to_string(limit.count()) + "ms."); + return total; +} + +WalkProbe probe_walk( + VideoStream& stream, ProControllerContext& context, + JoystickPosition stick, Milliseconds limit, + double moving_change, const std::string& label +){ + WalkProbe result; + context.wait_for_all_requests(); + WalkProbeSampler sampler(result, moving_change, current_time()); + run_until( + stream, context, + [&](ProControllerContext& context){ + pbf_move_left_joystick(context, stick, limit, 0ms); + }, + {{sampler}} + ); + context.wait_for_all_requests(); + stream.log("[Walk] " + label + ": probe moved from " + std::to_string(result.first_move.count()) + + "ms to " + std::to_string(result.last_move.count()) + "ms."); + return result; +} + +WalkResult walk_to_wall( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label +){ + const WalkResult result = walk_until(stream, context, escape, stick, limit, label); + if (result.stop == WalkStop::DIALOG || result.stop == WalkStop::TRANSITION){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": " + walk_stop_name(result.stop) + " instead of a wall.", stream); + } + if (result.stop == WalkStop::LIMIT){ + stream.log("[Walk] " + label + ": held for the whole limit without seeing the stop; taking the wall as reached."); + } + return result; +} + +WalkResult walk_expecting( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkExpectation& expected +){ + const WalkResult result = walk_until(stream, context, escape, stick, limit, label, expected.arrived); + if (result.stop != expected.stop){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": expected " + walk_stop_name(expected.stop) + ", got " + + walk_stop_name(result.stop) + ".", stream); + } + return result; +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h index 9beb976f6e..504edccfe8 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -1,189 +1,189 @@ -/* BDSP AutoStory - Walk - * - * From: https://github.com/PokemonAutomation/ - * - * Walking that reads the screen instead of trusting a duration: hold a - * direction and stop on whatever the screen says happened. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walk_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Walk_H - -#include -#include -#include -#include "Common/Cpp/Time.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" -#include "CommonFramework/Tools/VideoStream.h" -#include "Controllers/Joystick.h" -#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Runs from a wild battle that has interrupted a walk, and returns once the -// player stands on the overworld again. The walker only sees that a battle -// started; how to leave it is the program's battle code, passed in. -using WildBattleEscape = std::function; - -enum class WalkStop{ - ARRIVED, // `arrived` matched. - DIALOG, // A dialogue box opened: an NPC or a trainer stopped us. - TRANSITION, // The screen went black and came back without a battle: a door or a new area. - BLOCKED, // The screen stopped changing: we are walking into a wall. - LIMIT, // Walked `limit` without any of the above. -}; -const char* walk_stop_name(WalkStop stop); - -// Whether the walk has arrived, from the screen after each chunk. -using WalkArrived = std::function; - -struct WalkResult{ - WalkStop stop; - Milliseconds walked; - size_t wild_battles; - // Chunks whose screen change passed the blocked threshold: on a tap - // walk, the taps that moved a tile. A trainer's freeze and a bump do - // not count (r248a: "4 taps before the battle" were 2 steps and 2 - // taps into Taylor's freeze). - size_t moved_chunks = 0; -}; - -// Holds the left stick at `stick` until one of the WalkStop events. Wild battles on -// the way are run from, and walking resumes. Does not throw for any WalkStop; the -// caller decides which ones are expected. -WalkResult walk_until( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label, - const WalkArrived& arrived = nullptr -); - -// How walk_until_dpad() presses: RUN holds B too, twice as fast and as -// tile-true. -enum class DpadPace{ - WALK, - RUN, -}; -struct DpadWalkOptions{ - WalkArrived arrived = nullptr; - DpadPace pace = DpadPace::WALK; -}; - -// walk_until() on the D-pad, for a straight walk along a row or a column -// (`stick` is one of the four directions). The D-pad moves whole tiles and -// keeps to the row: the stick drifts off it, catches tree corners and slides -// a row on stairs (Route 203, r226a and r227b; by hand the D-pad went -// through the same places). -WalkResult walk_until_dpad( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label, - const DpadWalkOptions& options = {} -); - -// A set number of whole tiles on the D-pad, a tap a tile (a tap moves -// exactly one tile; a stick hold timed for n tiles is a tile or two off), -// with the same watch for wild battles and dialogue as walk_until(). Ends -// LIMIT after `tiles` taps, or BLOCKED when two taps in a row move nothing. -WalkResult walk_tiles_dpad( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, size_t tiles, const std::string& label -); - -// walk_until() where there are no wild Pokemon (a gym, a building): any -// battle that starts is a trainer's, so the walk stops with DIALOG, left in -// the battle for the caller to fight, instead of trying to run from it -// (r206d tried to run from the second Oreburgh Gym trainer). -WalkResult walk_until_no_wild( - VideoStream& stream, ProControllerContext& context, - JoystickPosition stick, Milliseconds limit, const std::string& label -); - -// Holds the left stick at `stick` for `duration` in one go, for a walk of a -// set length: walk_until() checks the screen between short holds, and each -// break can cost a step. Stops early for a dialog or a transition. A wild -// battle is run from and ends the walk with `wild_battles` 1, at a place the -// caller cannot know. -WalkResult walk_for( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds duration, const std::string& label -); - -// The stop walk_expecting() requires, and what counts as arrived. -struct WalkExpectation{ - WalkStop stop = WalkStop::ARRIVED; - WalkArrived arrived = nullptr; -}; -// walk_until() that requires `expected.stop`. Fails with the screenshot otherwise. -WalkResult walk_expecting( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label, - const WalkExpectation& expected -); - -// After a move that may have walked onto an encounter tile (a tap in a cave -// or in tall grass): waits briefly for a wild battle and runs from it. -// Returns whether there was one; we may then be off the place we expected. -bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); - -// walk_until() toward a wall the route was measured to end at. Seeing the -// stop only ends the hold early: holding on for the whole `limit` also -// counts as there, as people or animations nearby can keep the screen -// changing at a wall (s03a1: a hiker and a child by the Mart fence; s03c1: -// Roark's pickaxe; r162a: Jubilife's TV). A wrong place is caught by the -// next gate. Fails on a dialog or a transition: we are not where we think. -WalkResult walk_to_wall( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label -); - -// Walks toward someone to talk to: after each short stretch, presses A and stops -// once a dialogue opens. Use it where walking into the person does not read as -// BLOCKED, e.g. in tall grass, whose sway keeps the screen changing. -WalkResult walk_to_talk( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - JoystickPosition stick, Milliseconds limit, const std::string& label -); - -// How much the screen changed between two frames, 0 for identical. Walking scrolls -// the whole view: 16 to 85 per 400ms on Route 202, against about 6 for running in -// place against trees. -double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after); -constexpr double WALK_BLOCKED_CHANGE = 10.0; -// For whole-tile D-pad taps: a tap that moves changes the screen by 21 to 50 -// (r244a: 40 per tap along Route 204's row 27); a tap into a wall or an NPC -// bumps for 10 to 13.4, and an NPC walking past does the same, so 10 counted -// bumps as steps (r244a, 2026-10-31). -constexpr double WALK_TAP_BLOCKED_CHANGE = 16.0; -// More than walking changes in 400ms: a new scene, such as a battle opening. -constexpr double WALK_SCENE_CHANGE = 120.0; - - -// One straight walk timed finely, to check a map against the game: holds the -// stick at `stick` and samples the screen every 100ms until the view has been -// still for 800ms or `limit` runs out. Times count from the start of the hold. -// A sample moved if it changed by at least `moving_change`: still is 4 to 7, -// walking 16 to 85. Handles no battles or dialogs: use it on open ground, -// away from tall grass. -struct WalkProbe{ - Milliseconds first_move{-1}; // the first sample that moved; -1 if none did - Milliseconds last_move{-1}; // the last sample that moved; -1 if none did - struct Sample{ - Milliseconds at; - double change; // walk_screen_change() since the previous sample - }; - std::vector samples; -}; -WalkProbe probe_walk( - VideoStream& stream, ProControllerContext& context, - JoystickPosition stick, Milliseconds limit, - double moving_change, const std::string& label -); - - -} -} -} -#endif +/* BDSP AutoStory - Walk + * + * From: https://github.com/PokemonAutomation/ + * + * Walking that reads the screen instead of trusting a duration: hold a + * direction and stop on whatever the screen says happened. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Walk_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Walk_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "Controllers/Joystick.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Runs from a wild battle that has interrupted a walk, and returns once the +// player stands on the overworld again. The walker only sees that a battle +// started; how to leave it is the program's battle code, passed in. +using WildBattleEscape = std::function; + +enum class WalkStop{ + ARRIVED, // `arrived` matched. + DIALOG, // A dialogue box opened: an NPC or a trainer stopped us. + TRANSITION, // The screen went black and came back without a battle: a door or a new area. + BLOCKED, // The screen stopped changing: we are walking into a wall. + LIMIT, // Walked `limit` without any of the above. +}; +const char* walk_stop_name(WalkStop stop); + +// Whether the walk has arrived, from the screen after each chunk. +using WalkArrived = std::function; + +struct WalkResult{ + WalkStop stop; + Milliseconds walked; + size_t wild_battles; + // Chunks whose screen change passed the blocked threshold: on a tap + // walk, the taps that moved a tile. A trainer's freeze and a bump do + // not count (r248a: "4 taps before the battle" were 2 steps and 2 + // taps into Taylor's freeze). + size_t moved_chunks = 0; +}; + +// Holds the left stick at `stick` until one of the WalkStop events. Wild battles on +// the way are run from, and walking resumes. Does not throw for any WalkStop; the +// caller decides which ones are expected. +WalkResult walk_until( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkArrived& arrived = nullptr +); + +// How walk_until_dpad() presses: RUN holds B too, twice as fast and as +// tile-true. +enum class DpadPace{ + WALK, + RUN, +}; +struct DpadWalkOptions{ + WalkArrived arrived = nullptr; + DpadPace pace = DpadPace::WALK; +}; + +// walk_until() on the D-pad, for a straight walk along a row or a column +// (`stick` is one of the four directions). The D-pad moves whole tiles and +// keeps to the row: the stick drifts off it, catches tree corners and slides +// a row on stairs (Route 203, r226a and r227b; by hand the D-pad went +// through the same places). +WalkResult walk_until_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const DpadWalkOptions& options = {} +); + +// A set number of whole tiles on the D-pad, a tap a tile (a tap moves +// exactly one tile; a stick hold timed for n tiles is a tile or two off), +// with the same watch for wild battles and dialogue as walk_until(). Ends +// LIMIT after `tiles` taps, or BLOCKED when two taps in a row move nothing. +WalkResult walk_tiles_dpad( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, size_t tiles, const std::string& label +); + +// walk_until() where there are no wild Pokemon (a gym, a building): any +// battle that starts is a trainer's, so the walk stops with DIALOG, left in +// the battle for the caller to fight, instead of trying to run from it +// (r206d tried to run from the second Oreburgh Gym trainer). +WalkResult walk_until_no_wild( + VideoStream& stream, ProControllerContext& context, + JoystickPosition stick, Milliseconds limit, const std::string& label +); + +// Holds the left stick at `stick` for `duration` in one go, for a walk of a +// set length: walk_until() checks the screen between short holds, and each +// break can cost a step. Stops early for a dialog or a transition. A wild +// battle is run from and ends the walk with `wild_battles` 1, at a place the +// caller cannot know. +WalkResult walk_for( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds duration, const std::string& label +); + +// The stop walk_expecting() requires, and what counts as arrived. +struct WalkExpectation{ + WalkStop stop = WalkStop::ARRIVED; + WalkArrived arrived = nullptr; +}; +// walk_until() that requires `expected.stop`. Fails with the screenshot otherwise. +WalkResult walk_expecting( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label, + const WalkExpectation& expected +); + +// After a move that may have walked onto an encounter tile (a tap in a cave +// or in tall grass): waits briefly for a wild battle and runs from it. +// Returns whether there was one; we may then be off the place we expected. +bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); + +// walk_until() toward a wall the route was measured to end at. Seeing the +// stop only ends the hold early: holding on for the whole `limit` also +// counts as there, as people or animations nearby can keep the screen +// changing at a wall (s03a1: a hiker and a child by the Mart fence; s03c1: +// Roark's pickaxe; r162a: Jubilife's TV). A wrong place is caught by the +// next gate. Fails on a dialog or a transition: we are not where we think. +WalkResult walk_to_wall( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label +); + +// Walks toward someone to talk to: after each short stretch, presses A and stops +// once a dialogue opens. Use it where walking into the person does not read as +// BLOCKED, e.g. in tall grass, whose sway keeps the screen changing. +WalkResult walk_to_talk( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, + JoystickPosition stick, Milliseconds limit, const std::string& label +); + +// How much the screen changed between two frames, 0 for identical. Walking scrolls +// the whole view: 16 to 85 per 400ms on Route 202, against about 6 for running in +// place against trees. +double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after); +constexpr double WALK_BLOCKED_CHANGE = 10.0; +// For whole-tile D-pad taps: a tap that moves changes the screen by 21 to 50 +// (r244a: 40 per tap along Route 204's row 27); a tap into a wall or an NPC +// bumps for 10 to 13.4, and an NPC walking past does the same, so 10 counted +// bumps as steps (r244a, 2026-10-31). +constexpr double WALK_TAP_BLOCKED_CHANGE = 16.0; +// More than walking changes in 400ms: a new scene, such as a battle opening. +constexpr double WALK_SCENE_CHANGE = 120.0; + + +// One straight walk timed finely, to check a map against the game: holds the +// stick at `stick` and samples the screen every 100ms until the view has been +// still for 800ms or `limit` runs out. Times count from the start of the hold. +// A sample moved if it changed by at least `moving_change`: still is 4 to 7, +// walking 16 to 85. Handles no battles or dialogs: use it on open ground, +// away from tall grass. +struct WalkProbe{ + Milliseconds first_move{-1}; // the first sample that moved; -1 if none did + Milliseconds last_move{-1}; // the last sample that moved; -1 if none did + struct Sample{ + Milliseconds at; + double change; // walk_screen_change() since the previous sample + }; + std::vector samples; +}; +WalkProbe probe_walk( + VideoStream& stream, ProControllerContext& context, + JoystickPosition stick, Milliseconds limit, + double moving_change, const std::string& label +); + + +} +} +} +#endif From 4cec241a2c602976122f205fe9e1a2732fa97825 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 17:20:57 -0800 Subject: [PATCH 14/39] BDSP walker: give up after the third dialogue the map does not explain r14 walked into Barry's Route 201 warning, which no map records, took it for an unknown trainer, replanned into it again and looped for minutes. A route now fails on the third such stop instead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index da8af82032..e7bccff334 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -267,6 +267,8 @@ class RouteWalker{ // Every banner read on the way, to check the map we arrive on against. std::vector m_all_banners; std::string m_map_start; + // Dialogue the map does not explain, so far on this route. + size_t m_unexplained_talks = 0; }; std::vector RouteWalker::run(std::vector from){ @@ -643,6 +645,12 @@ LegNext RouteWalker::unknown_trainer(LegWalk& walk, const std::vector 2){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + walk.label + ": dialogue the map does not explain stopped us 3 times.", m_stream); + } route_status(m_stream, m_plan.label + ": a trainer the map lacks stopped us; fighting."); m_trainers.fight("unknown trainer"); if (walk.planned.leg.ends_at_wall){ From 33761c21c627bc5804d2a9fb88d8bb9c0947a950 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 18:18:41 -0800 Subject: [PATCH 15/39] BDSP walker: a scene at a map's edge stands in for the missing banner Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index e7bccff334..8db5d2c782 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -579,6 +579,14 @@ void RouteWalker::check_banners(LegWalk& walk){ const std::string banner_text = quoted_list(walk.banners); m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ + // A scene that starts as we cross the edge covers the banner (r19: Rowan + // and Dawn on Route 201's edge). With no banner read and a dialogue up, + // let the next step, the scene, decide where we are. + VideoSnapshot now = m_stream.video().snapshot(); + if (banner_text.empty() && now && ShortDialogDetector().detect(*now.frame)){ + m_stream.log("[Route] " + walk.label + ": no banner, but a scene started; going on.", COLOR_ORANGE); + return; + } OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + (banner_text.empty() ? "nothing" : banner_text) + ".", m_stream); From b95836c411458c4bb039ea3d6fe2414955a4f7d9 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 18:22:46 -0800 Subject: [PATCH 16/39] BDSP walker: wait for the scene's first line before failing on a missing banner Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 8db5d2c782..44f5629512 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -581,9 +581,10 @@ void RouteWalker::check_banners(LegWalk& walk){ if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ // A scene that starts as we cross the edge covers the banner (r19: Rowan // and Dawn on Route 201's edge). With no banner read and a dialogue up, - // let the next step, the scene, decide where we are. - VideoSnapshot now = m_stream.video().snapshot(); - if (banner_text.empty() && now && ShortDialogDetector().detect(*now.frame)){ + // let the next step, the scene, decide where we are. The scene may still + // be walking its people in (r20), so give its first line a few seconds. + ShortDialogWatcher scene_line(COLOR_YELLOW); + if (banner_text.empty() && wait_until(m_stream, m_context, 6000ms, {{scene_line}}) == 0){ m_stream.log("[Route] " + walk.label + ": no banner, but a scene started; going on.", COLOR_ORANGE); return; } From 608a99c78b9afe0e24307dfb67a807f02bcff961 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 18:26:53 -0800 Subject: [PATCH 17/39] BDSP walker: no banner read is a warning; only a wrong banner fails Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 44f5629512..fdc58baed1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -579,13 +579,12 @@ void RouteWalker::check_banners(LegWalk& walk){ const std::string banner_text = quoted_list(walk.banners); m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ - // A scene that starts as we cross the edge covers the banner (r19: Rowan - // and Dawn on Route 201's edge). With no banner read and a dialogue up, - // let the next step, the scene, decide where we are. The scene may still - // be walking its people in (r20), so give its first line a few seconds. - ShortDialogWatcher scene_line(COLOR_YELLOW); - if (banner_text.empty() && wait_until(m_stream, m_context, 6000ms, {{scene_line}}) == 0){ - m_stream.log("[Route] " + walk.label + ": no banner, but a scene started; going on.", COLOR_ORANGE); + // No banner read at all is weak evidence: a scene can cover it (r19), and + // the Lakefront-to-Route 201 crossing showed none we could read (r20, r21). + // Go on and let the next step check where we are; a banner for another + // map still fails. + if (banner_text.empty()){ + m_stream.log("[Route] " + walk.label + ": no banner read for " + walk.map_after + "; going on.", COLOR_ORANGE); return; } OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, From 31816e59e910c297e5032ffac1006e54c001f4f8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 18:30:37 -0800 Subject: [PATCH 18/39] BDSP walker: arriving with no banner read at all is a warning too Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index fdc58baed1..001f1455d4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -345,7 +345,11 @@ std::vector RouteWalker::arrive() const{ const bool seen = std::any_of(m_all_banners.begin(), m_all_banners.end(), [&](const std::string& read){ return location_banner_matches(read, map_end); }); - if (changed_map && !seen){ + // No banner read at all is only a warning (r20-r22: none we could read on + // the Lakefront-to-Route 201 crossing); the next step checks where we are. + if (changed_map && m_all_banners.empty()){ + m_stream.log("[Route] " + m_plan.label + ": no banner read for " + map_end + "; going on.", COLOR_ORANGE); + }else if (changed_map && !seen){ const std::string reads = quoted_list(m_all_banners); OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + From d839fb9475cd3dec8f10dc94cb98a71588f0d48d Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 19:39:25 -0800 Subject: [PATCH 19/39] BDSP walker: time wall legs at the plan's pace, walk or run B held with the D-pad runs once the player has the Running Shoes: r27 ran 35 tiles north in about 5.75 s, and the walking count put the stop 11 tiles short of the wall. Legs that reach their wall log the pace. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 14 +++++++--- .../PokemonBDSP_AutoStory_PlannedRoute.h | 3 +++ .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 5 +++- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 26 +++++++++++++------ 4 files changed, 36 insertions(+), 12 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 001f1455d4..3ce37c13c3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -150,8 +150,10 @@ std::string quoted_list(const std::vector& reads){ // Long enough to reach the wall from the farthest tile, with room for a // slow start; past the wall the player stands still. +// At a run the limit is still timed at a walk: a pace measured too fast +// must not cut a long leg short. Milliseconds wall_limit(const TileLeg& leg){ - const Milliseconds per_tile = route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP; + const Milliseconds per_tile = route_ms_per_tile(leg.direction, MoveSpeed::WALK); return ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; } @@ -385,7 +387,13 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ for (const TilePos& pos : m_from){ nearest = std::min(nearest, slide_to_wall(m_grid, pos, leg.direction).tiles); } - size_t walked_tiles = tiles_moved(walk.result, route_ms_per_tile(leg.direction) / ROUTE_RUN_SPEEDUP); + size_t walked_tiles = tiles_moved(walk.result, route_ms_per_tile(leg.direction, m_plan.speed)); + // Every leg that reaches its wall measures the pace: the tiles are known. + if (walk.result.stop == WalkStop::BLOCKED && nearest != SIZE_MAX && walked_tiles >= nearest){ + m_stream.log("[Speed] " + std::string(m_plan.speed == MoveSpeed::RUN ? "run" : "walk") + " " + + direction_name(leg.direction) + ": " + std::to_string(nearest) + " tiles in " + + std::to_string(walk.result.walked.count()) + " ms to the wall."); + } walked_tiles = unstick_wall_leg(walk, walked_tiles, nearest); // Any stop short of the wall's distance: someone who stays put is // talked to, and the leg is planned again from where we are. Within the @@ -409,7 +417,7 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ // A walk to the wall that stopped well short of it: tap through a corner, // then wait for someone in the way. Returns the tiles walked by then. size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest){ - const Milliseconds per_tile = route_ms_per_tile(walk.planned.leg.direction) / ROUTE_RUN_SPEEDUP; + const Milliseconds per_tile = route_ms_per_tile(walk.planned.leg.direction, m_plan.speed); const Milliseconds limit = wall_limit(walk.planned.leg); // A stick walk can catch a corner when we stand part of a tile off // centre, where a D-pad tap, which moves whole tiles, gets through: on diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h index 845b8977d7..8577d82743 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -19,6 +19,7 @@ #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Walk.h" namespace PokemonAutomation{ @@ -46,6 +47,8 @@ struct RouteWalkPlan{ std::string label; std::vector avoid = {}; RobustPlanCosts costs = route_plan_costs(); + // The pace of legs to a wall, which their tile counts are timed by. + MoveSpeed speed = MoveSpeed::WALK; }; // Walks from any of the world tiles `from` of `maps` until every tile the diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index 803019b456..6af44d9591 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -41,7 +41,10 @@ bool across(Direction direction){ } -Milliseconds route_ms_per_tile(Direction direction){ +Milliseconds route_ms_per_tile(Direction direction, MoveSpeed speed){ + if (speed == MoveSpeed::RUN){ + return across(direction) ? ROUTE_RUN_MS_PER_TILE_ACROSS : ROUTE_RUN_MS_PER_TILE_UP_DOWN; + } return across(direction) ? ROUTE_MS_PER_TILE_ACROSS : ROUTE_MS_PER_TILE_UP_DOWN; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index 97bda06d6e..b686d72e77 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -33,14 +33,24 @@ constexpr Milliseconds ROUTE_MS_PER_TILE_ACROSS = Milliseconds(160); // east constexpr Milliseconds ROUTE_MS_PER_TILE_UP_DOWN = Milliseconds(240); // north or south // From pressing the stick to the first step, when standing still. constexpr Milliseconds ROUTE_START_DELAY = Milliseconds(250); -// Running (B held) covers a tile in this fraction of the walking time. B -// held with the D-pad did not run: r238a walked the Ravaged Path's corridor -// at 160 ms a tile, and a limit set for twice that speed cut a wall leg -// short by two tiles. 1 until running is measured to work. -constexpr int ROUTE_RUN_SPEEDUP = 1; - -// Walking time per tile in `direction`. -Milliseconds route_ms_per_tile(Pokemon::Direction direction); +// How fast the player moves on a leg to a wall (B held with the D-pad). +// B only runs once Mom has given the Running Shoes (00.03); before that +// it walks. The bike comes later, with its own measured times. +enum class MoveSpeed{ + WALK, + RUN, +}; + +// Running time per tile, measured on the console: r27 ran Twinleaf's +// column 17 north onto Route 201, 35 tiles in about 5.75 s (164 ms a +// tile) where walking took 240. Across is scaled the same until measured. +// Rounded down: a count from too fast a pace runs over, which a wall leg +// forgives, where one from too slow a pace stops short of the wall. +constexpr Milliseconds ROUTE_RUN_MS_PER_TILE_ACROSS = Milliseconds(100); +constexpr Milliseconds ROUTE_RUN_MS_PER_TILE_UP_DOWN = Milliseconds(150); + +// Time per tile in `direction` at `speed`. +Milliseconds route_ms_per_tile(Pokemon::Direction direction, MoveSpeed speed = MoveSpeed::WALK); // The left stick for a direction. Row 0 is the top of a map, and the // stick's y is positive upward. JoystickPosition route_stick(Pokemon::Direction direction); From 1ce3bb14117dbf49610f774b87070893baea10c1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 20:40:51 -0800 Subject: [PATCH 20/39] BDSP walker: watch for the new map's banner a while after the last leg Sandgem's banner showed about a second after the walk ended (r35). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 3ce37c13c3..f21131a8d9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -344,12 +344,23 @@ LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t c std::vector RouteWalker::arrive() const{ const std::string map_end = map_of(m_maps, m_from); const bool changed_map = !m_map_start.empty() && !map_end.empty() && map_end != m_map_start; - const bool seen = std::any_of(m_all_banners.begin(), m_all_banners.end(), [&](const std::string& read){ + bool seen = std::any_of(m_all_banners.begin(), m_all_banners.end(), [&](const std::string& read){ return location_banner_matches(read, map_end); }); + // The banner can come up after the last leg ends: Sandgem's showed about + // a second later (r35). Watch for it a while before judging. + for (const WallClock end = current_time() + 3000ms; changed_map && !seen && current_time() < end;){ + m_context.wait_for_all_requests(); + if (const VideoSnapshot screen = m_stream.video().snapshot()){ + seen = location_banner_matches(read_location_banner(*screen.frame), map_end); + } + if (!seen){ + m_context.wait_for(250ms); + } + } // No banner read at all is only a warning (r20-r22: none we could read on // the Lakefront-to-Route 201 crossing); the next step checks where we are. - if (changed_map && m_all_banners.empty()){ + if (changed_map && !seen && m_all_banners.empty()){ m_stream.log("[Route] " + m_plan.label + ": no banner read for " + map_end + "; going on.", COLOR_ORANGE); }else if (changed_map && !seen){ const std::string reads = quoted_list(m_all_banners); From ec50ad2ebbf5a028195c6b2fcf651ab0638383ee Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 22:47:29 -0800 Subject: [PATCH 21/39] BDSP AutoStory walk: turn with a 50 ms D-pad tap A 100 ms stick tap stepped about 0.6 tile off the grid (r49: a tile west of Sandgem's Center door). 40 to 80 ms D-pad taps only turned, on the console. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 6 +++++- .../AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h | 9 ++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index 6af44d9591..bfc9160262 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -81,7 +81,11 @@ Milliseconds route_open_hold(Direction direction, size_t tiles){ void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); - pbf_move_left_joystick(context, route_stick(direction), ROUTE_TURN_TAP, 300ms); + const DpadPosition dpad = + direction == Direction::NORTH ? DPAD_UP : + direction == Direction::SOUTH ? DPAD_DOWN : + direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + pbf_press_dpad(context, dpad, ROUTE_TURN_TAP, 300ms); context.wait_for_all_requests(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index b686d72e77..1568e5ae26 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -61,9 +61,12 @@ JoystickPosition route_stick(Pokemon::Direction direction); // 800ms hold south 4: the player starts at once and finishes the step. Milliseconds route_open_hold(Pokemon::Direction direction, size_t tiles); -// A tap this short turns a player who faces another way, without a step: on -// the console a 150ms tap south after walking north only turned the player. -constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(100); +// A D-pad tap this short only turns, whichever way the player faced: on the +// console 40, 50, 60 and 80 ms taps each turned without a step. A 100 ms +// stick tap stepped about 0.6 tile, off the tile grid, and a 160 ms D-pad +// tap steps a whole tile (r49: a stick "turn" put us a tile west of +// Sandgem's Center door; r248a). +constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(50); // Turns the player to face `direction` without moving. Only for a player who // faces another way: one already facing it may take a step. From ac97a4d0822210334deb1f1a83a06cf71bdcba53 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:17:02 -0800 Subject: [PATCH 22/39] BDSP AutoStory walk: after a trainer on a wall leg, judge the walk on Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index f21131a8d9..4e4b303185 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -483,7 +483,7 @@ LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ } void RouteWalker::check_at_wall(LegWalk& walk){ - const WalkResult check = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, 2000ms, + WalkResult check = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, 2000ms, walk.label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); if (check.stop == WalkStop::DIALOG){ // A trainer who saw us froze the walk (the "!") until its time ran @@ -497,6 +497,10 @@ void RouteWalker::check_at_wall(LegWalk& walk){ m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), walk.label + ": on to the wall", run_watching(walk)); + // Where the walk on stopped is what counts now, not the line that + // stopped the check (r54: at the wall after Route 202's second + // trainer, and failed on the "dialog"). + check = walk.result; } if (check.stop != WalkStop::BLOCKED && !(check.stop == WalkStop::LIMIT && holding_against_wall(m_stream, m_context, walk.stick.x, walk.stick.y, walk.label + ": at the wall?")) From b6990c88bdfd9725e8f46ab6bee06bb0fc3cb42c Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 23:35:05 -0800 Subject: [PATCH 23/39] BDSP AutoStory walk: after a trainer on an open leg, plan from the taps and the sight line Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 4e4b303185..a7a8a9f711 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -737,7 +737,12 @@ LegNext RouteWalker::known_trainer(LegWalk& walk, const std::vector Date: Mon, 2 Nov 2026 00:02:49 -0800 Subject: [PATCH 24/39] BDSP AutoStory walk: turn an open leg with a tap too short to step The 160 ms turn tap's step was guessed from the screen and counted a tile it had not walked (r60: a column off below Jubilife's Center). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index a7a8a9f711..0a061b3184 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -564,21 +564,15 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ return LegNext::NEXT_LEG; } -// The first tap of a leg was meant to turn, but a 160 ms D-pad tap turns -// and steps whichever way the player faced (r248a: all seven did; r246a: -// "south 1" out of the cave went two tiles, onto the Youngster's row), so -// the tap is measured like any other and a step counts for the leg. +// Turns to the leg's way with a tap too short to step (50 ms: on the +// console 40 to 80 ms taps only turned). The leg's taps then each move a +// tile. The old 160 ms "turn" tap stepped whichever way we faced, and +// guessing from the screen whether it had was wrong both ways (r248a +// stepped; r60 only turned but counted a tile, a column off below +// Jubilife's Center). void RouteWalker::turn_tap(LegWalk& walk){ - const DpadPosition dpad = walk.stick.x < 0 ? DPAD_LEFT : walk.stick.x > 0 ? DPAD_RIGHT : walk.stick.y > 0 ? DPAD_UP : DPAD_DOWN; - m_context.wait_for_all_requests(); - VideoSnapshot before = m_stream.video().snapshot(); - pbf_press_dpad(m_context, dpad, 160ms, 700ms); - m_context.wait_for_all_requests(); - VideoSnapshot after = m_stream.video().snapshot(); - if (before && after && walk_screen_change(*before.frame, *after.frame) > WALK_TAP_BLOCKED_CHANGE){ - walk.turn_moved = 1; - m_stream.log("[Route] " + walk.label + ": the turn tap moved a tile (already facing that way); it counts.", COLOR_ORANGE); - } + route_turn(m_stream, m_context, walk.planned.leg.direction); + walk.turn_moved = 0; } LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ From 433501b6e3f7fd3ccfb4c6ce3bc01018099f6a7f Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:10:48 -0800 Subject: [PATCH 25/39] BDSP AutoStory walk: a straight stick walk goes on the D-pad The stick stops between tiles (a 100 ms tap moved ~0.6 tile), and walks after stick holds ran a column off (r57 to r61). Diagonals stay on the stick. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index e0e3104d80..f4066a9ed7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -65,6 +65,17 @@ VideoSnapshot snapshot_or_fail(VideoStream& stream, const std::string& label){ return frame; } +// A stick straight north, south, east or west goes on the D-pad: the D-pad +// moves whole tiles, the stick stops between them (on the console a 100 ms +// stick tap moved ~0.6 tile; r57 to r61 walked a column off after stick +// holds). Diagonals stay on the stick. +bool straight(JoystickPosition stick){ + return (stick.x == 0) != (stick.y == 0); +} +DpadPosition dpad_toward(JoystickPosition stick){ + return stick.x < 0 ? DPAD_LEFT : stick.x > 0 ? DPAD_RIGHT : stick.y > 0 ? DPAD_UP : DPAD_DOWN; +} + // After a black screen: a wild battle if a battle screen shows up, else a transition. bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context){ BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); @@ -388,7 +399,7 @@ WalkResult walk_until( ){ return walk_until_spec(stream, context, escape, { .stick = stick, .limit = limit, .label = label, .arrived = arrived, - .input = WalkInput::STICK, .wild = WildPolicy::RUN_FROM, + .input = straight(stick) ? WalkInput::DPAD : WalkInput::STICK, .wild = WildPolicy::RUN_FROM, }); } @@ -443,7 +454,11 @@ WalkResult walk_for( const int ret = run_until( stream, context, [&](ProControllerContext& context){ - pbf_move_left_joystick(context, stick, duration, 0ms); + if (straight(stick)){ + pbf_press_dpad(context, dpad_toward(stick), duration, 0ms); + }else{ + pbf_move_left_joystick(context, stick, duration, 0ms); + } }, {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} ); From ac5f55d63546708fe382688275fb71899b89d51e Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:24:57 -0800 Subject: [PATCH 26/39] BDSP AutoStory walk: put the route walker's console steps behind RouteIO No behaviour change. RouteWalker (walk_route(), talk_to_npc()) now does everything on the console through a RouteIO: walks to a wall, D-pad tap walks, presses and waits, the dialogue watch, the at-the-wall holding check, screen looks and its log and failures. ConsoleRouteIO is today's code: holding_against_wall() moved unchanged into RouteIO.cpp, and each call site makes the same controller commands, waits and reads as before. walk_route() and talk_to_npc() keep their signatures and build a ConsoleRouteIO; overloads take a RouteIO so tests can script the console. route_turn() and the walker's taps, A presses and waits are now a RouteInput (kind, direction, hold, release, count, whether to wait for them), pressed by press_route_input(); route_turn_tap() is the D-pad tap route_turn() turns with. Review with: git diff --color-moved=dimmed-zebra --color-moved-ws=allow-indentation-change HEAD~1 HEAD Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 287 ++++++++---------- .../PokemonBDSP_AutoStory_PlannedRoute.h | 11 + .../Utils/PokemonBDSP_AutoStory_RouteIO.cpp | 101 ++++++ .../Utils/PokemonBDSP_AutoStory_RouteIO.h | 103 +++++++ .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 38 ++- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 28 ++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 7 files changed, 411 insertions(+), 159 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 0a061b3184..bbeafd4c0e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -7,17 +7,13 @@ #include #include #include +#include "Common/Cpp/Exceptions.h" #include "Common/Cpp/PrettyPrint.h" #include "Common/Cpp/Filesystem/Filesystem.h" #include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/VideoPipeline/VideoFeed.h" -#include "CommonTools/Async/InferenceRoutines.h" -#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" #include "Pokemon/Navigation/Pokemon_RouteLog.h" -#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP_AutoStory_RouteIO.h" #include "PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_PlannedRoute.h" @@ -55,21 +51,21 @@ PlannedRouteLeg planned_route_leg(const StoryWorld& maps, const PlannedLeg& leg) return {.leg = route_log_leg(leg.leg), .after = route_tiles(maps, leg.after)}; } -void log_plan(VideoStream& stream, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ +void log_plan(RouteIO& io, const StoryWorld& maps, const std::string& label, const std::vector& from, const std::vector& legs){ std::vector logged; for (const PlannedLeg& leg : legs){ logged.push_back(planned_route_leg(maps, leg)); } - stream.log(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged)); + io.note(ROUTE_LOG_TAG + route_plan_line(label, route_tiles(maps, from), logged), RouteNote::LOG); } // Logs a leg that ended on its planned tiles, and how that is known: an // open leg counts its taps through a wild battle, so its end is assumed; a // leg to a wall is walked again to its wall. -void log_leg_done(VideoStream& stream, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ +void log_leg_done(RouteIO& io, const StoryWorld& maps, const std::string& label, const PlannedLeg& planned, size_t wild_battles){ const bool assumed = !planned.leg.ends_at_wall && wild_battles > 0; - stream.log(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), - assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : "")); + io.note(ROUTE_LOG_TAG + route_leg_end_line(label, route_log_leg(planned.leg), route_tiles(maps, planned.after), + assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : ""), RouteNote::LOG); } // The tiles the player passes when walking `tiles` tiles from `from`, the @@ -89,30 +85,6 @@ std::vector leg_path(const StoryWorld& grid, TilePos from, Direction di return path; } -// Holding toward a wall with people or Pokemon moving beside us never reads -// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking -// scrolls the view far more (35 to 90). True if holding toward (x, y) for -// 1.6 s changes the screen less than walking would: we stand at a wall. -bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ - constexpr double WALKING_CHANGE = 25.0; - context.wait_for_all_requests(); - VideoSnapshot before = stream.video().snapshot(); - double most = 0; - for (size_t chunk = 0; chunk < 4 && before; chunk++){ - pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); - context.wait_for_all_requests(); - VideoSnapshot after = stream.video().snapshot(); - if (!after){ - return false; - } - most = std::max(most, walk_screen_change(*before.frame, *after.frame)); - before = std::move(after); - } - stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + - (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); - return before && most < WALKING_CHANGE; -} - // The map all of `tiles` are on, or empty if they are on several or none. std::string map_of(const StoryWorld& maps, const std::vector& tiles){ std::string name; @@ -157,6 +129,28 @@ Milliseconds wall_limit(const TileLeg& leg){ return ROUTE_START_DELAY + per_tile * leg.tiles * 5 / 4 + 1000ms; } +// Fails the program with `message` (RouteNote::FAILURE). +[[noreturn]] void fail(RouteIO& io, const std::string& message){ + io.note(message, RouteNote::FAILURE); + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "RouteIO::note() returned from a failure: " + message); +} + +// route_turn() through `io`. +void turn(RouteIO& io, Direction direction){ + io.note(std::string("[Route] Turning ") + direction_name(direction) + ".", RouteNote::LOG); + io.press(route_turn_tap(direction)); +} + +// A press of A, to talk to someone in front of us. +RouteInput press_a(RouteInputEnd end){ + return {.kind = RouteInputKind::BUTTON_A, .direction = Direction::NORTH, .hold = 80ms, .release = 200ms, + .count = 1, .end = end}; +} + +// A wait for someone walking about to get out of the way. +constexpr RouteInput WAIT_FOR_THE_WAY{.kind = RouteInputKind::WAIT, .direction = Direction::NORTH, .hold = 2500ms, + .release = 0ms, .count = 1, .end = RouteInputEnd::DONE}; + // Tiles a walk to the wall moved, from the time it walked. size_t tiles_moved(const WalkResult& walk, Milliseconds per_tile){ const Milliseconds moving = walk.walked - ROUTE_START_DELAY - @@ -175,7 +169,6 @@ enum class LegNext{ struct LegWalk{ const PlannedLeg& planned; std::string label; - JoystickPosition stick; std::string map_before; std::string map_after; std::vector banners = {}; @@ -212,13 +205,8 @@ DpadWalkOptions run_watching(LegWalk& walk){ // turned out to be when something stopped it. class RouteWalker{ public: - RouteWalker( - VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, - const StoryWorld& maps, RouteTrainers& trainers, const RouteWalkPlan& plan - ) - : m_stream(stream) - , m_context(context) - , m_escape(escape) + RouteWalker(RouteIO& io, const StoryWorld& maps, RouteTrainers& trainers, const RouteWalkPlan& plan) + : m_io(io) , m_maps(maps) , m_grid(maps) , m_trainers(trainers) @@ -255,9 +243,7 @@ class RouteWalker{ LegNext replan_from(const LegWalk& walk, std::vector stopped, const std::string& why); void mark_blocker(const LegWalk& walk, const std::vector& stopped); - VideoStream& m_stream; - ProControllerContext& m_context; - const WildBattleEscape& m_escape; + RouteIO& m_io; const StoryWorld& m_maps; // Our own copy, so things found in the way can be added to it. StoryWorld m_grid; @@ -286,8 +272,8 @@ std::vector RouteWalker::run(std::vector from){ m_map_start = map_of(m_maps, m_from); for (size_t plans = 0; plans < 8; plans++){ const std::vector legs = plan_legs(); - route_status(m_stream, m_plan.label + ": " + std::to_string(legs.size()) + " legs planned"); - log_plan(m_stream, m_maps, m_plan.label, m_from, legs); + m_io.note(m_plan.label + ": " + std::to_string(legs.size()) + " legs planned", RouteNote::STATUS); + log_plan(m_io, m_maps, m_plan.label, m_from, legs); bool replanned = false; for (size_t number = 1; number <= legs.size() && !replanned; number++){ replanned = walk_leg(legs[number - 1], number, legs.size()) == LegNext::REPLAN; @@ -296,8 +282,7 @@ std::vector RouteWalker::run(std::vector from){ return arrive(); } } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": too many plans.", m_stream); + fail(m_io, m_plan.label + ": too many plans."); } std::vector RouteWalker::plan_legs() const{ @@ -306,23 +291,21 @@ std::vector RouteWalker::plan_legs() const{ // From a few possible tiles the exact-open-leg costs found no // plan where the default ones did (r240a, three tiles on Route // 204's row 27); the default plan is longer but sound. - m_stream.log("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + - "; trying the default costs.", COLOR_ORANGE); + m_io.note("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + + "; trying the default costs.", RouteNote::WARNING); legs = plan_robust_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); } if (!legs){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": no plan from " + describe(m_maps, m_from) + ".", m_stream); + fail(m_io, m_plan.label + ": no plan from " + describe(m_maps, m_from) + "."); } return std::move(*legs); } LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t count){ - route_status(m_stream, route_leg_line(number, count, planned_route_leg(m_maps, planned))); + m_io.note(route_leg_line(number, count, planned_route_leg(m_maps, planned)), RouteNote::STATUS); LegWalk walk{ .planned = planned, .label = m_plan.label + ": " + to_string(planned.leg), - .stick = route_stick(planned.leg.direction), .map_before = map_of(m_maps, m_from), .map_after = map_of(m_maps, planned.after), }; @@ -333,8 +316,7 @@ LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t c m_facing = planned.leg.direction; check_banners(walk); if (walk.result.stop == WalkStop::TRANSITION){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": unexpected transition.", m_stream); + fail(m_io, walk.label + ": unexpected transition."); } return after_leg(walk); } @@ -350,25 +332,24 @@ std::vector RouteWalker::arrive() const{ // The banner can come up after the last leg ends: Sandgem's showed about // a second later (r35). Watch for it a while before judging. for (const WallClock end = current_time() + 3000ms; changed_map && !seen && current_time() < end;){ - m_context.wait_for_all_requests(); - if (const VideoSnapshot screen = m_stream.video().snapshot()){ - seen = location_banner_matches(read_location_banner(*screen.frame), map_end); - } + m_io.look([&](const ImageViewRGB32& screen){ + seen = location_banner_matches(read_location_banner(screen), map_end); + }); if (!seen){ - m_context.wait_for(250ms); + m_io.press({.kind = RouteInputKind::PAUSE, .direction = Direction::NORTH, .hold = 250ms, .release = 0ms, + .count = 1, .end = RouteInputEnd::QUEUED}); } } // No banner read at all is only a warning (r20-r22: none we could read on // the Lakefront-to-Route 201 crossing); the next step checks where we are. if (changed_map && !seen && m_all_banners.empty()){ - m_stream.log("[Route] " + m_plan.label + ": no banner read for " + map_end + "; going on.", COLOR_ORANGE); + m_io.note("[Route] " + m_plan.label + ": no banner read for " + map_end + "; going on.", RouteNote::WARNING); }else if (changed_map && !seen){ const std::string reads = quoted_list(m_all_banners); - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + - "'s location banner never showed (read " + (reads.empty() ? "nothing" : reads) + ").", m_stream); + fail(m_io, m_plan.label + ": arrived at " + describe(m_maps, m_from) + " on the map, but " + map_end + + "'s location banner never showed (read " + (reads.empty() ? "nothing" : reads) + ")."); } - m_stream.log(ROUTE_LOG_TAG + route_arrived_line(m_plan.label, route_tiles(m_maps, m_from))); + m_io.note(ROUTE_LOG_TAG + route_arrived_line(m_plan.label, route_tiles(m_maps, m_from)), RouteNote::LOG); return m_from; } @@ -378,15 +359,15 @@ std::vector RouteWalker::arrive() const{ LegNext RouteWalker::wall_leg(LegWalk& walk){ const TileLeg& leg = walk.planned.leg; const Milliseconds limit = wall_limit(leg); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); // A wild battle may end the walk early. Walking to a wall again ends in // the same place, so finish the leg. const bool interrupted = walk.result.wild_battles > 0; for (size_t again = 0; again < 3 && walk.result.wild_battles > 0 && (walk.result.stop == WalkStop::LIMIT || walk.result.stop == WalkStop::BLOCKED); again++ ){ - m_stream.log("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again."); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + m_io.note("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again.", RouteNote::LOG); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); } if (interrupted){ return LegNext::NEXT_LEG; @@ -401,9 +382,9 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ size_t walked_tiles = tiles_moved(walk.result, route_ms_per_tile(leg.direction, m_plan.speed)); // Every leg that reaches its wall measures the pace: the tiles are known. if (walk.result.stop == WalkStop::BLOCKED && nearest != SIZE_MAX && walked_tiles >= nearest){ - m_stream.log("[Speed] " + std::string(m_plan.speed == MoveSpeed::RUN ? "run" : "walk") + " " + + m_io.note("[Speed] " + std::string(m_plan.speed == MoveSpeed::RUN ? "run" : "walk") + " " + direction_name(leg.direction) + ": " + std::to_string(nearest) + " tiles in " + - std::to_string(walk.result.walked.count()) + " ms to the wall."); + std::to_string(walk.result.walked.count()) + " ms to the wall.", RouteNote::LOG); } walked_tiles = unstick_wall_leg(walk, walked_tiles, nearest); // Any stop short of the wall's distance: someone who stays put is @@ -435,28 +416,20 @@ size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t // Route 203's stairs the walk west stopped nine times in a row and two // taps crossed (r225a, 2026-10-31). Tap twice, then walk on to the wall. if (walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest){ - const Direction direction = walk.planned.leg.direction; - const DpadPosition dpad = - direction == Direction::NORTH ? DPAD_UP : - direction == Direction::SOUTH ? DPAD_DOWN : - direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; two D-pad taps, then on to the wall.", COLOR_ORANGE); - for (size_t tap = 0; tap < 2; tap++){ - pbf_press_dpad(m_context, dpad, 160ms, 640ms); - } - m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + m_io.note("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; two D-pad taps, then on to the wall.", RouteNote::WARNING); + m_io.press({.kind = RouteInputKind::DPAD, .direction = walk.planned.leg.direction, .hold = 160ms, .release = 640ms, + .count = 2, .end = RouteInputEnd::DONE}); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); walked_tiles += tiles_moved(walk.result, per_tile); } // Someone walking about may stand in the way for a moment: wait and // walk to the wall again before calling it an obstacle. for (size_t again = 0; again < 2 && walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++){ - m_stream.log("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + - " tiles, short of the wall; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(m_context, 2500ms); - m_context.wait_for_all_requests(); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, limit, walk.label, run_watching(walk)); + m_io.note("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + + " tiles, short of the wall; waiting for the way to clear.", RouteNote::WARNING); + m_io.press(WAIT_FOR_THE_WAY); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); walked_tiles += tiles_moved(walk.result, per_tile); } return walked_tiles; @@ -475,16 +448,15 @@ LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ } stopped = sorted_unique(std::move(stopped)); if (stopped.empty()){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": stopped early, and no tile fits.", m_stream); + fail(m_io, walk.label + ": stopped early, and no tile fits."); } mark_blocker(walk, stopped); return replan_from(walk, std::move(stopped), "stopped early after about " + std::to_string(walked_tiles) + " tiles"); } void RouteWalker::check_at_wall(LegWalk& walk){ - WalkResult check = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, 2000ms, - walk.label + ": at the wall?", {.arrived = nullptr, .pace = DpadPace::RUN}); + WalkResult check = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = 2000ms, + .label = walk.label + ": at the wall?", .options = {.arrived = nullptr, .pace = DpadPace::RUN}}); if (check.stop == WalkStop::DIALOG){ // A trainer who saw us froze the walk (the "!") until its time ran // out, then walked over (r241a: Aroma Lady Taylor's line crosses @@ -492,33 +464,32 @@ void RouteWalker::check_at_wall(LegWalk& walk){ // they saw us from is not reliable (BDSP's trainers stand a few // tiles from Platinum's, r242a), and a leg to a wall ends at its // wall however far it got. - route_status(m_stream, walk.label + ": a trainer stopped us on the way; battling."); + m_io.note(walk.label + ": a trainer stopped us on the way; battling.", RouteNote::STATUS); m_trainers.fight("a trainer on the way"); - m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk.result = walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), - walk.label + ": on to the wall", run_watching(walk)); + m_io.note("[Route] " + walk.label + ": on to the wall after the battle.", RouteNote::WARNING); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = wall_limit(walk.planned.leg), + .label = walk.label + ": on to the wall", .options = run_watching(walk)}); // Where the walk on stopped is what counts now, not the line that // stopped the check (r54: at the wall after Route 202's second // trainer, and failed on the "dialog"). check = walk.result; } if (check.stop != WalkStop::BLOCKED && - !(check.stop == WalkStop::LIMIT && holding_against_wall(m_stream, m_context, walk.stick.x, walk.stick.y, walk.label + ": at the wall?")) + !(check.stop == WalkStop::LIMIT && m_io.holding_against_wall(walk.planned.leg.direction, walk.label + ": at the wall?")) ){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": ran out of time short of the wall, and walking on gets " + - walk_stop_name(check.stop) + ": not where the plan has us.", m_stream); + fail(m_io, walk.label + ": ran out of time short of the wall, and walking on gets " + + walk_stop_name(check.stop) + ": not where the plan has us."); } } // After a battle on a leg to a wall: a leg to a wall ends at its wall // however far it got, so walk on to it and carry on. void RouteWalker::on_to_wall(LegWalk& walk){ - m_stream.log("[Route] " + walk.label + ": on to the wall after the battle.", COLOR_ORANGE); - walk_until_dpad(m_stream, m_context, m_escape, walk.stick, wall_limit(walk.planned.leg), - walk.label + ": on to the wall", run_watching(walk)); + m_io.note("[Route] " + walk.label + ": on to the wall after the battle.", RouteNote::WARNING); + m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = wall_limit(walk.planned.leg), + .label = walk.label + ": on to the wall", .options = run_watching(walk)}); m_from = walk.planned.after; - log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, 0); + log_leg_done(m_io, m_maps, m_plan.label, walk.planned, 0); } @@ -531,13 +502,13 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ turn_tap(walk); } if (leg.tiles > walk.turn_moved){ - walk.result = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - walk.turn_moved, walk.label); + walk.result = m_io.walk_taps(leg.direction, leg.tiles - walk.turn_moved, walk.label); } // A wild battle starts once the step onto the grass is done, so the // taps still count; the walk went on after it. if (walk.result.wild_battles > 0){ - m_stream.log("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + - " wild battle(s) on the way; the tap count holds.", COLOR_ORANGE); + m_io.note("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + + " wild battle(s) on the way; the tap count holds.", RouteNote::WARNING); } // Taps that moved nothing: someone stands in the way (the Youngster // below the Ravaged Path wanders across the row under the cliff, r239a, @@ -548,11 +519,10 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ // which the taps give exactly. size_t moved = std::min(leg.tiles, walk.result.moved_chunks + walk.turn_moved); if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ - m_stream.log("[Route] " + walk.label + ": blocked after " + std::to_string(moved) + " of " + - std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", COLOR_ORANGE); - pbf_wait(m_context, 2500ms); - m_context.wait_for_all_requests(); - const WalkResult rest = walk_tiles_dpad(m_stream, m_context, m_escape, walk.stick, leg.tiles - moved, walk.label + ": the rest"); + m_io.note("[Route] " + walk.label + ": blocked after " + std::to_string(moved) + " of " + + std::to_string(leg.tiles) + " tiles; waiting for the way to clear.", RouteNote::WARNING); + m_io.press(WAIT_FOR_THE_WAY); + const WalkResult rest = m_io.walk_taps(leg.direction, leg.tiles - moved, walk.label + ": the rest"); moved = std::min(leg.tiles, moved + rest.moved_chunks); walk.result.stop = rest.stop; walk.result.walked += rest.walked; @@ -571,7 +541,7 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ // stepped; r60 only turned but counted a tile, a column off below // Jubilife's Center). void RouteWalker::turn_tap(LegWalk& walk){ - route_turn(m_stream, m_context, walk.planned.leg.direction); + turn(m_io, walk.planned.leg.direction); walk.turn_moved = 0; } @@ -593,10 +563,9 @@ LegNext RouteWalker::blocked_open_leg(LegWalk& walk, size_t moved){ // walk did not end where the plan says. From tiles on several maps (a leg // not confirmed) the banner may already have shown. void RouteWalker::check_banners(LegWalk& walk){ - m_context.wait_for_all_requests(); - if (VideoSnapshot screen = m_stream.video().snapshot()){ - watch_banner(walk, *screen.frame); - } + m_io.look([&walk](const ImageViewRGB32& screen){ + watch_banner(walk, screen); + }); const std::string banner_text = quoted_list(walk.banners); m_all_banners.insert(m_all_banners.end(), walk.banners.begin(), walk.banners.end()); if (!walk.map_before.empty() && !walk.map_after.empty() && walk.map_after != walk.map_before && !banner_ok(walk)){ @@ -605,22 +574,21 @@ void RouteWalker::check_banners(LegWalk& walk){ // Go on and let the next step check where we are; a banner for another // map still fails. if (banner_text.empty()){ - m_stream.log("[Route] " + walk.label + ": no banner read for " + walk.map_after + "; going on.", COLOR_ORANGE); + m_io.note("[Route] " + walk.label + ": no banner read for " + walk.map_after + "; going on.", RouteNote::WARNING); return; } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + - (banner_text.empty() ? "nothing" : banner_text) + ".", m_stream); + fail(m_io, walk.label + ": expected to enter " + walk.map_after + ", but the location banner read " + + (banner_text.empty() ? "nothing" : banner_text) + "."); } if (!banner_text.empty()){ - m_stream.log("[Route] " + walk.label + ": location banner " + banner_text + "."); + m_io.note("[Route] " + walk.label + ": location banner " + banner_text + ".", RouteNote::LOG); } // Where each leg ended, to compare with the plan after a run. - if (VideoSnapshot screen = m_stream.video().snapshot()){ + m_io.look([&walk](const ImageViewRGB32& screen){ const std::string folder = SCREENSHOTS_PATH() + "AutoStory/route/"; Filesystem::create_directories(Filesystem::Path(folder)); - screen.frame->save(folder + now_to_filestring() + "-" + normalize_location_name(walk.label) + ".png"); - } + screen.save(folder + now_to_filestring() + "-" + normalize_location_name(walk.label) + ".png"); + }); } // A trainer who sees us freezes the player and walks over; the dialogue @@ -634,13 +602,12 @@ LegNext RouteWalker::after_leg(LegWalk& walk){ const std::vector watching = watching_trainers(paths); bool talking = walk.result.stop == WalkStop::DIALOG; if (!talking){ - ShortDialogWatcher dialog(COLOR_YELLOW); const Milliseconds wait = watching.empty() ? 500ms : 4000ms; - talking = wait_until(m_stream, m_context, wait, {{dialog}}) == 0; + talking = m_io.dialog_within(wait, COLOR_YELLOW); } if (!talking){ m_from = walk.planned.after; - log_leg_done(m_stream, m_maps, m_plan.label, walk.planned, walk.result.wild_battles); + log_leg_done(m_io, m_maps, m_plan.label, walk.planned, walk.result.wild_battles); return LegNext::NEXT_LEG; } if (watching.empty()){ @@ -677,10 +644,9 @@ LegNext RouteWalker::unknown_trainer(LegWalk& walk, const std::vector 2){ - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - walk.label + ": dialogue the map does not explain stopped us 3 times.", m_stream); + fail(m_io, walk.label + ": dialogue the map does not explain stopped us 3 times."); } - route_status(m_stream, m_plan.label + ": a trainer the map lacks stopped us; fighting."); + m_io.note(m_plan.label + ": a trainer the map lacks stopped us; fighting.", RouteNote::STATUS); m_trainers.fight("unknown trainer"); if (walk.planned.leg.ends_at_wall){ on_to_wall(walk); @@ -707,11 +673,10 @@ LegNext RouteWalker::known_trainer(LegWalk& walk, const std::vector& } const TilePos blocker = stopped[0].next(walk.planned.leg.direction); m_grid.block(blocker); - m_stream.log("[Route] " + walk.label + ": something the map lacks is at " + - describe(m_maps, blocker) + "; marked it.", COLOR_ORANGE); + m_io.note("[Route] " + walk.label + ": something the map lacks is at " + + describe(m_maps, blocker) + "; marked it.", RouteNote::WARNING); } LegNext RouteWalker::replan_from(const LegWalk& walk, std::vector stopped, const std::string& why){ - m_stream.log(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.planned.leg), why, route_tiles(m_maps, stopped)), COLOR_ORANGE); + m_io.note(ROUTE_LOG_TAG + route_replan_line(m_plan.label, route_log_leg(walk.planned.leg), why, route_tiles(m_maps, stopped)), RouteNote::WARNING); m_facing = walk.planned.leg.direction; m_from = std::move(stopped); return LegNext::REPLAN; @@ -778,7 +742,15 @@ std::vector walk_route( const StoryWorld& maps, std::vector from, RouteTrainers& trainers, const RouteWalkPlan& plan ){ - return RouteWalker(stream, context, escape, maps, trainers, plan).run(std::move(from)); + ConsoleRouteIO io(stream, context, escape); + return walk_route(io, maps, std::move(from), trainers, plan); +} + +std::vector walk_route( + RouteIO& io, const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +){ + return RouteWalker(io, maps, trainers, plan).run(std::move(from)); } std::vector walk_waypoints( @@ -803,13 +775,21 @@ void talk_to_npc( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const StoryWorld& maps, std::vector& at, RouteTrainers& trainers, const NpcTalk& talk +){ + ConsoleRouteIO io(stream, context, escape); + talk_to_npc(io, maps, at, trainers, talk); +} + +void talk_to_npc( + RouteIO& io, const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk ){ for (Direction side : talk.sides){ const TilePos stand = talk.npc.next(side); if (!standable(maps, stand)){ continue; } - at = walk_route(stream, context, escape, maps, at, trainers, { + at = walk_route(io, maps, at, trainers, { .goal = [stand](const TilePos& pos){ return pos == stand; }, .label = talk.label, .avoid = {}, @@ -820,18 +800,15 @@ void talk_to_npc( side == Direction::NORTH ? Direction::SOUTH : side == Direction::SOUTH ? Direction::NORTH : side == Direction::EAST ? Direction::WEST : Direction::EAST; - route_turn(stream, context, face); - pbf_press_button(context, BUTTON_A, 80ms, 200ms); - context.wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream, context, 2000ms, {{dialog}}) == 0){ - route_status(stream, talk.label + ": talking from " + describe(maps, stand) + "."); + turn(io, face); + io.press(press_a(RouteInputEnd::DONE)); + if (io.dialog_within(2000ms, COLOR_CYAN)){ + io.note(talk.label + ": talking from " + describe(maps, stand) + ".", RouteNote::STATUS); return; } - route_status(stream, talk.label + ": nobody answered from " + describe(maps, stand) + "; trying another side."); + io.note(talk.label + ": nobody answered from " + describe(maps, stand) + "; trying another side.", RouteNote::STATUS); } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue.", stream); + fail(io, talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue."); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h index 8577d82743..e422d6891c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -19,6 +19,7 @@ #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_PlanCosts.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_RouteIO.h" #include "PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Walk.h" @@ -64,6 +65,11 @@ std::vector walk_route( const StoryWorld& maps, std::vector from, RouteTrainers& trainers, const RouteWalkPlan& plan ); +// walk_route() through `io`. +std::vector walk_route( + RouteIO& io, const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +); // The corners of a line a human walked, and the costs to plan each // stretch with. @@ -101,6 +107,11 @@ void talk_to_npc( const StoryWorld& maps, std::vector& at, RouteTrainers& trainers, const NpcTalk& talk ); +// talk_to_npc() through `io`. +void talk_to_npc( + RouteIO& io, const StoryWorld& maps, std::vector& at, + RouteTrainers& trainers, const NpcTalk& talk +); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp new file mode 100644 index 0000000000..37e6e3b06a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp @@ -0,0 +1,101 @@ +/* BDSP AutoStory - Route IO + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP_AutoStory_RouteIO.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using namespace Pokemon; + + +namespace{ + +// Holding toward a wall with people or Pokemon moving beside us never reads +// as blocked (8 to 16 per 400 ms in the Oreburgh Mine, s03d1), but walking +// scrolls the view far more (35 to 90). True if holding toward (x, y) for +// 1.6 s changes the screen less than walking would: we stand at a wall. +bool holding_against_wall(VideoStream& stream, ProControllerContext& context, double x, double y, const std::string& label){ + constexpr double WALKING_CHANGE = 25.0; + context.wait_for_all_requests(); + VideoSnapshot before = stream.video().snapshot(); + double most = 0; + for (size_t chunk = 0; chunk < 4 && before; chunk++){ + pbf_move_left_joystick(context, {x, y}, 400ms, 0ms); + context.wait_for_all_requests(); + VideoSnapshot after = stream.video().snapshot(); + if (!after){ + return false; + } + most = std::max(most, walk_screen_change(*before.frame, *after.frame)); + before = std::move(after); + } + stream.log("[Walk] " + label + ": most screen change holding on " + std::to_string(most) + + (most < WALKING_CHANGE ? ", less than walking: at the wall." : ", as much as walking: still moving.")); + return before && most < WALKING_CHANGE; +} + +} + + +WalkResult ConsoleRouteIO::walk_to_wall(const RouteWallWalk& walk){ + return walk_until_dpad(m_stream, m_context, m_escape, route_stick(walk.direction), walk.limit, walk.label, walk.options); +} + +WalkResult ConsoleRouteIO::walk_taps(Direction direction, size_t tiles, const std::string& label){ + return walk_tiles_dpad(m_stream, m_context, m_escape, route_stick(direction), tiles, label); +} + +void ConsoleRouteIO::press(const RouteInput& input){ + press_route_input(m_context, input); +} + +bool ConsoleRouteIO::dialog_within(Milliseconds wait, Color color){ + ShortDialogWatcher dialog(color); + return wait_until(m_stream, m_context, wait, {{dialog}}) == 0; +} + +bool ConsoleRouteIO::holding_against_wall(Direction direction, const std::string& label){ + const JoystickPosition stick = route_stick(direction); + return PokemonBDSP::holding_against_wall(m_stream, m_context, stick.x, stick.y, label); +} + +void ConsoleRouteIO::look(const std::function& at){ + m_context.wait_for_all_requests(); + if (VideoSnapshot screen = m_stream.video().snapshot()){ + at(*screen.frame); + } +} + +void ConsoleRouteIO::note(const std::string& text, RouteNote kind){ + switch (kind){ + case RouteNote::LOG: + m_stream.log(text); + return; + case RouteNote::WARNING: + m_stream.log(text, COLOR_ORANGE); + return; + case RouteNote::STATUS: + route_status(m_stream, text); + return; + case RouteNote::FAILURE: + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, text, m_stream); + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h new file mode 100644 index 0000000000..1db4631068 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h @@ -0,0 +1,103 @@ +/* BDSP AutoStory - Route IO + * + * From: https://github.com/PokemonAutomation/ + * + * What the planned route walker (PlannedRoute) does on the console: walks, + * presses, screen reads and its log. The walker's decisions take a RouteIO, + * so tests can run them on a script of console outcomes; ConsoleRouteIO is + * the console. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_RouteIO_H +#define PokemonAutomation_PokemonBDSP_AutoStory_RouteIO_H + +#include +#include +#include "Common/Cpp/Color.h" +#include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// A walk on the D-pad toward a wall: walk_until_dpad() toward `direction`. +struct RouteWallWalk{ + Pokemon::Direction direction; + Milliseconds limit; + std::string label; + DpadWalkOptions options; +}; + +// How a line goes to the log. +enum class RouteNote{ + LOG, + WARNING, // in orange + STATUS, // route_status(): also on the video overlay + FAILURE, // fails the program with a screenshot; does not return +}; + +class RouteIO{ +public: + virtual ~RouteIO() = default; + + // Holds the D-pad toward a wall (walk_until_dpad()). + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) = 0; + + // Taps `tiles` whole tiles toward `direction` (walk_tiles_dpad()). + virtual WalkResult walk_taps(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; + + // Presses or waits (press_route_input()). + virtual void press(const RouteInput& input) = 0; + + // Whether a dialogue box shows within `wait`. + virtual bool dialog_within(Milliseconds wait, Color color) = 0; + + // Holds the stick toward `direction` for 1.6 s, and returns whether the + // screen changed less than walking would: we stand at a wall. + virtual bool holding_against_wall(Pokemon::Direction direction, const std::string& label) = 0; + + // Once the presses so far are done, shows `at` the screen, if there is one. + virtual void look(const std::function& at) = 0; + + // Writes `text` to the log; a FAILURE fails the program. + virtual void note(const std::string& text, RouteNote kind) = 0; +}; + + +// The console: today's walker steps, unchanged. +class ConsoleRouteIO : public RouteIO{ +public: + ConsoleRouteIO(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape) + : m_stream(stream) + , m_context(context) + , m_escape(escape) + {} + + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) override; + virtual WalkResult walk_taps(Pokemon::Direction direction, size_t tiles, const std::string& label) override; + virtual void press(const RouteInput& input) override; + virtual bool dialog_within(Milliseconds wait, Color color) override; + virtual bool holding_against_wall(Pokemon::Direction direction, const std::string& label) override; + virtual void look(const std::function& at) override; + virtual void note(const std::string& text, RouteNote kind) override; + +private: + VideoStream& m_stream; + ProControllerContext& m_context; + const WildBattleEscape& m_escape; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index bfc9160262..cbd3f3c2d4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -79,14 +79,44 @@ Milliseconds route_open_hold(Direction direction, size_t tiles){ return step * tiles - step / 2; } -void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ - stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); +void press_route_input(ProControllerContext& context, const RouteInput& input){ + const Direction direction = input.direction; const DpadPosition dpad = direction == Direction::NORTH ? DPAD_UP : direction == Direction::SOUTH ? DPAD_DOWN : direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - pbf_press_dpad(context, dpad, ROUTE_TURN_TAP, 300ms); - context.wait_for_all_requests(); + for (size_t press = 0; press < input.count; press++){ + switch (input.kind){ + case RouteInputKind::DPAD: + pbf_press_dpad(context, dpad, input.hold, input.release); + break; + case RouteInputKind::STICK: + pbf_move_left_joystick(context, route_stick(direction), input.hold, input.release); + break; + case RouteInputKind::BUTTON_A: + pbf_press_button(context, BUTTON_A, input.hold, input.release); + break; + case RouteInputKind::WAIT: + pbf_wait(context, input.hold); + break; + case RouteInputKind::PAUSE: + context.wait_for(input.hold); + break; + } + } + if (input.end == RouteInputEnd::DONE){ + context.wait_for_all_requests(); + } +} + +RouteInput route_turn_tap(Direction direction){ + return {.kind = RouteInputKind::DPAD, .direction = direction, .hold = ROUTE_TURN_TAP, .release = 300ms, + .count = 1, .end = RouteInputEnd::DONE}; +} + +void route_turn(VideoStream& stream, ProControllerContext& context, Direction direction){ + stream.log(std::string("[Route] Turning ") + direction_name(direction) + "."); + press_route_input(context, route_turn_tap(direction)); } std::optional talk_to_adjacent_npc( diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index 1568e5ae26..ea46031644 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -68,6 +68,34 @@ Milliseconds route_open_hold(Pokemon::Direction direction, size_t tiles); // Sandgem's Center door; r248a). constexpr Milliseconds ROUTE_TURN_TAP = Milliseconds(50); +// What a RouteInput presses. +enum class RouteInputKind{ + DPAD, // the D-pad toward `direction`: whole tiles + STICK, // the left stick toward `direction`: any distance + BUTTON_A, + WAIT, // nothing, for `hold`, after what is already queued + PAUSE, // nothing, for `hold` from now (ControllerContext::wait_for()) +}; +// Whether press_route_input() returns once the presses are done, or at once. +enum class RouteInputEnd{ + DONE, + QUEUED, +}; +// A short press, `count` times: held for `hold`, then let go for `release`. +// `direction` is for DPAD and STICK only. +struct RouteInput{ + RouteInputKind kind; + Pokemon::Direction direction; + Milliseconds hold; + Milliseconds release; + size_t count; + RouteInputEnd end; +}; +void press_route_input(ProControllerContext& context, const RouteInput& input); + +// The tap route_turn() turns with: on the D-pad, ROUTE_TURN_TAP long. +RouteInput route_turn_tap(Pokemon::Direction direction); + // Turns the player to face `direction` without moving. Only for a player who // faces another way: one already facing it may take a step. void route_turn(VideoStream& stream, ProControllerContext& context, Pokemon::Direction direction); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 4c4e69c4f8..801fa55772 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1659,6 +1659,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp From e8a5efc8b32e6ccc49c628700d7b68d8b229f03b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 00:25:04 -0800 Subject: [PATCH 27/39] BDSP AutoStory walk: test the route walker on scripted console outcomes ScriptedRouteIO plays a strict script of console calls and outcomes; a call the script does not expect fails the test. New tests: - A trainer's line freezes the at-the-wall check (DIALOG), the battle runs, and the walk on reaches the wall (BLOCKED): the walker goes on (r54: it judged the stale DIALOG and failed). - Logan (Route 202, (26, 4), looking south) stops an open leg west along row 5 after 3 taps: the replan starts from (27, 5) and (26, 5), and its legs never walk into him from either (r55). - A wall leg out of time whose check also runs out of time counts as at the wall only when the holding check says so. - route_turn() turns with one D-pad tap of at most 80 ms (40 to 80 ms only turned on the console; a 100 ms stick tap stepped 0.6 tile). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + ...kemonBDSP_AutoStory_PlannedRoute_Tests.cpp | 385 ++++++++++++++++++ ...PokemonBDSP_AutoStory_PlannedRoute_Tests.h | 28 ++ .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 32 ++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 5 files changed, 449 insertions(+) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 60409b4a26..145096b969 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -6,6 +6,7 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.h" @@ -26,6 +27,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_StoryMaps(database); add_tests_AutoStory_Segment00(database); add_tests_AutoStory_Walk(database); + add_tests_AutoStory_PlannedRoute(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp new file mode 100644 index 0000000000..ac87afdcc7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp @@ -0,0 +1,385 @@ +/* BDSP AutoStory Planned Route Tests + * + * From: https://github.com/PokemonAutomation/ + * + * walk_route() (Utils/PokemonBDSP_AutoStory_PlannedRoute.h) on a script + * of console outcomes: each case lists the walks, presses and screen reads + * the walker must make, in order, and what the console answers. A call the + * script does not expect fails the case. + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "Utils/PokemonBDSP_AutoStory_PlannedRoute.h" +#include "Utils/PokemonBDSP_AutoStory_RouteIO.h" +#include "PokemonBDSP_AutoStory_PlannedRoute_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::TilePos; + + +// One call the walker is expected to make, and what the console answers: +// `walk` for a walk, `answer` for a dialogue watch or the holding check. +struct ScriptStep{ + std::string call; + WalkResult walk; + bool answer; +}; + +ScriptStep walk_step(std::string call, WalkStop stop, Milliseconds walked, size_t moved){ + return {.call = std::move(call), .walk = {.stop = stop, .walked = walked, .wild_battles = 0, .moved_chunks = moved}, .answer = false}; +} +ScriptStep answer_step(std::string call, bool answer){ + return {.call = std::move(call), .walk = {.stop = WalkStop::LIMIT, .walked = 0ms, .wild_battles = 0, .moved_chunks = 0}, .answer = answer}; +} +ScriptStep press_step(std::string call){ + return answer_step(std::move(call), false); +} + +// What the console did on a scripted walk: the steps called so far, every +// line logged, and the legs walked (to a wall, or a number of taps). +struct Script{ + std::vector steps; + size_t next = 0; + std::vector notes = {}; + std::vector legs = {}; +}; + +// Thrown by ScriptedRouteIO: a call the script did not expect, or the +// walker failing the program. +struct ScriptMismatch{ + std::string message; +}; +struct RouteFailed{ + std::string message; +}; + +std::string describe(const RouteInput& input){ + const char* kind = + input.kind == RouteInputKind::DPAD ? "D-pad" : + input.kind == RouteInputKind::STICK ? "stick" : + input.kind == RouteInputKind::BUTTON_A ? "A" : + input.kind == RouteInputKind::WAIT ? "wait" : "pause"; + const bool directed = input.kind == RouteInputKind::DPAD || input.kind == RouteInputKind::STICK; + return std::format("{}{}{} {} ms x{}", kind, directed ? " " : "", directed ? Pokemon::direction_name(input.direction) : "", + input.hold.count(), input.count); +} + +// The console as a script: each call must be the script's next step. +// look() sees no screen. +class ScriptedRouteIO : public RouteIO{ +public: + explicit ScriptedRouteIO(Script& script) + : m_script(script) + {} + + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) override{ + m_script.legs.push_back({.direction = walk.direction, .tiles = 0, .ends_at_wall = true}); + return expect("wall: " + walk.label).walk; + } + virtual WalkResult walk_taps(Direction direction, size_t tiles, const std::string& label) override{ + m_script.legs.push_back({.direction = direction, .tiles = tiles, .ends_at_wall = false}); + return expect(std::format("{} taps: {}", tiles, label)).walk; + } + virtual void press(const RouteInput& input) override{ + expect(describe(input)); + } + virtual bool dialog_within(Milliseconds wait, Color) override{ + return expect(std::format("dialog within {} ms", wait.count())).answer; + } + virtual bool holding_against_wall(Direction direction, const std::string&) override{ + return expect(std::string("holding ") + Pokemon::direction_name(direction)).answer; + } + virtual void look(const std::function&) override{} + virtual void note(const std::string& text, RouteNote kind) override{ + m_script.notes.push_back(text); + if (kind == RouteNote::FAILURE){ + throw RouteFailed{text}; + } + } + +private: + const ScriptStep& expect(const std::string& call){ + if (m_script.next >= m_script.steps.size()){ + throw ScriptMismatch{"a call after the script's end: \"" + call + "\""}; + } + const ScriptStep& step = m_script.steps[m_script.next++]; + if (step.call != call){ + throw ScriptMismatch{std::format("step {}: expected \"{}\", got \"{}\"", m_script.next, step.call, call)}; + } + return step; + } + + Script& m_script; +}; + + +// How a scripted walk ended: where it arrived, or why it failed. +struct ScriptedWalk{ + std::vector at; + std::string failure; + std::vector fights; +}; + +ScriptedWalk walk_script(const StoryWorld& maps, const TilePos& from, const TilePos& goal, std::vector sights, Script& script){ + ScriptedWalk out; + RouteTrainers trainers{ + .sights = std::move(sights), + .fight = [&out](const std::string& trainer){ out.fights.push_back(trainer); }, + }; + ScriptedRouteIO io(script); + try{ + out.at = walk_route(io, maps, {from}, trainers, { + .goal = [goal](const TilePos& pos){ return pos == goal; }, + .label = "T", + .avoid = {}, + .costs = route_plan_costs(), + .speed = MoveSpeed::WALK, + }); + }catch (const RouteFailed& e){ + out.failure = e.message; + } + return out; +} + +// The script's calls all made, or which are left. +std::string script_left(const Script& script){ + if (script.next == script.steps.size()){ + return ""; + } + return std::format("{} of {} steps called; next \"{}\".", script.next, script.steps.size(), script.steps[script.next].call); +} + +StoryWorld make_world(const std::vector& rows){ + Pokemon::MapData map; + map.name = "Test"; + map.tiles = Pokemon::Grid{(int)rows[0].size(), (int)rows.size(), {}}; + for (const std::string& row : rows){ + for (char glyph : row){ + map.tiles.cells.push_back(*Pokemon::tile_from_glyph(glyph)); + } + } + StoryWorld world; + world.add("Test", map, {0, 0}); + return world; +} + + +// r54: a trainer's line froze the check walk at a leg's wall; after the +// battle the walk on reached the wall, but the check still judged the +// frozen walk's DIALOG and failed. The walk on is what counts. +class Test_WallLegTrainerOnTheWay : public UnitTest{ +public: + Test_WallLegTrainerOnTheWay() + : UnitTest("PokemonBDSP::AutoStory - Planned route: a trainer on a wall leg, then on to the wall") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + Script script{.steps = { + walk_step("wall: T: east 4 (to wall)", WalkStop::LIMIT, 2050ms, 0), + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::DIALOG, 400ms, 0), + walk_step("wall: T: east 4 (to wall): on to the wall", WalkStop::BLOCKED, 1200ms, 0), + answer_step("dialog within 500 ms", false), + }}; + try{ + const ScriptedWalk walk = walk_script(make_world({"#.....#"}), {1, 0}, {5, 0}, {}, script); + if (!walk.failure.empty()){ + return "failed after the walk on reached the wall: " + walk.failure; + } + if (walk.fights != std::vector{"a trainer on the way"}){ + return std::format("fought {} times, expected once.", walk.fights.size()); + } + if (walk.at != std::vector{{5, 0}}){ + return "did not arrive at (5, 0)."; + } + }catch (const ScriptMismatch& e){ + return e.message; + } + const std::string left = script_left(script); + return left.empty() ? UnitTestResult(true) : UnitTestResult(left); + } +}; + + +// A leg to a wall that ran out of time: a check walk that also runs out +// of time counts as at the wall only if holding on there changes the +// screen less than walking (r127a ran along the wrong row). +struct WallCheckCase{ + std::string name; + std::vector check; + bool fails; +}; + +class Test_WallLegLimit : public UnitTest{ +public: + Test_WallLegLimit() + : UnitTest("PokemonBDSP::AutoStory - Planned route: a wall leg out of time needs the holding check") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "the check stops at the wall", .check = { + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::BLOCKED, 1050ms, 0), + answer_step("dialog within 500 ms", false), + }, .fails = false}, + {.name = "the check runs out of time, holding on reads a wall", .check = { + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::LIMIT, 2000ms, 0), + answer_step("holding east", true), + answer_step("dialog within 500 ms", false), + }, .fails = false}, + {.name = "the check runs out of time, holding on still moves", .check = { + walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::LIMIT, 2000ms, 0), + answer_step("holding east", false), + }, .fails = true}, + }; + for (const WallCheckCase& item : cases){ + Script script{.steps = {walk_step("wall: T: east 4 (to wall)", WalkStop::LIMIT, 2050ms, 0)}}; + script.steps.insert(script.steps.end(), item.check.begin(), item.check.end()); + try{ + const ScriptedWalk walk = walk_script(make_world({"#.....#"}), {1, 0}, {5, 0}, {}, script); + if (walk.failure.empty() == item.fails){ + return item.name + (item.fails ? ": went on as at the wall." : ": failed: " + walk.failure); + } + }catch (const ScriptMismatch& e){ + return item.name + ": " + e.message; + } + const std::string left = script_left(script); + if (!left.empty()){ + return item.name + ": " + left; + } + } + return true; + } +}; + + +// Whether walking `legs` from `from` ever steps into `person`'s tile: a +// leg to a wall that stops against them, or a tap toward them. +bool walks_into(const StoryWorld& world, TilePos from, const std::vector& legs, const TilePos& person){ + for (const TileLeg& leg : legs){ + for (size_t tap = 0; leg.ends_at_wall || tap < leg.tiles; tap++){ + if (from.next(leg.direction) == person){ + return true; + } + const std::optional next = step(world, from, leg.direction); + if (!next){ + break; + } + from = *next; + } + } + return false; +} + +// Route 202 alone, at its own tiles, and Logan's sight: he stands at +// (26, 4) and looks south. +StoryWorld route202_world(){ + StoryWorld world; + world.add("Route 202", story_map("Route202"), {0, 0}); + return world; +} +std::vector logan_sight(){ + std::vector sights; + for (const TrainerSight& sight : route202_trainer_sights()){ + if (sight.name == "logan"){ + sights.push_back(sight); + } + } + return sights; +} + +// Whether the replan after the battle starts from each of `tiles`. +std::string check_replan_from(const Script& script, const std::vector& tiles){ + const auto replan = std::find_if(script.notes.begin(), script.notes.end(), [](const std::string& note){ + return note.find("planning again") != std::string::npos; + }); + if (replan == script.notes.end()){ + return "never planned again after the battle."; + } + for (const std::string& tile : tiles){ + if (replan->find(tile) == std::string::npos){ + return "the replan does not start from " + tile + ": " + *replan; + } + } + return ""; +} + +// r55: Logan (Route 202, (26, 4), looking south) stopped an open leg west +// along row 5. "5 taps" were 4, under his nose at (26, 5); the replan from +// the tap count alone walked into him. The replan must start from both the +// tile the taps give and the tile where the leg enters his sight, and walk +// on from either without bumping into him. +class Test_OpenLegKnownTrainer : public UnitTest{ +public: + Test_OpenLegKnownTrainer() + : UnitTest("PokemonBDSP::AutoStory - Planned route: a known trainer stops an open leg") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const StoryWorld world = route202_world(); + // West along row 5 from (30, 5) to (24, 5): his line is the 4th tap, + // and his dialogue comes after 3 taps that moved. + Script script{.steps = { + press_step("D-pad west 50 ms x1"), + walk_step("6 taps: T: west 6 (open)", WalkStop::DIALOG, 1500ms, 3), + walk_step("wall: T: east 4 (to wall)", WalkStop::BLOCKED, 2000ms, 0), + answer_step("dialog within 500 ms", false), + press_step("D-pad west 50 ms x1"), + walk_step("6 taps: T: west 6 (open)", WalkStop::LIMIT, 1500ms, 6), + answer_step("dialog within 500 ms", false), + }}; + ScriptedWalk walk; + try{ + walk = walk_script(world, {30, 5}, {24, 5}, logan_sight(), script); + }catch (const ScriptMismatch& e){ + return e.message; + } + if (!walk.failure.empty() || walk.fights != std::vector{"logan"}){ + return std::format("expected to fight Logan once and go on; fought {}, failure \"{}\".", walk.fights.size(), walk.failure); + } + // The taps say (27, 5); his sight line crosses the row at (26, 5). + const std::string replan = check_replan_from(script, {"Route 202 (27, 5)", "Route 202 (26, 5)"}); + if (!replan.empty()){ + return replan; + } + const std::vector after_battle(script.legs.begin() + 1, script.legs.end()); + const TilePos logan = map_point("Route202", "trainer_logan"); + for (const TilePos& from : {TilePos{27, 5}, TilePos{26, 5}}){ + if (walks_into(world, from, after_battle, logan)){ + return "the replan walks into Logan from " + Pokemon::to_string(from) + "."; + } + } + if (walk.at != std::vector{{24, 5}}){ + return "did not arrive at (24, 5)."; + } + const std::string left = script_left(script); + return left.empty() ? UnitTestResult(true) : UnitTestResult(left); + } +}; + +} + + +void add_tests_AutoStory_PlannedRoute(UnitTestDatabase& database){ + database.add(); + database.add(); + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h new file mode 100644 index 0000000000..13331b8db4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h @@ -0,0 +1,28 @@ +/* BDSP AutoStory Planned Route Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_PlannedRoute_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// walk_route() on a script of console outcomes (a ScriptedRouteIO): what +// the walker does after a trainer stops a leg, and when a leg to a wall +// counts as at its wall. +void add_tests_AutoStory_PlannedRoute(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp index 9357b6cee2..c38ff46daf 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -23,6 +23,37 @@ namespace PokemonBDSP{ namespace{ +// A turn must not step: on the console D-pad taps of 40 to 80 ms only +// turned, a 160 ms D-pad tap stepped a whole tile, and a 100 ms stick tap +// stepped about 0.6 tile, off the tile grid (r49, r248a). So route_turn() +// taps the D-pad, once, for at most 80 ms. +class Test_RouteTurnTap : public UnitTest{ +public: + Test_RouteTurnTap() + : UnitTest("PokemonBDSP::AutoStory - a turn is a D-pad tap too short to step") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + constexpr Milliseconds LONGEST_TURN(80); + for (Pokemon::Direction direction : {Pokemon::Direction::NORTH, Pokemon::Direction::SOUTH, Pokemon::Direction::EAST, Pokemon::Direction::WEST}){ + const RouteInput tap = route_turn_tap(direction); + const std::string name = std::string("turning ") + Pokemon::direction_name(direction); + if (tap.kind != RouteInputKind::DPAD){ + return name + ": not a D-pad tap; a stick tap moves part of a tile."; + } + if (tap.direction != direction || tap.count != 1){ + return name + ": expected one tap that way."; + } + if (tap.hold > LONGEST_TURN){ + return name + ": a " + std::to_string(tap.hold.count()) + " ms tap; over 80 ms may step."; + } + } + if (ROUTE_TURN_TAP > LONGEST_TURN){ + return "ROUTE_TURN_TAP is " + std::to_string(ROUTE_TURN_TAP.count()) + " ms; over 80 ms may step."; + } + return true; + } +}; + // indoors() decides when leave_door_south() is out: r257 walked on south // past Sandgem's road into a house after leaving the Center, because the // evening tint made the outside as warm as a room. Synthetic screens: the @@ -211,6 +242,7 @@ class Test_LocationBannerShowing : public UnitTest{ void add_tests_AutoStory_Walk(UnitTestDatabase& database){ + database.add(); database.add(); database.add(); database.add(); diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 801fa55772..4ff2eeeaf0 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1641,6 +1641,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp From 8a4342ff0ac23bda3b86a721751f46e974f03812 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 01:57:46 -0800 Subject: [PATCH 28/39] BDSP AutoStory walk: a quiet tap followed by a moving one counted as a step A tap whose view barely changed (the president walking off as we turned) was counted as not moving, so "east 2" read "moved 1" while the console showed both tiles walked (r67). What blocks one tap still blocks the next, so a quiet tap that a moving tap follows moved too. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index f4066a9ed7..53d7a461e0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -329,7 +329,11 @@ class WalkUntil{ } const bool still = still_chunk(change); if (!still){ - m_result.moved_chunks++; + // A quiet tap and then a moving one: the first moved too. What + // blocks a tap still blocks the next; the view barely changed + // while the president walked off (r67: "east 2 moved 1", and + // the console showed both tiles walked). + m_result.moved_chunks += 1 + (m_spec.input == WalkInput::TAPS ? m_unchanged : 0); } m_unchanged = still ? m_unchanged + 1 : 0; if (m_unchanged >= 2){ From 0d6adc701a8a0e9679ef5f97291e768bb9cc24ad Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 5 Nov 2026 22:17:38 -0800 Subject: [PATCH 29/39] BDSP AutoStory walk: align with PR #231 lint conventions --- .../PokemonBDSP_TransitionWatchers.cpp | 6 +- .../PokemonBDSP_TransitionWatchers.h | 6 +- ...okemonBDSP_AutoStory_OverworldDetector.cpp | 6 +- .../PokemonBDSP_AutoStory_OverworldDetector.h | 7 +- ...kemonBDSP_AutoStory_PlannedRoute_Tests.cpp | 89 ++++++++---- ...PokemonBDSP_AutoStory_PlannedRoute_Tests.h | 6 +- .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 132 ++++++++++-------- .../PokemonBDSP_AutoStory_Walk_Tests.h | 6 +- .../Utils/PokemonBDSP_AutoStory_Indoors.cpp | 8 +- .../Utils/PokemonBDSP_AutoStory_Indoors.h | 6 +- .../Utils/PokemonBDSP_AutoStory_Location.cpp | 10 +- .../Utils/PokemonBDSP_AutoStory_Location.h | 9 +- .../PokemonBDSP_AutoStory_OverlayLog.cpp | 6 +- .../Utils/PokemonBDSP_AutoStory_OverlayLog.h | 6 +- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 10 +- .../PokemonBDSP_AutoStory_PlannedRoute.h | 6 +- .../Utils/PokemonBDSP_AutoStory_RouteIO.cpp | 8 +- .../Utils/PokemonBDSP_AutoStory_RouteIO.h | 7 +- .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 10 +- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 11 +- .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 12 +- .../Utils/PokemonBDSP_AutoStory_Walk.h | 14 +- 22 files changed, 222 insertions(+), 159 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp index 3cf98c2e72..d5e37c567f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.cpp @@ -40,6 +40,6 @@ bool WhiteScreenOverWatcher::white_is_over(const ImageViewRGB32& frame){ } -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h index 9091f99fb0..2dd7e59d7c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h +++ b/SerialPrograms/Source/PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h @@ -45,7 +45,7 @@ class WhiteScreenOverWatcher : public VisualInferenceCallback{ }; -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp index 6b250862fd..effcc50af0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.cpp @@ -131,6 +131,6 @@ bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context){ } -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h index ce36eb5f86..f2717765b6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h @@ -81,6 +81,7 @@ class PoketchButtonDetector : public StaticScreenDetector { ImageFloatBox m_button; }; +// Inference watcher tracking the visibility of the Poketch button on screen. class PoketchButtonWatcher : public PoketchButtonDetector, public VisualInferenceCallback { public: PoketchButtonWatcher(Color color = COLOR_RED); @@ -94,7 +95,7 @@ class PoketchButtonWatcher : public PoketchButtonDetector, public VisualInferenc bool ensure_poketch_visible(VideoStream& stream, ProControllerContext& context); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp index ac87afdcc7..ca7f599636 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp @@ -183,12 +183,7 @@ StoryWorld make_world(const std::vector& rows){ // r54: a trainer's line froze the check walk at a leg's wall; after the // battle the walk on reached the wall, but the check still judged the // frozen walk's DIALOG and failed. The walk on is what counts. -class Test_WallLegTrainerOnTheWay : public UnitTest{ -public: - Test_WallLegTrainerOnTheWay() - : UnitTest("PokemonBDSP::AutoStory - Planned route: a trainer on a wall leg, then on to the wall") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ +UnitTestResult test_wall_leg_trainer_on_the_way(Logger&){ Script script{.steps = { walk_step("wall: T: east 4 (to wall)", WalkStop::LIMIT, 2050ms, 0), walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::DIALOG, 400ms, 0), @@ -211,8 +206,7 @@ class Test_WallLegTrainerOnTheWay : public UnitTest{ } const std::string left = script_left(script); return left.empty() ? UnitTestResult(true) : UnitTestResult(left); - } -}; +} // A leg to a wall that ran out of time: a check walk that also runs out @@ -224,12 +218,7 @@ struct WallCheckCase{ bool fails; }; -class Test_WallLegLimit : public UnitTest{ -public: - Test_WallLegLimit() - : UnitTest("PokemonBDSP::AutoStory - Planned route: a wall leg out of time needs the holding check") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ +UnitTestResult test_wall_leg_limit(Logger&){ const std::vector cases{ {.name = "the check stops at the wall", .check = { walk_step("wall: T: east 4 (to wall): at the wall?", WalkStop::BLOCKED, 1050ms, 0), @@ -262,8 +251,7 @@ class Test_WallLegLimit : public UnitTest{ } } return true; - } -}; +} // Whether walking `legs` from `from` ever steps into `person`'s tile: a @@ -322,12 +310,7 @@ std::string check_replan_from(const Script& script, const std::vector body; }; -} +struct PlannedRouteExpectation{ +}; +struct PlannedRouteCase{ + PlannedRouteInput input; + PlannedRouteExpectation expectation; +}; + +class Test_PlannedRoute : public UnitTest{ +public: + explicit Test_PlannedRoute(PlannedRouteCase test_case) + : UnitTest("PokemonBDSP::AutoStory - Planned route: " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_case.input.body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + PlannedRouteCase m_case; +}; + +} // namespace + + +const PlannedRouteCase PLANNED_ROUTE_CASES[] = { + { + .input = {"a trainer on a wall leg, then on to the wall", test_wall_leg_trainer_on_the_way}, + .expectation = {}, + }, + { + .input = {"a wall leg out of time needs the holding check", test_wall_leg_limit}, + .expectation = {}, + }, + { + .input = {"a known trainer stops an open leg", test_open_leg_known_trainer}, + .expectation = {}, + }, +}; void add_tests_AutoStory_PlannedRoute(UnitTestDatabase& database){ - database.add(); - database.add(); - database.add(); + for (const auto& item : PLANNED_ROUTE_CASES){ + database.add(item); + } } -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h index 13331b8db4..740e5c27e8 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h @@ -22,7 +22,7 @@ void add_tests_AutoStory_PlannedRoute(UnitTestDatabase& database); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp index c38ff46daf..e29b302195 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -27,12 +27,7 @@ namespace{ // turned, a 160 ms D-pad tap stepped a whole tile, and a 100 ms stick tap // stepped about 0.6 tile, off the tile grid (r49, r248a). So route_turn() // taps the D-pad, once, for at most 80 ms. -class Test_RouteTurnTap : public UnitTest{ -public: - Test_RouteTurnTap() - : UnitTest("PokemonBDSP::AutoStory - a turn is a D-pad tap too short to step") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ +UnitTestResult test_route_turn_tap(Logger&){ constexpr Milliseconds LONGEST_TURN(80); for (Pokemon::Direction direction : {Pokemon::Direction::NORTH, Pokemon::Direction::SOUTH, Pokemon::Direction::EAST, Pokemon::Direction::WEST}){ const RouteInput tap = route_turn_tap(direction); @@ -51,8 +46,7 @@ class Test_RouteTurnTap : public UnitTest{ return "ROUTE_TURN_TAP is " + std::to_string(ROUTE_TURN_TAP.count()) + " ms; over 80 ms may step."; } return true; - } -}; +} // indoors() decides when leave_door_south() is out: r257 walked on south // past Sandgem's road into a house after leaving the Center, because the @@ -66,12 +60,7 @@ struct IndoorsCase{ bool indoors; }; -class Test_IndoorsAnyLight : public UnitTest{ -public: - Test_IndoorsAnyLight() - : UnitTest("PokemonBDSP::AutoStory - indoors() under the evening tint") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ +UnitTestResult test_indoors_any_light(Logger&){ const std::vector cases{ {.name = "evening outside, orange all over", .fill = 0xffe0a040, .dark_bottom = 0, .indoors = false}, {.name = "a room: orange, black below its floor", .fill = 0xffe0a040, .dark_bottom = 0.10, .indoors = true}, @@ -92,8 +81,7 @@ class Test_IndoorsAnyLight : public UnitTest{ } } return true; - } -}; +} // door_fade_look(): out of a door is a dark look, then a bright one that is // not a room. r259 turned round in Sandgem's lit Center, read "not a room" @@ -109,12 +97,7 @@ struct DoorCase{ size_t out_at; }; -class Test_DoorFade : public UnitTest{ -public: - Test_DoorFade() - : UnitTest("PokemonBDSP::AutoStory - out of a door needs its fade") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ +UnitTestResult test_door_fade(Logger&){ const std::vector cases{ {.name = "r259: a bright look in the Center that misses the room", .looks = {{458, true}, {440, false}, {457, true}}, .out_at = 0}, {.name = "the fade, then the town", .looks = {{458, true}, {300, true}, {60, false}, {310, false}}, .out_at = 4}, @@ -134,17 +117,10 @@ class Test_DoorFade : public UnitTest{ } } return true; - } -}; +} -class Test_LocationBanner : public UnitTest{ -public: - Test_LocationBanner() - : UnitTest("PokemonBDSP::AutoStory - location banner match") - {} - - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ +UnitTestResult test_location_banner(Logger&){ struct Case{ const char* read; const char* expected; @@ -167,16 +143,10 @@ class Test_LocationBanner : public UnitTest{ } } return true; - } -}; +} -class Test_Indoors : public UnitTest{ -public: - Test_Indoors() - : UnitTest("PokemonBDSP::AutoStory - indoors") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ +UnitTestResult test_indoors(Logger& logger){ struct Case{ const char* file; bool inside; }; const Case cases[] = { {"PokemonBDSP/AutoStory/JubilifeCenterInside-20261030-c6.png", true}, @@ -203,16 +173,10 @@ class Test_Indoors : public UnitTest{ } } return true; - } -}; +} -class Test_LocationBannerShowing : public UnitTest{ -public: - Test_LocationBannerShowing() - : UnitTest("PokemonBDSP::AutoStory - location banner showing") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ +UnitTestResult test_location_banner_showing(Logger& logger){ struct Case{ const char* file; bool banner; }; const Case cases[] = { {"PokemonBDSP/AutoStory/JubilifeCenterDoorstep-20261030-c5.png", true}, @@ -234,24 +198,78 @@ class Test_LocationBannerShowing : public UnitTest{ } } return true; +} + + +struct AutoStoryWalkInput{ + std::string name; + std::function body; +}; + +struct AutoStoryWalkExpectation{ +}; + +struct AutoStoryWalkCase{ + AutoStoryWalkInput input; + AutoStoryWalkExpectation expectation; +}; + +class Test_AutoStoryWalk : public UnitTest{ +public: + explicit Test_AutoStoryWalk(AutoStoryWalkCase test_case) + : UnitTest("PokemonBDSP::AutoStory - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_case.input.body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } } + +private: + AutoStoryWalkCase m_case; }; +} // namespace -} +const AutoStoryWalkCase AUTO_STORY_WALK_CASES[] = { + { + .input = {"a turn is a D-pad tap too short to step", test_route_turn_tap}, + .expectation = {}, + }, + { + .input = {"indoors() under the evening tint", test_indoors_any_light}, + .expectation = {}, + }, + { + .input = {"out of a door needs its fade", test_door_fade}, + .expectation = {}, + }, + { + .input = {"location banner match", test_location_banner}, + .expectation = {}, + }, + { + .input = {"indoors", test_indoors}, + .expectation = {}, + }, + { + .input = {"location banner showing", test_location_banner_showing}, + .expectation = {}, + }, +}; void add_tests_AutoStory_Walk(UnitTestDatabase& database){ - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); - database.add(); + for (const auto& item : AUTO_STORY_WALK_CASES){ + database.add(item); + } } -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h index 266dfa1c0d..6532456aa1 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h @@ -21,7 +21,7 @@ void add_tests_AutoStory_Walk(UnitTestDatabase& database); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp index c7e7a3458c..2ae32da5c3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp @@ -27,7 +27,7 @@ bool mostly_dark(const EdgeDark& edge){ return edge.count > 0 && edge.dark * 100 >= edge.count * 15; } -} +} // namespace bool indoors(const ImageViewRGB32& screen){ const size_t width = screen.width(), height = screen.height(); @@ -63,6 +63,6 @@ bool indoors(const ImageViewRGB32& screen){ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h index 9de7ba3159..b86d80d934 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h @@ -27,7 +27,7 @@ namespace PokemonBDSP{ bool indoors(const ImageViewRGB32& screen); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp index f79677bbe1..df31791552 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -45,7 +45,7 @@ std::string digits_of(const std::string& text){ return out; } -} +} // namespace std::string normalize_location_name(const std::string& text){ @@ -63,7 +63,7 @@ Language& story_language_setting(){ static Language language = Language::English; return language; } -} +} // namespace Language story_language(){ return story_language_setting(); @@ -143,6 +143,6 @@ bool location_banner_matches(const std::string& read, const std::string& expecte } -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h index 683874d64c..cb4e4c8626 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h @@ -31,8 +31,11 @@ std::string normalize_location_name(const std::string& text); // language-free checks: the banner's box showing, a sign's box opening, the // first row of a Bag pocket. Language story_language(); +// Sets the active story language. void set_story_language(Language language); +// Returns whether text recognition is supported for the current story language. bool story_reads_text(); +// Returns whether text recognition is supported for the specified language. bool story_reads_text(Language language); // What the reads below return for a banner or save card that showed, when @@ -61,7 +64,7 @@ std::string read_save_screen_location(const ImageViewRGB32& screen); bool location_banner_matches(const std::string& read, const std::string& expected); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp index eab60111f3..f4cc54ee6e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.cpp @@ -37,6 +37,6 @@ void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h index 803b9bc4aa..5e79b82227 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_OverlayLog.h @@ -21,7 +21,7 @@ namespace PokemonBDSP{ void add_wrapped_log(VideoOverlay& overlay, const std::string& text, Color color); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index bbeafd4c0e..3b3bdf4564 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -98,7 +98,7 @@ std::string map_of(const StoryWorld& maps, const std::vector& tiles){ return name; } -} +} // namespace namespace{ @@ -734,7 +734,7 @@ LegNext RouteWalker::replan_from(const LegWalk& walk, std::vector stopp return LegNext::REPLAN; } -} +} // namespace std::vector walk_route( @@ -813,6 +813,6 @@ void talk_to_npc( -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h index e422d6891c..5995c93a17 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -115,7 +115,7 @@ void talk_to_npc( -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp index 37e6e3b06a..ef4f69d5f3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.cpp @@ -46,7 +46,7 @@ bool holding_against_wall(VideoStream& stream, ProControllerContext& context, do return before && most < WALKING_CHANGE; } -} +} // namespace WalkResult ConsoleRouteIO::walk_to_wall(const RouteWallWalk& walk){ @@ -96,6 +96,6 @@ void ConsoleRouteIO::note(const std::string& text, RouteNote kind){ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h index 1db4631068..0968862a21 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h @@ -44,6 +44,7 @@ enum class RouteNote{ FAILURE, // fails the program with a screenshot; does not return }; +// Abstract I/O interface for route traversal, logging, and controller execution. class RouteIO{ public: virtual ~RouteIO() = default; @@ -97,7 +98,7 @@ class ConsoleRouteIO : public RouteIO{ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index cbd3f3c2d4..2ee8492217 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -39,7 +39,7 @@ bool across(Direction direction){ return direction == Direction::EAST || direction == Direction::WEST; } -} +} // namespace Milliseconds route_ms_per_tile(Direction direction, MoveSpeed speed){ if (speed == MoveSpeed::RUN){ @@ -285,7 +285,7 @@ bool outside_for_three_looks(VideoStream& stream, ProControllerContext& context, return true; } -} +} // namespace void leave_door_south(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ // A last line of talk may still be up, and it would stop the walk. @@ -326,6 +326,6 @@ void leave_door_south(VideoStream& stream, ProControllerContext& context, const -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index ea46031644..2eed12b8e9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -30,6 +30,7 @@ namespace PokemonBDSP{ // Walking time per tile, measured on the console from the post-catch save: // a row is deeper than a column is wide under the tilted camera. constexpr Milliseconds ROUTE_MS_PER_TILE_ACROSS = Milliseconds(160); // east or west +// Walking time per vertical tile (north or south). constexpr Milliseconds ROUTE_MS_PER_TILE_UP_DOWN = Milliseconds(240); // north or south // From pressing the stick to the first step, when standing still. constexpr Milliseconds ROUTE_START_DELAY = Milliseconds(250); @@ -47,6 +48,7 @@ enum class MoveSpeed{ // Rounded down: a count from too fast a pace runs over, which a wall leg // forgives, where one from too slow a pace stops short of the wall. constexpr Milliseconds ROUTE_RUN_MS_PER_TILE_ACROSS = Milliseconds(100); +// Running time per vertical tile (north or south). constexpr Milliseconds ROUTE_RUN_MS_PER_TILE_UP_DOWN = Milliseconds(150); // Time per tile in `direction` at `speed`. @@ -91,6 +93,7 @@ struct RouteInput{ size_t count; RouteInputEnd end; }; +// Executes the specified route input sequence on the controller context. void press_route_input(ProControllerContext& context, const RouteInput& input); // The tap route_turn() turns with: on the D-pad, ROUTE_TURN_TAP long. @@ -130,6 +133,7 @@ bool talk_with_nudges( Milliseconds wait = Milliseconds(3000), const std::function& accept = nullptr ); +// D-pad nudge tap duration for micro-adjusting player position. constexpr Milliseconds ROUTE_NUDGE_TAP = Milliseconds(50); // Whether a walk to the wall that ends on any of `after` can tell it has @@ -168,6 +172,7 @@ void leave_door_south(VideoStream& stream, ProControllerContext& context, const // Brightness (mean r + g + b) below which a look is the door's fade, and // above which a look that is not a room is outside. inline constexpr double DOOR_FADE_DARK = 120.0; +// Brightness threshold above which non-room frame is classified as outside. inline constexpr double DOOR_OUTSIDE_BRIGHT = 200.0; // What leave_door_south() has seen of the door's fade so far. @@ -185,7 +190,7 @@ bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index 53d7a461e0..a89cc82a6c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -101,7 +101,7 @@ bool battle_follows(VideoStream& stream, ProControllerContext& context){ return wait_until(stream, context, 5s, {{battle_menu}, {battle_dialog}}) >= 0; } -} +} // namespace // A step in tall grass that was started before the stick was released still // finishes, and its encounter starts after the walk has ended. Watch a little @@ -172,7 +172,7 @@ class WalkProbeSampler : public VisualInferenceCallback{ bool m_has_last = false; }; -} +} // namespace namespace{ @@ -394,7 +394,7 @@ WalkResult walk_until_spec( return WalkUntil(stream, context, escape, spec).run(); } -} +} // namespace WalkResult walk_until( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, @@ -575,6 +575,6 @@ WalkResult walk_expecting( } -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h index 504edccfe8..f46828599d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -29,6 +29,7 @@ namespace PokemonBDSP{ // started; how to leave it is the program's battle code, passed in. using WildBattleEscape = std::function; +// Possible termination conditions for an automated walking sequence. enum class WalkStop{ ARRIVED, // `arrived` matched. DIALOG, // A dialogue box opened: an NPC or a trainer stopped us. @@ -36,11 +37,13 @@ enum class WalkStop{ BLOCKED, // The screen stopped changing: we are walking into a wall. LIMIT, // Walked `limit` without any of the above. }; +// Returns display name string corresponding to given WalkStop condition. const char* walk_stop_name(WalkStop stop); // Whether the walk has arrived, from the screen after each chunk. using WalkArrived = std::function; +// Result metrics and terminal state returned by automated walking execution. struct WalkResult{ WalkStop stop; Milliseconds walked; @@ -67,6 +70,7 @@ enum class DpadPace{ WALK, RUN, }; +// Configuration options controlling D-pad based walking maneuvers. struct DpadWalkOptions{ WalkArrived arrived = nullptr; DpadPace pace = DpadPace::WALK; @@ -80,7 +84,7 @@ struct DpadWalkOptions{ WalkResult walk_until_dpad( VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, JoystickPosition stick, Milliseconds limit, const std::string& label, - const DpadWalkOptions& options = {} + const DpadWalkOptions& options = DpadWalkOptions() ); // A set number of whole tiles on the D-pad, a tap a tile (a tap moves @@ -151,6 +155,7 @@ WalkResult walk_to_talk( // the whole view: 16 to 85 per 400ms on Route 202, against about 6 for running in // place against trees. double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after); +// Screen change metric threshold indicative of blocked movement against an obstacle. constexpr double WALK_BLOCKED_CHANGE = 10.0; // For whole-tile D-pad taps: a tap that moves changes the screen by 21 to 50 // (r244a: 40 per tap along Route 204's row 27); a tap into a wall or an NPC @@ -176,6 +181,7 @@ struct WalkProbe{ }; std::vector samples; }; +// Probes walking movement and records timestamped screen change samples. WalkProbe probe_walk( VideoStream& stream, ProControllerContext& context, JoystickPosition stick, Milliseconds limit, @@ -183,7 +189,7 @@ WalkProbe probe_walk( ); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif From 9c480afd0680b4f1b79451a49ec1c9da84068782 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sat, 7 Nov 2026 16:24:55 -0800 Subject: [PATCH 30/39] BDSP walk: track encounter origin and lag recovery during tap walks --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 18 +++++++++-- .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 30 +++++++++++++++++++ .../Utils/PokemonBDSP_AutoStory_Walk.h | 12 +++++--- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 3b3bdf4564..ff6b4dccb9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -636,9 +636,21 @@ std::vector RouteWalker::watching_trainers(const std::vector>& paths){ - std::vector anywhere = m_from; - for (const std::vector& path : paths){ - anywhere.insert(anywhere.end(), path.begin(), path.end()); + std::vector anywhere; + const size_t moved = std::min(walk.planned.leg.tiles, walk.result.moved_chunks + walk.turn_moved); + if (walk.result.stop == WalkStop::DIALOG && moved > 0){ + for (const TilePos& pos : m_from){ + const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, moved); + if (!path.empty()){ + anywhere.push_back(path.back()); + } + } + } + if (anywhere.empty()){ + anywhere = m_from; + for (const std::vector& path : paths){ + anywhere.insert(anywhere.end(), path.begin(), path.end()); + } } anywhere = sorted_unique(std::move(anywhere)); // A scene the map lacks (r14: Barry's Route 201 warning, then the row that diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index a89cc82a6c..9050540834 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -237,6 +237,7 @@ class WalkUntil{ } const Milliseconds chunk = std::min(CHUNK, m_spec.limit - m_result.walked); const int ret = hold(chunk); + m_chunks++; m_result.walked += chunk; ChunkVerdict verdict = after_watchers(ret); if (verdict.next == ChunkNext::LOOK){ @@ -334,6 +335,8 @@ class WalkUntil{ // while the president walked off (r67: "east 2 moved 1", and // the console showed both tiles walked). m_result.moved_chunks += 1 + (m_spec.input == WalkInput::TAPS ? m_unchanged : 0); + }else if (m_unchanged == 0){ + m_before_still = m_before; } m_unchanged = still ? m_unchanged + 1 : 0; if (m_unchanged >= 2){ @@ -357,6 +360,14 @@ class WalkUntil{ void escape_battle(const std::string& what){ m_result.wild_battles++; + if (m_chunks <= 1){ + m_result.origin = WalkOrigin::MAY_PRECEDE_LEG; + } + if (m_spec.input == WalkInput::TAPS){ + // Preserve the reporting credit; callers must carry uncertain + // pose after an encounter rather than infer a completed tile. + m_result.moved_chunks++; + } m_stream.log("[Walk] " + m_spec.label + ": " + what); m_escape(m_stream, m_context); m_context.wait_for_all_requests(); @@ -366,13 +377,30 @@ class WalkUntil{ WalkResult finish(WalkStop stop){ m_result.stop = stop; + bool late_battle = false; if (m_spec.wild == WildPolicy::RUN_FROM && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && run_from_late_encounter(m_stream, m_context, m_escape, m_spec.label) ){ m_result.wild_battles++; + late_battle = true; + m_result.origin = WalkOrigin::MAY_PRECEDE_LEG; + } + // When the final tap of a TAPS leg lagged during frame drops at its + // 800ms check (m_unchanged == 1 at WalkStop::LIMIT), it finishes + // scrolling during the 1200ms late-encounter wait (r4: away_from_barry + // "north 2 moved 1" while console arrived at (14, 16)). + if (!late_battle && m_spec.input == WalkInput::TAPS && stop == WalkStop::LIMIT && + m_unchanged == 1 && m_before_still + ){ + VideoSnapshot after = snapshot_or_fail(m_stream, m_spec.label); + const double change = walk_screen_change(*m_before_still.frame, *after.frame); + if (!still_chunk(change)){ + m_result.moved_chunks++; + } } m_stream.log("[Walk] " + m_spec.label + ": " + walk_stop_name(stop) + " after " + std::to_string(m_result.walked.count()) + "ms, " + + std::to_string(m_result.moved_chunks) + " moved chunks, " + std::to_string(m_result.wild_battles) + " wild battle(s)."); return m_result; } @@ -384,7 +412,9 @@ class WalkUntil{ const DpadPosition m_dpad; WalkResult m_result{WalkStop::LIMIT, 0ms, 0}; VideoSnapshot m_before; + VideoSnapshot m_before_still; size_t m_unchanged = 0; + size_t m_chunks = 0; double m_largest = 0; }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h index f46828599d..ae33d766d7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -43,16 +43,20 @@ const char* walk_stop_name(WalkStop stop); // Whether the walk has arrived, from the screen after each chunk. using WalkArrived = std::function; +// An encounter already active at the first input, or found only after a +// leg, may belong to earlier movement. Never localize from this leg alone. +enum class WalkOrigin{ CURRENT_LEG, MAY_PRECEDE_LEG }; + // Result metrics and terminal state returned by automated walking execution. struct WalkResult{ WalkStop stop; - Milliseconds walked; - size_t wild_battles; - // Chunks whose screen change passed the blocked threshold: on a tap - // walk, the taps that moved a tile. A trainer's freeze and a bump do + Milliseconds walked{}; + size_t wild_battles = 0; + // Count of completed steps or taps that clearly moved. Blocked taps do // not count (r248a: "4 taps before the battle" were 2 steps and 2 // taps into Taylor's freeze). size_t moved_chunks = 0; + WalkOrigin origin = WalkOrigin::CURRENT_LEG; }; // Holds the left stick at `stick` until one of the WalkStop events. Wild battles on From 070078e407a60b14a6d55a3198b829783c28319f Mon Sep 17 00:00:00 2001 From: dayfine Date: Sat, 7 Nov 2026 16:50:10 -0800 Subject: [PATCH 31/39] BDSP AutoStory walk: adopt wall recovery for interrupted open and wall legs --- .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 72 ++++++++++++++----- 1 file changed, 55 insertions(+), 17 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index ff6b4dccb9..0d5f55b236 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -12,6 +12,7 @@ #include "Common/Cpp/Filesystem/Filesystem.h" #include "CommonFramework/GlobalAutoPaths.h" #include "Pokemon/Navigation/Pokemon_RouteLog.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_WallRecovery.h" #include "PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP_AutoStory_RouteIO.h" #include "PokemonBDSP_AutoStory_RouteWalk.h" @@ -222,6 +223,7 @@ class RouteWalker{ // Legs to a wall. LegNext wall_leg(LegWalk& walk); + bool finish_interrupted_wall(LegWalk& walk, Milliseconds limit); size_t unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest); LegNext stopped_short_of_wall(LegWalk& walk, size_t walked_tiles); void check_at_wall(LegWalk& walk); @@ -229,6 +231,7 @@ class RouteWalker{ // Open legs, tapped. LegNext open_leg(LegWalk& walk); + LegNext interrupted_open_leg(LegWalk& walk); void turn_tap(LegWalk& walk); LegNext blocked_open_leg(LegWalk& walk, size_t moved); @@ -286,14 +289,14 @@ std::vector RouteWalker::run(std::vector from){ } std::vector RouteWalker::plan_legs() const{ - std::optional> legs = plan_robust_route(m_grid, m_from, m_plan.goal, m_plan.costs); + std::optional> legs = plan_recovered_route(m_grid, m_from, m_plan.goal, m_plan.costs); if (!legs){ // From a few possible tiles the exact-open-leg costs found no // plan where the default ones did (r240a, three tiles on Route // 204's row 27); the default plan is longer but sound. m_io.note("[Route] " + m_plan.label + ": no plan with the route costs from " + describe(m_maps, m_from) + "; trying the default costs.", RouteNote::WARNING); - legs = plan_robust_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); + legs = plan_recovered_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); } if (!legs){ fail(m_io, m_plan.label + ": no plan from " + describe(m_maps, m_from) + "."); @@ -360,16 +363,7 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ const TileLeg& leg = walk.planned.leg; const Milliseconds limit = wall_limit(leg); walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); - // A wild battle may end the walk early. Walking to a wall again ends in - // the same place, so finish the leg. - const bool interrupted = walk.result.wild_battles > 0; - for (size_t again = 0; again < 3 && walk.result.wild_battles > 0 && - (walk.result.stop == WalkStop::LIMIT || walk.result.stop == WalkStop::BLOCKED); again++ - ){ - m_io.note("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again.", RouteNote::LOG); - walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label, .options = run_watching(walk)}); - } - if (interrupted){ + if (finish_interrupted_wall(walk, limit)){ return LegNext::NEXT_LEG; } // A wall well before the nearest one the map has: something the map @@ -406,6 +400,30 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ return LegNext::NEXT_LEG; } +bool RouteWalker::finish_interrupted_wall(LegWalk& walk, Milliseconds limit){ + if (walk.result.wild_battles == 0){ + return false; + } + for (size_t again = 0; again < 3 && walk.result.wild_battles > 0; again++){ + if (walk.result.origin == WalkOrigin::MAY_PRECEDE_LEG){ + fail(m_io, "encounter_origin_unknown: " + walk.label); + } + if (walk.result.stop != WalkStop::LIMIT && walk.result.stop != WalkStop::BLOCKED){ + fail(m_io, "unconfirmed transition/dialogue during wall recovery: " + walk.label); + } + m_io.note("[Route] " + walk.label + ": a wild battle on the way; walking to the wall again.", RouteNote::LOG); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, + .label = walk.label, .options = run_watching(walk)}); + } + if (walk.result.wild_battles > 0){ + fail(m_io, "recovery budget exhausted: " + walk.label); + } + if (walk.result.stop != WalkStop::BLOCKED){ + check_at_wall(walk); + } + return true; +} + // A walk to the wall that stopped well short of it: tap through a corner, // then wait for someone in the way. Returns the tiles walked by then. size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest){ @@ -486,8 +504,12 @@ void RouteWalker::check_at_wall(LegWalk& walk){ // however far it got, so walk on to it and carry on. void RouteWalker::on_to_wall(LegWalk& walk){ m_io.note("[Route] " + walk.label + ": on to the wall after the battle.", RouteNote::WARNING); - m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = wall_limit(walk.planned.leg), + const Milliseconds limit = wall_limit(walk.planned.leg); + walk.result = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = limit, .label = walk.label + ": on to the wall", .options = run_watching(walk)}); + if (!finish_interrupted_wall(walk, limit) && walk.result.stop != WalkStop::BLOCKED){ + check_at_wall(walk); + } m_from = walk.planned.after; log_leg_done(m_io, m_maps, m_plan.label, walk.planned, 0); } @@ -504,11 +526,8 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ if (leg.tiles > walk.turn_moved){ walk.result = m_io.walk_taps(leg.direction, leg.tiles - walk.turn_moved, walk.label); } - // A wild battle starts once the step onto the grass is done, so the - // taps still count; the walk went on after it. if (walk.result.wild_battles > 0){ - m_io.note("[Route] " + walk.label + ": " + std::to_string(walk.result.wild_battles) + - " wild battle(s) on the way; the tap count holds.", RouteNote::WARNING); + return interrupted_open_leg(walk); } // Taps that moved nothing: someone stands in the way (the Youngster // below the Ravaged Path wanders across the row under the cliff, r239a, @@ -527,6 +546,12 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ walk.result.stop = rest.stop; walk.result.walked += rest.walked; walk.result.wild_battles += rest.wild_battles; + if (rest.origin == WalkOrigin::MAY_PRECEDE_LEG){ + walk.result.origin = rest.origin; + } + } + if (walk.result.wild_battles > 0){ + return interrupted_open_leg(walk); } if (walk.result.stop == WalkStop::BLOCKED && moved < leg.tiles){ return blocked_open_leg(walk, moved); @@ -534,6 +559,19 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ return LegNext::NEXT_LEG; } +LegNext RouteWalker::interrupted_open_leg(LegWalk& walk){ + if (walk.result.origin == WalkOrigin::MAY_PRECEDE_LEG){ + fail(m_io, "encounter_origin_unknown: " + walk.label); + } + if (walk.result.stop != WalkStop::LIMIT && walk.result.stop != WalkStop::BLOCKED){ + fail(m_io, "interrupted open leg has an unconfirmed dialogue/transition: " + walk.label); + } + auto possible = interrupted_leg_positions(m_grid, m_from, walk.planned.leg.direction, walk.planned.leg.tiles); + const auto next = replan_from(walk, std::move(possible), "encounter: credited taps do not establish pose"); + m_facing.reset(); + return next; +} + // Turns to the leg's way with a tap too short to step (50 ms: on the // console 40 to 80 ms taps only turned). The leg's taps then each move a // tile. The old 160 ms "turn" tap stepped whichever way we faced, and From 5819bc08916d6671db0df4b0215b658a4c11e149 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 00:31:10 -0800 Subject: [PATCH 32/39] BDSP: fold #226 fixes into the walk and location reader Moves #226's versions of these files into the PR that owns them. #226 commits: 0d35443b9 AutoStory: shared screen events and judges; BDSP and FRLG adapters (#268) (#290) 0192d6bb5 BDSP AutoStory: a warp is a fade or a banner; walks report warps (#268 slices 1-3) (#284) 5493a9eb8 BDSP AutoStory: routes never run along a wander zone; short wall legs walk every tile (#277) 71c3ed149 BDSP AutoStory: a wall leg that never moved waits, then plans from its start (#271) 0738cd55b BDSP AutoStory: 01.03 runs straight north up column 46 to the Center f2567a2aa BDSP AutoStory: a battle on a counted leg's first tap re-plans to the step's end 65e145a3b BDSP AutoStory: a battle on a route leg's first input re-plans too a0d22048d BDSP walk: do not mark origin as preceding leg when movement moved chunks f9d253be4 BDSP AutoStory: tag late encounter origin unknown only when chunks moved <= 1 5a1626885 BDSP AutoStory: soft cost penalty for wandering hazards and wall check safeguard 9a2eb0a11 BDSP AutoStory: avoid wandering NPC hazards and simulate wandering NPCs ac1ef0b49 BDSP AutoStory: align Oreburgh Mine maps, corridor legs, and Floaroma pathing Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Utils/PokemonBDSP_AutoStory_Location.cpp | 43 ++++ .../Utils/PokemonBDSP_AutoStory_Location.h | 43 ++++ .../PokemonBDSP_AutoStory_PlannedRoute.cpp | 196 ++++++++++++++---- .../PokemonBDSP_AutoStory_PlannedRoute.h | 33 +++ .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 53 +++-- .../Utils/PokemonBDSP_AutoStory_Walk.h | 30 ++- 6 files changed, 342 insertions(+), 56 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp index df31791552..9556ec5018 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -6,11 +6,13 @@ #include #include +#include #include #include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonFramework/ImageTools/ImageStats.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/Language.h" +#include "CommonFramework/VideoPipeline/VideoOverlayScopes.h" #include "CommonTools/Images/ImageFilter.h" #include "CommonTools/OCR/OCR_RawTesseractOCR.h" #include "PokemonBDSP_AutoStory_Location.h" @@ -143,6 +145,47 @@ bool location_banner_matches(const std::string& read, const std::string& expecte } +std::string LocationBannerReads::read(const ImageViewRGB32& screen, WallClock now){ + if (!location_banner_showing(screen)){ + m_reads = 0; + return ""; + } + if (m_reads >= 2 || (m_reads == 1 && now - m_first < std::chrono::milliseconds(300))){ + return ""; + } + if (m_reads == 0){ + m_first = now; + } + m_reads++; + return read_location_banner(screen); +} + +LocationBannerWatcher::LocationBannerWatcher(std::string expected) + : VisualInferenceCallback("LocationBannerWatcher") + , m_expected(std::move(expected)) +{} +void LocationBannerWatcher::make_overlays(VideoOverlaySet& items) const{ + items.add(COLOR_CYAN, LOCATION_BANNER_WHITE); +} +bool LocationBannerWatcher::process_frame(const ImageViewRGB32& frame, WallClock timestamp){ + const std::string read = m_reads.read(frame, timestamp); + if (read.empty() || m_matched){ + return m_matched; + } + m_last_read = read; + if (!m_expected.empty() && location_banner_matches(read, m_expected)){ + m_seen = read; + m_matched = true; + return true; + } + if (m_seen.empty() && m_expected.empty()){ + m_seen = read; + } + return false; +} + + + } // namespace PokemonBDSP } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h index cb4e4c8626..d2baff6d70 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h @@ -11,8 +11,10 @@ #define PokemonAutomation_PokemonBDSP_AutoStory_Location_H #include +#include "Common/Cpp/Time.h" #include "CommonFramework/Language.h" #include "CommonFramework/ImageTypes/ImageViewRGB32.h" +#include "CommonTools/InferenceCallbacks/VisualInferenceCallback.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -63,6 +65,47 @@ std::string read_save_screen_location(const ImageViewRGB32& screen); // read as Route 202. bool location_banner_matches(const std::string& read, const std::string& expected); +// Reads the location banner at most twice per showing: when its box shows, +// and once more 300 ms later in case the first frame caught it sliding in. +// The box test runs on every frame; OCR (tens of ms) only then (#268's cost +// rule: the old look read every poll while the box showed, 30 to 40 reads). +class LocationBannerReads{ +public: + // The banner's text if this frame was read, "" if not. + std::string read(const ImageViewRGB32& screen, WallClock now); + +private: + size_t m_reads = 0; + WallClock m_first = WallClock::min(); +}; + +// Watches the location banner through an action. With `expected` (a map's +// banner name) it stops the action when that banner reads; without, it +// only keeps the first banner read. +class LocationBannerWatcher : public VisualInferenceCallback{ +public: + explicit LocationBannerWatcher(std::string expected = ""); + + virtual void make_overlays(VideoOverlaySet& items) const override; + virtual bool process_frame(const ImageViewRGB32& frame, WallClock timestamp) override; + + // The expected banner if it read, else the first banner read; "" if none. + const std::string& seen() const{ + return m_seen; + } + // The last banner read, for the log. + const std::string& last_read() const{ + return m_last_read; + } + +private: + std::string m_expected; + LocationBannerReads m_reads; + std::string m_seen; + std::string m_last_read; + bool m_matched = false; +}; + } // namespace PokemonBDSP } // namespace NintendoSwitch diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp index 0d5f55b236..5a8263eeb2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.cpp @@ -69,23 +69,6 @@ void log_leg_done(RouteIO& io, const StoryWorld& maps, const std::string& label, assumed ? EndBasis::ASSUMED : EndBasis::WALKED, assumed ? std::to_string(wild_battles) + " wild battle(s) on the way" : ""), RouteNote::LOG); } -// The tiles the player passes when walking `tiles` tiles from `from`, the -// first one after `from` first. Stops early where the player is stopped. -std::vector leg_path(const StoryWorld& grid, TilePos from, Direction direction, size_t tiles){ - std::vector path; - size_t travelled = 0; - while (travelled < tiles){ - const std::optional next = step(grid, from, direction); - if (!next){ - break; - } - travelled += std::abs(next->x - from.x) + std::abs(next->y - from.y); - from = *next; - path.push_back(from); - } - return path; -} - // The map all of `tiles` are on, or empty if they are on several or none. std::string map_of(const StoryWorld& maps, const std::vector& tiles){ std::string name; @@ -154,16 +137,51 @@ constexpr RouteInput WAIT_FOR_THE_WAY{.kind = RouteInputKind::WAIT, .direction = // Tiles a walk to the wall moved, from the time it walked. size_t tiles_moved(const WalkResult& walk, Milliseconds per_tile){ + if (walk.stop == WalkStop::LIMIT && walk.moved_chunks > 0){ + const Milliseconds moving = walk.walked - ROUTE_START_DELAY; + const size_t tiles_from_time = moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); + const size_t tiles_from_chunks = (walk.moved_chunks * 400ms) / per_tile; + return std::min(tiles_from_time, std::max(walk.moved_chunks, tiles_from_chunks)); + } const Milliseconds moving = walk.walked - ROUTE_START_DELAY - (walk.stop == WalkStop::BLOCKED ? 800ms : 0ms); return moving.count() <= 0 ? (size_t)0 : (size_t)(moving / per_tile); } +// Whether a walk to the wall never moved: no chunk changed and its time +// counts no tile. It stopped where it began, so whoever stopped it stands +// on the next tile, however short the leg. On lab-B (2026-11-08 16:50, +// 01.08) Route 204's youngster, who wanders (12, 2) to (14, 2), stood a +// tile ahead of the leg "west 2 (to wall)" from (13, 2). The wait for the +// way needed a stop three tiles short of the wall, so he was talked to at +// once, and the replan from both (12, 2) and (13, 2) found no plan: an +// uncertain start needs a wall both tiles reach, and no wall is anchored +// from a tile someone wanders on. +bool never_moved(const WalkResult& walk, size_t walked_tiles){ + return walk.stop == WalkStop::BLOCKED && walk.moved_chunks == 0 && walked_tiles == 0; +} + +// Legs to a wall this many tiles or shorter must walk every tile: a tile +// short is someone in the way, not the time count's slack. On lab-B +// (2026-11-08, 01.08) Route 204's youngster stopped "west 2 (to wall)" +// after 1 tile; a tile short passed as at the wall, the route logged +// arriving at (11, 2) from (13, 2), and the taps north hit the cliff. +constexpr size_t EXACT_WALL_LEG_TILES = 3; + +// The fewest tiles a leg to a wall `expected_tiles` away must have walked +// to count as at its wall. +size_t min_tiles_at_wall(size_t expected_tiles){ + return expected_tiles > EXACT_WALL_LEG_TILES ? expected_tiles * 7 / 10 : expected_tiles; +} + // Whether a leg's walk goes on to the next leg or the route plans again. enum class LegNext{ NEXT_LEG, REPLAN, + ARRIVED_EARLY, // through a warp at the leg's end (walk_route_to_warp()) }; +// How far past a leg's end a walk that ran on may have gone. +constexpr size_t WARP_SLIDE_TILES = 2; // One planned leg as it is walked: where it goes, and the location // banners read on the way. @@ -214,7 +232,7 @@ class RouteWalker{ , m_plan(plan) {} - std::vector run(std::vector from); + RouteEnd run(std::vector from); private: std::vector plan_legs() const; @@ -226,7 +244,7 @@ class RouteWalker{ bool finish_interrupted_wall(LegWalk& walk, Milliseconds limit); size_t unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t nearest); LegNext stopped_short_of_wall(LegWalk& walk, size_t walked_tiles); - void check_at_wall(LegWalk& walk); + void check_at_wall(LegWalk& walk, size_t walked_tiles = SIZE_MAX, size_t expected_tiles = SIZE_MAX); void on_to_wall(LegWalk& walk); // Open legs, tapped. @@ -258,11 +276,12 @@ class RouteWalker{ // Every banner read on the way, to check the map we arrive on against. std::vector m_all_banners; std::string m_map_start; + WarpSeen m_warp; // Dialogue the map does not explain, so far on this route. size_t m_unexplained_talks = 0; }; -std::vector RouteWalker::run(std::vector from){ +RouteEnd RouteWalker::run(std::vector from){ // Tiles to avoid are ground the game lets us walk on (a trainer's line // of sight), so they are off the map, not walls: a leg must not end // "at" one, or the walk would run on through it. @@ -277,12 +296,12 @@ std::vector RouteWalker::run(std::vector from){ const std::vector legs = plan_legs(); m_io.note(m_plan.label + ": " + std::to_string(legs.size()) + " legs planned", RouteNote::STATUS); log_plan(m_io, m_maps, m_plan.label, m_from, legs); - bool replanned = false; - for (size_t number = 1; number <= legs.size() && !replanned; number++){ - replanned = walk_leg(legs[number - 1], number, legs.size()) == LegNext::REPLAN; + LegNext next = LegNext::NEXT_LEG; + for (size_t number = 1; number <= legs.size() && next == LegNext::NEXT_LEG; number++){ + next = walk_leg(legs[number - 1], number, legs.size()); } - if (!replanned){ - return arrive(); + if (next != LegNext::REPLAN){ + return {next == LegNext::ARRIVED_EARLY ? m_from : arrive(), m_warp}; } } fail(m_io, m_plan.label + ": too many plans."); @@ -298,6 +317,13 @@ std::vector RouteWalker::plan_legs() const{ "; trying the default costs.", RouteNote::WARNING); legs = plan_recovered_route(m_grid, m_from, m_plan.goal, RobustPlanCosts()); } + if (!legs && !m_plan.avoid.empty()){ + m_io.note("[Route] " + m_plan.label + ": no plan avoiding hazard tiles; trying without hazard avoidance.", RouteNote::WARNING); + legs = plan_recovered_route(m_maps, m_from, m_plan.goal, m_plan.costs); + if (!legs){ + legs = plan_recovered_route(m_maps, m_from, m_plan.goal, RobustPlanCosts()); + } + } if (!legs){ fail(m_io, m_plan.label + ": no plan from " + describe(m_maps, m_from) + "."); } @@ -317,10 +343,15 @@ LegNext RouteWalker::walk_leg(const PlannedLeg& planned, size_t number, size_t c return next; } m_facing = planned.leg.direction; - check_banners(walk); if (walk.result.stop == WalkStop::TRANSITION){ - fail(m_io, walk.label + ": unexpected transition."); + if (!warp_within(m_grid, m_from, planned.leg.direction, planned.leg.tiles + WARP_SLIDE_TILES)){ + fail(m_io, walk.label + ": unexpected transition."); + } + m_io.note("[Route] " + walk.label + ": through the warp at its end (" + describe(walk.result.warp) + "); handing over.", RouteNote::WARNING); + m_warp = walk.result.warp; + return LegNext::ARRIVED_EARLY; } + check_banners(walk); return after_leg(walk); } @@ -395,7 +426,11 @@ LegNext RouteWalker::wall_leg(LegWalk& walk){ // into the lab). Out of tall grass a stop reads, so walk on: a player // still moving is not where the plan has him. if (walk.result.stop == WalkStop::LIMIT && route_wall_readable(m_grid, walk.planned.after)){ - check_at_wall(walk); + // A short leg a tile short stopped against someone in the way. + if (nearest <= EXACT_WALL_LEG_TILES && walked_tiles < nearest){ + return stopped_short_of_wall(walk, walked_tiles); + } + check_at_wall(walk, walked_tiles, nearest); } return LegNext::NEXT_LEG; } @@ -404,10 +439,9 @@ bool RouteWalker::finish_interrupted_wall(LegWalk& walk, Milliseconds limit){ if (walk.result.wild_battles == 0){ return false; } + // A battle that may have begun before the hold changes nothing here: + // from where the leg began, it ends at its wall however far it got. for (size_t again = 0; again < 3 && walk.result.wild_battles > 0; again++){ - if (walk.result.origin == WalkOrigin::MAY_PRECEDE_LEG){ - fail(m_io, "encounter_origin_unknown: " + walk.label); - } if (walk.result.stop != WalkStop::LIMIT && walk.result.stop != WalkStop::BLOCKED){ fail(m_io, "unconfirmed transition/dialogue during wall recovery: " + walk.label); } @@ -442,8 +476,13 @@ size_t RouteWalker::unstick_wall_leg(LegWalk& walk, size_t walked_tiles, size_t walked_tiles += tiles_moved(walk.result, per_tile); } // Someone walking about may stand in the way for a moment: wait and - // walk to the wall again before calling it an obstacle. - for (size_t again = 0; again < 2 && walk.result.stop == WalkStop::BLOCKED && walked_tiles + 2 < nearest; again++){ + // walk to the wall again before calling it an obstacle. A walk that + // never moved waits on a short leg too (never_moved()). + const auto in_the_way = [&]{ + return walk.result.stop == WalkStop::BLOCKED && + (walked_tiles + 2 < nearest || (nearest > 0 && never_moved(walk.result, walked_tiles))); + }; + for (size_t again = 0; again < 2 && in_the_way(); again++){ m_io.note("[Route] " + walk.label + ": stopped after about " + std::to_string(walked_tiles) + " tiles, short of the wall; waiting for the way to clear.", RouteNote::WARNING); m_io.press(WAIT_FOR_THE_WAY); @@ -457,10 +496,13 @@ LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ // Someone who stays put: talk to them. A trainer we walked up to from // the side starts the battle this way. talk_to_blocker(walk.label); + // A walk that never moved is where it began (never_moved()); any other + // stop may be a tile either side of the tiles its time counts. + const size_t last = never_moved(walk.result, walked_tiles) ? 0 : walked_tiles + 1; std::vector stopped; for (const TilePos& pos : m_from){ const std::vector path = leg_path(m_grid, pos, walk.planned.leg.direction, SIZE_MAX); - for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= walked_tiles + 1 && k <= path.size(); k++){ + for (size_t k = walked_tiles == 0 ? 0 : walked_tiles - 1; k <= last && k <= path.size(); k++){ stopped.push_back(k == 0 ? pos : path[k - 1]); } } @@ -472,7 +514,13 @@ LegNext RouteWalker::stopped_short_of_wall(LegWalk& walk, size_t walked_tiles){ return replan_from(walk, std::move(stopped), "stopped early after about " + std::to_string(walked_tiles) + " tiles"); } -void RouteWalker::check_at_wall(LegWalk& walk){ +void RouteWalker::check_at_wall(LegWalk& walk, size_t walked_tiles, size_t expected_tiles){ + if (expected_tiles != SIZE_MAX && expected_tiles > 0 && walked_tiles != SIZE_MAX){ + if (walked_tiles < min_tiles_at_wall(expected_tiles)){ + fail(m_io, walk.label + ": ran out of time well short of the wall (" + + std::to_string(walked_tiles) + " of " + std::to_string(expected_tiles) + " tiles): not where the plan has us."); + } + } WalkResult check = m_io.walk_to_wall({.direction = walk.planned.leg.direction, .limit = 2000ms, .label = walk.label + ": at the wall?", .options = {.arrived = nullptr, .pace = DpadPace::RUN}}); if (check.stop == WalkStop::DIALOG){ @@ -559,10 +607,12 @@ LegNext RouteWalker::open_leg(LegWalk& walk){ return LegNext::NEXT_LEG; } +// Where an open leg's battle left us: any tile of the leg from where it +// began, none moved to all. A battle that may have begun before the leg +// (WalkOrigin::MAY_PRECEDE_LEG) found us on the tile the leg before ended +// on, since a battle starts on a tile stepped onto, so the same tiles +// hold. Planning again from them is bounded by run()'s plans. LegNext RouteWalker::interrupted_open_leg(LegWalk& walk){ - if (walk.result.origin == WalkOrigin::MAY_PRECEDE_LEG){ - fail(m_io, "encounter_origin_unknown: " + walk.label); - } if (walk.result.stop != WalkStop::LIMIT && walk.result.stop != WalkStop::BLOCKED){ fail(m_io, "interrupted open leg has an unconfirmed dialogue/transition: " + walk.label); } @@ -799,6 +849,17 @@ std::vector walk_route( std::vector walk_route( RouteIO& io, const StoryWorld& maps, std::vector from, RouteTrainers& trainers, const RouteWalkPlan& plan +){ + RouteEnd end = walk_route_to_warp(io, maps, std::move(from), trainers, plan); + if (end.warp.any()){ + fail(io, plan.label + ": unexpected transition."); + } + return std::move(end.tiles); +} + +RouteEnd walk_route_to_warp( + RouteIO& io, const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan ){ return RouteWalker(io, maps, trainers, plan).run(std::move(from)); } @@ -861,6 +922,65 @@ void talk_to_npc( fail(io, talk.label + ": no side of " + describe(maps, talk.npc) + " started a dialogue."); } +namespace{ + +// Waits for the way at most this often in one counted leg: Oreburgh's +// Hiker tries a step every 2 s, and may stay or keep to the column. +constexpr size_t COUNTED_WALL_WAITS = 8; + +// A count by time is off by a tile or so a stop. Short of the wall by more +// than WAIT_SHORT_OF is someone in the way (Oreburgh's Hiker stops us 4 +// to 7 tiles short of the Center's wall); past COUNT_SLACK either way at +// the end, the walk is not where the step has us. +constexpr size_t COUNTED_WALL_WAIT_SHORT_OF = 2; +constexpr size_t COUNTED_WALL_COUNT_SLACK = 3; + +// One hold of `leg` to the wall; fails on anything but a stop or the time. +WalkResult hold_counted_wall_leg(RouteIO& io, const CountedWallLeg& leg){ + const WalkResult result = io.walk_to_wall({.direction = leg.direction, + .limit = wall_limit({.direction = leg.direction, .tiles = leg.most}), .label = leg.label, + .options = {.arrived = nullptr, .pace = DpadPace::RUN}}); + if (result.stop != WalkStop::BLOCKED && result.stop != WalkStop::LIMIT){ + fail(io, leg.label + ": " + walk_stop_name(result.stop) + " instead of a stop."); + } + if (result.wild_battles > 0){ + fail(io, leg.label + ": a wild battle on the way; the count of tiles is lost."); + } + return result; +} + +} // namespace + +size_t walk_counted_wall_leg(RouteIO& io, const CountedWallLeg& leg){ + const Milliseconds per_tile = route_ms_per_tile(leg.direction, leg.speed); + const size_t slack = COUNTED_WALL_COUNT_SLACK; + size_t walked = 0; + for (size_t waits = 0;; waits++){ + const WalkResult result = hold_counted_wall_leg(io, leg); + walked += tiles_moved(result, per_tile); + if (result.stop == WalkStop::LIMIT){ + // The stop went unseen: we must be held against the wall. + if (!io.holding_against_wall(leg.direction, leg.label + ": at the wall?")){ + fail(io, leg.label + ": ran out of time after about " + std::to_string(walked) + " tiles, short of the wall."); + } + break; + } + if (walked + COUNTED_WALL_WAIT_SHORT_OF >= leg.fewest || waits == COUNTED_WALL_WAITS){ + break; + } + io.note("[Route] " + leg.label + ": stopped after about " + std::to_string(walked) + + " tiles, short of the wall; waiting for the way to clear.", RouteNote::WARNING); + io.press(WAIT_FOR_THE_WAY); + } + io.note(std::format("[Route] {}: about {} tiles to the wall; the map has {} to {}.", leg.label, walked, leg.fewest, leg.most), + RouteNote::LOG); + if (walked + slack < leg.fewest || walked > leg.most + slack){ + fail(io, std::format("{}: about {} tiles to the wall, where the map has {} to {}: not where the step has us.", + leg.label, walked, leg.fewest, leg.most)); + } + return walked; +} + } // namespace PokemonBDSP diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h index 5995c93a17..777019cfdd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h @@ -71,6 +71,21 @@ std::vector walk_route( RouteTrainers& trainers, const RouteWalkPlan& plan ); +// Where walk_route_to_warp() ended: on `tiles`, or through a warp it saw +// (`warp`), with `tiles` where the leg that went through began. +struct RouteEnd{ + std::vector tiles; + WarpSeen warp = {}; +}; +// walk_route(), but a fade on a leg with a warp at most a tile or two past +// its end ends the walk there, as arrived early: the walk ran on onto the +// warp (01.05's Oreburgh Gate, lab-B 2026-11-08), and the step after goes +// through it (#268 slice 2). Any other fade fails as in walk_route(). +RouteEnd walk_route_to_warp( + RouteIO& io, const StoryWorld& maps, std::vector from, + RouteTrainers& trainers, const RouteWalkPlan& plan +); + // The corners of a line a human walked, and the costs to plan each // stretch with. struct WaypointWalkPlan{ @@ -113,6 +128,24 @@ void talk_to_npc( RouteTrainers& trainers, const NpcTalk& talk ); +// A leg held toward a wall the map has between `fewest` and `most` tiles +// ahead (from each tile the player may stand on), its tiles counted by time +// at `speed`. +struct CountedWallLeg{ + Pokemon::Direction direction; + size_t fewest; + size_t most; + MoveSpeed speed; + std::string label; +}; +// Walks `leg` to its wall and returns about how many tiles it took. Someone +// walking about who stops it short is waited for, and the walk goes on +// (Oreburgh's Hiker crosses column 46 on rows 25 to 27). Fails when the +// count is far off the map's: a walk that went astray is not at its wall +// (lab-B 2026-11-08 13:50: a hold east from (48, 57) moved 0 where the map +// has (49, 57) open), nor is one through a door or into a dialogue. +size_t walk_counted_wall_leg(RouteIO& io, const CountedWallLeg& leg); + } // namespace PokemonBDSP diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index 9050540834..72fdbbfd50 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -9,6 +9,7 @@ #include #include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageBoxes.h" #include "CommonFramework/ImageTools/ImageDiff.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "CommonFramework/VideoPipeline/VideoFeed.h" @@ -20,6 +21,7 @@ #include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" #include "PokemonBDSP/Inference/PokemonBDSP_TransitionWatchers.h" #include "PokemonBDSP/Inference/Battles/PokemonBDSP_BattleMenuDetector.h" +#include "PokemonBDSP_AutoStory_Location.h" #include "PokemonBDSP_AutoStory_Walk.h" namespace PokemonAutomation{ @@ -40,6 +42,7 @@ const char* walk_stop_name(WalkStop stop){ return "?"; } + double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after){ // Small copies: only the whole-view scroll matters, not detail. return ImageMatch::pixel_RMSD(before.scale_to(192, 108), after.scale_to(192, 108)); @@ -80,7 +83,7 @@ DpadPosition dpad_toward(JoystickPosition stick){ bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context){ BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); - BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 100, 10, + BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, FADE_MAX_DARK_SUM, FADE_MAX_STDDEV_SUM, BlackScreenWatcher::FinderType::GONE, 500ms); const int ret = wait_until(stream, context, 10s, {{battle_menu}, {battle_dialog}, {gone}}); if (ret == 2){ @@ -95,12 +98,16 @@ bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context) // A grass encounter opens with a leaf animation, neither white nor black, and // shows the battle's text box a few seconds later. After a change of the // whole screen that walking cannot make, wait for the battle to show. -bool battle_follows(VideoStream& stream, ProControllerContext& context){ +bool battle_follows(VideoStream& stream, ProControllerContext& context, Milliseconds wait = 5000ms){ BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); - return wait_until(stream, context, 5s, {{battle_menu}, {battle_dialog}}) >= 0; + return wait_until(stream, context, wait, {{battle_menu}, {battle_dialog}}) >= 0; } +// After a white flash: an encounter's, if its battle shows; else a cave +// exit's white fade (#268 slice 3), which the walk once fled as a battle. +constexpr Milliseconds WHITE_FLASH_BATTLE_WAIT = 8000ms; + } // namespace // A step in tall grass that was started before the stick was released still @@ -250,19 +257,24 @@ class WalkUntil{ } private: + // The banner watcher only looks (m_banner keeps the first read); a + // fade ends the chunk: dark (fade_of()'s uniform test, a cave's dim + // fade too) or the white of an encounter or a cave exit. int hold(Milliseconds chunk){ WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BlackScreenWatcher black(COLOR_RED); + BlackScreenWatcher black(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, FADE_MAX_DARK_SUM, FADE_MAX_STDDEV_SUM); BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); ShortDialogWatcher dialog(COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); - return run_until( + const int ret = run_until( m_stream, m_context, [&](ProControllerContext& context){ press(context, chunk); }, - {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}} + {{encounter_flash}, {black}, {battle_menu}, {dialog}, {battle_dialog}, {m_banner}} ); + m_result.warp.banner = m_banner.seen(); + return ret; } void press(ProControllerContext& context, Milliseconds chunk) const{ @@ -286,10 +298,13 @@ class WalkUntil{ // The watchers: 0 the encounter flash, 1 black, 2 the battle menu, 3 a // dialogue, 4 the battle's text box. ChunkVerdict after_watchers(int ret){ - bool battle = ret == 0 || ret == 2 || ret == 4; - if (ret == 1){ - battle = black_screen_was_battle(m_stream, m_context); + bool battle = ret == 2 || ret == 4; + if (ret == 0 || ret == 1){ + battle = ret == 0 + ? battle_follows(m_stream, m_context, WHITE_FLASH_BATTLE_WAIT) + : black_screen_was_battle(m_stream, m_context); if (!battle){ + m_result.warp.fade = ret == 0 ? WarpFade::WHITE : WarpFade::BLACK; return {ChunkNext::STOP, WalkStop::TRANSITION}; } } @@ -298,7 +313,7 @@ class WalkUntil{ return {ChunkNext::STOP, WalkStop::DIALOG}; } if (battle){ - escape_battle("wild battle; running."); + escape_battle("wild battle; running.", BattleMovement::NONE); return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; } if (ret == 3){ @@ -318,7 +333,7 @@ class WalkUntil{ return {ChunkNext::STOP, WalkStop::DIALOG}; } if (change > WALK_SCENE_CHANGE && battle_follows(m_stream, m_context)){ - escape_battle("wild battle (leaf opening); running."); + escape_battle("wild battle (leaf opening); running.", BattleMovement::STEP_MOVED); return {ChunkNext::NEXT_CHUNK, WalkStop::LIMIT}; } // Two still chunks in a row, so one dropped frame does not stop us. A @@ -358,9 +373,10 @@ class WalkUntil{ return change < line; } - void escape_battle(const std::string& what){ + enum class BattleMovement{ NONE, STEP_MOVED }; + void escape_battle(const std::string& what, BattleMovement movement){ m_result.wild_battles++; - if (m_chunks <= 1){ + if (m_chunks <= 1 && movement == BattleMovement::NONE){ m_result.origin = WalkOrigin::MAY_PRECEDE_LEG; } if (m_spec.input == WalkInput::TAPS){ @@ -383,7 +399,9 @@ class WalkUntil{ ){ m_result.wild_battles++; late_battle = true; - m_result.origin = WalkOrigin::MAY_PRECEDE_LEG; + if (m_chunks <= 1 && m_result.moved_chunks <= 1){ + m_result.origin = WalkOrigin::MAY_PRECEDE_LEG; + } } // When the final tap of a TAPS leg lagged during frame drops at its // 800ms check (m_unchanged == 1 at WalkStop::LIMIT), it finishes @@ -398,6 +416,9 @@ class WalkUntil{ m_result.moved_chunks++; } } + if (m_result.moved_chunks > 1){ + m_result.origin = WalkOrigin::CURRENT_LEG; + } m_stream.log("[Walk] " + m_spec.label + ": " + walk_stop_name(stop) + " after " + std::to_string(m_result.walked.count()) + "ms, " + std::to_string(m_result.moved_chunks) + " moved chunks, " + @@ -416,6 +437,7 @@ class WalkUntil{ size_t m_unchanged = 0; size_t m_chunks = 0; double m_largest = 0; + LocationBannerWatcher m_banner; }; WalkResult walk_until_spec( @@ -481,7 +503,7 @@ WalkResult walk_for( WalkResult result{WalkStop::LIMIT, duration, 0}; context.wait_for_all_requests(); WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BlackScreenWatcher black(COLOR_RED); + BlackScreenWatcher black(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, FADE_MAX_DARK_SUM, FADE_MAX_STDDEV_SUM); BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); ShortDialogWatcher dialog(COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); @@ -501,6 +523,7 @@ WalkResult walk_for( battle = black_screen_was_battle(stream, context); if (!battle){ result.stop = WalkStop::TRANSITION; + result.warp.fade = WarpFade::BLACK; } } if (battle){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h index ae33d766d7..d3d61b6743 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -14,10 +14,12 @@ #include #include #include "Common/Cpp/Time.h" +#include "CommonFramework/ImageTools/ImageStats.h" #include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "CommonFramework/Tools/VideoStream.h" #include "Controllers/Joystick.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/AutoStory/Pokemon_SceneJudges.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -33,7 +35,7 @@ using WildBattleEscape = std::function; // An encounter already active at the first input, or found only after a -// leg, may belong to earlier movement. Never localize from this leg alone. +// leg, may belong to earlier movement: it began on the tile that movement +// ended on, so the leg's tiles from its start, none moved to all, still +// hold where we are. enum class WalkOrigin{ CURRENT_LEG, MAY_PRECEDE_LEG }; +// The fade and warp judgements are shared (Pokemon_ScreenEvents.h, +// Pokemon_SceneJudges.h); BDSP's walks and steps fill them. +// How a screen's fade looks. +using Pokemon::WarpFade; +// "black fade", for the log. +using Pokemon::warp_fade_name; +// A fade's most texture (the walk's black-screen watcher uses it too). +using Pokemon::FADE_MAX_STDDEV_SUM; +// A dark fade's brightest rgb sum (likewise). +using Pokemon::FADE_MAX_DARK_SUM; +// The fade a screen's middle shows, from its stats. +using Pokemon::fade_of; +// The fade a screen shows. +using Pokemon::fade_of_screen; +// What a walk or a step saw of a warp: a fade or a banner. +using Pokemon::WarpSeen; + // Result metrics and terminal state returned by automated walking execution. struct WalkResult{ WalkStop stop; @@ -57,6 +78,9 @@ struct WalkResult{ // taps into Taylor's freeze). size_t moved_chunks = 0; WalkOrigin origin = WalkOrigin::CURRENT_LEG; + // The fade that ended a TRANSITION, and any location banner read on + // the way (#268 slice 2: walkers report warps). + WarpSeen warp = {}; }; // Holds the left stick at `stick` until one of the WalkStop events. Wild battles on @@ -160,7 +184,7 @@ WalkResult walk_to_talk( // place against trees. double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& after); // Screen change metric threshold indicative of blocked movement against an obstacle. -constexpr double WALK_BLOCKED_CHANGE = 10.0; +constexpr double WALK_BLOCKED_CHANGE = 16.0; // For whole-tile D-pad taps: a tap that moves changes the screen by 21 to 50 // (r244a: 40 per tap along Route 204's row 27); a tap into a wall or an NPC // bumps for 10 to 13.4, and an NPC walking past does the same, so 10 counted From a369dcf13bcc37b97f22f24782d41ac5ae32ad72 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 02:25:16 -0800 Subject: [PATCH 33/39] BDSP AutoStory: WarpFadeWatch follows a warp's fade on the shared TransitionTracker A warp's fade is judged frame by frame on BDSP_FADE_PROFILE: its dark or white held as the step's rule says, or, when the hold fell between two frames, the first frame of another scene after the fall. The walk's fade watchers read their thresholds from the same profile (same numbers). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 61 ++++++++++++++++++ .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 64 ++++++++++++++++++- .../Utils/PokemonBDSP_AutoStory_Walk.h | 49 ++++++++++++++ 3 files changed, 171 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp index e29b302195..1816cc9094 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -14,6 +14,7 @@ #include "Utils/PokemonBDSP_AutoStory_Indoors.h" #include "Utils/PokemonBDSP_AutoStory_Location.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "Utils/PokemonBDSP_AutoStory_Walk.h" #include "PokemonBDSP_AutoStory_Walk_Tests.h" namespace PokemonAutomation{ @@ -48,6 +49,62 @@ UnitTestResult test_route_turn_tap(Logger&){ return true; } +// WarpFadeWatch on synthetic frames 33 ms apart, each a brightness and a +// texture (stddev a channel). Two scenes differ here when their +// brightness does. +struct FadeFrame{ + double brightness; + double stddev; +}; +struct WarpFadeCase{ + std::string name; + WarpFadeRule rule; + std::vector frames; + // The frame (1-based) the fade shows on, 0 for none, and how. + size_t seen_at; + WarpFade fade; +}; + +UnitTestResult test_warp_fade_watch(Logger&){ + constexpr WarpFadeRule DOOR{.partial_dark = true, .dark_for = Milliseconds(50), .white_for = Milliseconds(60)}; + constexpr WarpFadeRule TAP{}; + constexpr FadeFrame STREET{104, 20}; + constexpr FadeFrame ROOM{130, 20}; + constexpr FadeFrame FALL{55, 10}; + constexpr FadeFrame BLACK{7, 1}; + const std::vector cases{ + {.name = "a door's fade, its hold seen", .rule = DOOR, .frames = {STREET, FALL, BLACK, BLACK, BLACK, ROOM}, .seen_at = 4, .fade = WarpFade::BLACK}, + {.name = "a tap's fade, its hold seen", .rule = TAP, .frames = {STREET, FALL, BLACK, ROOM}, .seen_at = 3, .fade = WarpFade::BLACK}, + {.name = "a short fade, its hold between two frames", .rule = TAP, .frames = {STREET, FALL, ROOM}, .seen_at = 3, .fade = WarpFade::BLACK}, + {.name = "a door's short fade, its hold between two frames", .rule = DOOR, .frames = {STREET, FALL, ROOM}, .seen_at = 3, .fade = WarpFade::BLACK}, + {.name = "a dip back to the same street", .rule = TAP, .frames = {STREET, FALL, STREET}, .seen_at = 0, .fade = WarpFade::NONE}, + {.name = "a dim cave floor (46) is no fade", .rule = TAP, .frames = {STREET, {46, 20}, {46, 20}, {46, 20}}, .seen_at = 0, .fade = WarpFade::NONE}, + {.name = "a cave mouth's dim fade (55)", .rule = TAP, .frames = {STREET, {80, 10}, {55, 1}, ROOM}, .seen_at = 3, .fade = WarpFade::BLACK}, + {.name = "a cave exit's white, its hold between two frames", .rule = TAP, .frames = {STREET, {180, 10}, ROOM}, .seen_at = 3, .fade = WarpFade::WHITE}, + {.name = "a cave exit's white", .rule = DOOR, .frames = {STREET, {250, 1}, {250, 1}, {250, 1}}, .seen_at = 4, .fade = WarpFade::WHITE}, + }; + for (const WarpFadeCase& item : cases){ + WarpFadeWatch watch(item.rule, [](const Pokemon::TransitionSample& before, const Pokemon::TransitionSample& after){ + return after.middle.average.r != before.middle.average.r ? Pokemon::SceneChange::DIFFERENT : Pokemon::SceneChange::SAME; + }); + size_t seen_at = 0; + WarpFade fade = WarpFade::NONE; + for (size_t i = 0; i < item.frames.size() && seen_at == 0; i++){ + const FadeFrame& frame = item.frames[i]; + fade = watch.push({ + .time = WallClock() + Milliseconds(33 * (int64_t)i), + .middle = ImageStats(FloatPixel(frame.brightness, frame.brightness, frame.brightness), FloatPixel(frame.stddev, frame.stddev, frame.stddev), 1), + }); + seen_at = fade == WarpFade::NONE ? 0 : i + 1; + } + if (seen_at != item.seen_at || fade != item.fade){ + return item.name + ": " + warp_fade_name(fade) + " at frame " + std::to_string(seen_at) + ", expected " + + warp_fade_name(item.fade) + " at " + std::to_string(item.seen_at) + "."; + } + } + return true; +} + // indoors() decides when leave_door_south() is out: r257 walked on south // past Sandgem's road into a house after leaving the Center, because the // evening tint made the outside as warm as a room. Synthetic screens: the @@ -248,6 +305,10 @@ const AutoStoryWalkCase AUTO_STORY_WALK_CASES[] = { .input = {"out of a door needs its fade", test_door_fade}, .expectation = {}, }, + { + .input = {"a warp's fade, followed frame by frame, its hold seen or not", test_warp_fade_watch}, + .expectation = {}, + }, { .input = {"location banner match", test_location_banner}, .expectation = {}, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index 72fdbbfd50..1173f948a2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -7,6 +7,7 @@ */ #include +#include #include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/ImageTools/ImageBoxes.h" @@ -48,6 +49,63 @@ double walk_screen_change(const ImageViewRGB32& before, const ImageViewRGB32& af return ImageMatch::pixel_RMSD(before.scale_to(192, 108), after.scale_to(192, 108)); } +Pokemon::SceneChange walk_scenes_differ(const Pokemon::TransitionSample& before, const Pokemon::TransitionSample& after){ + if (!before.frame || !after.frame){ + return Pokemon::SceneChange::UNKNOWN; + } + return walk_screen_change(*before.frame.frame, *after.frame.frame) > WALK_SCENE_CHANGE + ? Pokemon::SceneChange::DIFFERENT + : Pokemon::SceneChange::SAME; +} + + +namespace{ + +// Keeps when `on` began: set on its first frame, cleared on a frame without. +void hold_since(std::optional& since, bool on, WallClock time){ + if (!on){ + since.reset(); + }else if (!since){ + since = time; + } +} + +} // namespace + +WarpFadeWatch::WarpFadeWatch(WarpFadeRule rule, Pokemon::TransitionTracker::CompareScenes compare) + : m_rule(rule) + , m_tracker(Pokemon::BDSP_FADE_PROFILE, std::move(compare)) +{} + +WarpFade WarpFadeWatch::push(const Pokemon::TransitionSample& sample){ + const Pokemon::FadeProfile& profile = Pokemon::BDSP_FADE_PROFILE; + const std::optional done = m_tracker.push(sample); + const double stddev = sample.middle.stddev.sum(); + const double sum = sample.middle.average.sum(); + const double dark_stddev = m_rule.partial_dark ? profile.partial_max_stddev_sum : profile.uniform_max_stddev_sum; + const bool dark = stddev <= dark_stddev && sum <= profile.dark_max_sum; + const bool white = stddev <= profile.uniform_max_stddev_sum && sum >= profile.white_min_sum; + if (dark && !m_dark_since){ + m_went_dark = sample.time; + } + hold_since(m_dark_since, dark, sample.time); + hold_since(m_white_since, white, sample.time); + if (m_dark_since && sample.time - *m_dark_since >= m_rule.dark_for){ + m_how = "dark"; + return WarpFade::BLACK; + } + if (m_white_since && sample.time - *m_white_since >= m_rule.white_for){ + m_how = "white"; + return WarpFade::WHITE; + } + // Too short for the rule, or its hold unseen: the scene after decides. + if (!done || done->change != Pokemon::SceneChange::DIFFERENT || done->kind == Pokemon::TransitionKind::FLASH){ + return WarpFade::NONE; + } + m_how = Pokemon::describe(*done, done->start); + return done->kind == Pokemon::TransitionKind::FADE_BLACK ? WarpFade::BLACK : WarpFade::WHITE; +} + namespace{ @@ -83,7 +141,7 @@ DpadPosition dpad_toward(JoystickPosition stick){ bool black_screen_was_battle(VideoStream& stream, ProControllerContext& context){ BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); - BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, FADE_MAX_DARK_SUM, FADE_MAX_STDDEV_SUM, + BlackScreenWatcher gone(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, Pokemon::BDSP_FADE_PROFILE.dark_max_sum, Pokemon::BDSP_FADE_PROFILE.uniform_max_stddev_sum, BlackScreenWatcher::FinderType::GONE, 500ms); const int ret = wait_until(stream, context, 10s, {{battle_menu}, {battle_dialog}, {gone}}); if (ret == 2){ @@ -262,7 +320,7 @@ class WalkUntil{ // fade too) or the white of an encounter or a cave exit. int hold(Milliseconds chunk){ WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BlackScreenWatcher black(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, FADE_MAX_DARK_SUM, FADE_MAX_STDDEV_SUM); + BlackScreenWatcher black(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, Pokemon::BDSP_FADE_PROFILE.dark_max_sum, Pokemon::BDSP_FADE_PROFILE.uniform_max_stddev_sum); BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); ShortDialogWatcher dialog(COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); @@ -503,7 +561,7 @@ WalkResult walk_for( WalkResult result{WalkStop::LIMIT, duration, 0}; context.wait_for_all_requests(); WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); - BlackScreenWatcher black(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, FADE_MAX_DARK_SUM, FADE_MAX_STDDEV_SUM); + BlackScreenWatcher black(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, Pokemon::BDSP_FADE_PROFILE.dark_max_sum, Pokemon::BDSP_FADE_PROFILE.uniform_max_stddev_sum); BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); ShortDialogWatcher dialog(COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h index d3d61b6743..24c0e3b993 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -11,6 +11,7 @@ #define PokemonAutomation_PokemonBDSP_AutoStory_Walk_H #include +#include #include #include #include "Common/Cpp/Time.h" @@ -20,6 +21,7 @@ #include "Controllers/Joystick.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" #include "Pokemon/AutoStory/Pokemon_SceneJudges.h" +#include "Pokemon/AutoStory/Pokemon_Transitions.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -193,6 +195,53 @@ constexpr double WALK_TAP_BLOCKED_CHANGE = 16.0; // More than walking changes in 400ms: a new scene, such as a battle opening. constexpr double WALK_SCENE_CHANGE = 120.0; +// Whether two frames show different scenes: a change walking cannot make +// (WALK_SCENE_CHANGE). UNKNOWN without both frames. +Pokemon::SceneChange walk_scenes_differ(const Pokemon::TransitionSample& before, const Pokemon::TransitionSample& after); + +// What a warp's fade must show before a step takes it as one. All of it +// reads Pokemon::BDSP_FADE_PROFILE. Dark is the profile's uniform tier, or +// with `partial_dark` its partial tier's texture (a doorway's dim fade +// frames, which run_into_door() has stopped on since #268) up to the same +// dark sum. Dark must hold `dark_for` and white (always uniform) `white_for`. +struct WarpFadeRule{ + bool partial_dark = false; + Milliseconds dark_for = Milliseconds(0); + Milliseconds white_for = Milliseconds(0); +}; + +// A warp's fade followed frame by frame (a Pokemon::TransitionTracker on +// BDSP_FADE_PROFILE) rather than judged on one frame. A fade shows when its +// dark or white has held as the rule says; or, when its hold fell between +// two frames (a 50 ms door fade), at the first frame of another scene +// after the fall, as `compare` tells scenes apart. That one unseen hold +// used to leave a door run to its 12 s timeout. Not thread-safe. +class WarpFadeWatch{ +public: + explicit WarpFadeWatch(WarpFadeRule rule, Pokemon::TransitionTracker::CompareScenes compare = walk_scenes_differ); + + // Adds a frame; times must not go back. Returns the fade this frame + // shows, NONE if none (yet). + WarpFade push(const Pokemon::TransitionSample& sample); + // When the screen last went dark as the rule counts dark; none if it + // has not. + std::optional went_dark() const{ + return m_went_dark; + } + // How the last fade showed, for the log; "" before one. + const std::string& how() const{ + return m_how; + } + +private: + WarpFadeRule m_rule; + Pokemon::TransitionTracker m_tracker; + std::optional m_dark_since; + std::optional m_white_since; + std::optional m_went_dark; + std::string m_how; +}; + // One straight walk timed finely, to check a map against the game: holds the // stick at `stick` and samples the screen every 100ms until the view has been From b6e936fbef2977ef2bcffcb489ae727de4f51dd1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 03:19:13 -0800 Subject: [PATCH 34/39] BDSP: time a walk's screen events in its result A walk's watchers (the encounter flash, black, the battle menu and text box, a dialogue box) and the late watch after the hold now record what they saw into WalkResult::events, the shared timed ScreenEventLog, from the walk's first input (WalkResult::started). WalkResult also counts the chunks that moved after the last battle was run from. Nothing reads them yet; the walker places an open leg's battle with them next. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Utils/PokemonBDSP_AutoStory_Walk.cpp | 61 ++++++++++++++++++- .../Utils/PokemonBDSP_AutoStory_Walk.h | 18 +++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp index 1173f948a2..6372a041bb 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.cpp @@ -166,18 +166,45 @@ bool battle_follows(VideoStream& stream, ProControllerContext& context, Millisec // exit's white fade (#268 slice 3), which the walk once fled as a battle. constexpr Milliseconds WHITE_FLASH_BATTLE_WAIT = 8000ms; +// Records that the screen shows `events` now, in `log` if there is one. +void record_now(Pokemon::ScreenEventLog* log, const Pokemon::ScreenEvents& events){ + if (log != nullptr){ + log->record(events, current_time()); + } +} + +// What the late watch's watcher `ret` saw: 0 the encounter flash, 1 black, +// 2 the battle menu, 3 the battle's text box. +Pokemon::ScreenEvents late_watch_events(int ret){ + switch (ret){ + case 0: + return {Pokemon::ScreenEvent::FADE_WHITE}; + case 1: + return {Pokemon::ScreenEvent::FADE_BLACK}; + case 2: + case 3: + return {Pokemon::ScreenEvent::BATTLE_STARTED}; + default: + return {}; + } +} + } // namespace // A step in tall grass that was started before the stick was released still // finishes, and its encounter starts after the walk has ended. Watch a little // longer, and run from it. Returns whether there was one. -bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label){ +bool run_from_late_encounter( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label, + Pokemon::ScreenEventLog* events +){ VideoSnapshot before = stream.video().snapshot(); WhiteScreenOverWatcher encounter_flash(COLOR_YELLOW, {0.1, 0.1, 0.8, 0.8}); BlackScreenWatcher black(COLOR_RED); BattleMenuWatcher battle_menu(BattleType::STANDARD, COLOR_YELLOW); BattleDialogWatcher battle_dialog(COLOR_YELLOW); const int ret = wait_until(stream, context, 1200ms, {{encounter_flash}, {black}, {battle_menu}, {battle_dialog}}); + record_now(events, late_watch_events(ret)); bool battle = ret == 0 || ret == 2 || ret == 3 || (ret == 1 && black_screen_was_battle(stream, context)); if (ret < 0){ VideoSnapshot after = stream.video().snapshot(); @@ -187,9 +214,11 @@ bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, if (!battle){ return false; } + record_now(events, {Pokemon::ScreenEvent::BATTLE_STARTED}); stream.log("[Walk] " + label + ": wild battle after the walk; running."); escape(stream, context); context.wait_for_all_requests(); + record_now(events, {}); return true; } namespace{ @@ -292,6 +321,8 @@ class WalkUntil{ // A tile a tap: 160 ms held, the rest released, so each chunk is one tile. const Milliseconds CHUNK = m_spec.input == WalkInput::TAPS ? 800ms : 400ms; m_context.wait_for_all_requests(); + m_result.started = current_time(); + m_result.events.record({}, m_result.started); m_before = snapshot_or_fail(m_stream, m_spec.label); while (true){ if (m_spec.arrived && m_spec.arrived(*m_before.frame)){ @@ -356,6 +387,7 @@ class WalkUntil{ // The watchers: 0 the encounter flash, 1 black, 2 the battle menu, 3 a // dialogue, 4 the battle's text box. ChunkVerdict after_watchers(int ret){ + record_now(&m_result.events, hold_events(ret)); bool battle = ret == 2 || ret == 4; if (ret == 0 || ret == 1){ battle = ret == 0 @@ -431,8 +463,26 @@ class WalkUntil{ return change < line; } + // What hold()'s watcher `ret` saw (see after_watchers()). + static Pokemon::ScreenEvents hold_events(int ret){ + switch (ret){ + case 0: + return {Pokemon::ScreenEvent::FADE_WHITE}; + case 1: + return {Pokemon::ScreenEvent::FADE_BLACK}; + case 2: + case 4: + return {Pokemon::ScreenEvent::BATTLE_STARTED}; + case 3: + return {Pokemon::ScreenEvent::DIALOG_BOX}; + default: + return {}; + } + } + enum class BattleMovement{ NONE, STEP_MOVED }; void escape_battle(const std::string& what, BattleMovement movement){ + record_now(&m_result.events, {Pokemon::ScreenEvent::BATTLE_STARTED}); m_result.wild_battles++; if (m_chunks <= 1 && movement == BattleMovement::NONE){ m_result.origin = WalkOrigin::MAY_PRECEDE_LEG; @@ -445,6 +495,8 @@ class WalkUntil{ m_stream.log("[Walk] " + m_spec.label + ": " + what); m_escape(m_stream, m_context); m_context.wait_for_all_requests(); + record_now(&m_result.events, {}); + m_moved_at_battle = m_result.moved_chunks; m_before = snapshot_or_fail(m_stream, m_spec.label); m_unchanged = 0; } @@ -453,9 +505,10 @@ class WalkUntil{ m_result.stop = stop; bool late_battle = false; if (m_spec.wild == WildPolicy::RUN_FROM && (stop == WalkStop::LIMIT || stop == WalkStop::BLOCKED) && - run_from_late_encounter(m_stream, m_context, m_escape, m_spec.label) + run_from_late_encounter(m_stream, m_context, m_escape, m_spec.label, &m_result.events) ){ m_result.wild_battles++; + m_moved_at_battle = m_result.moved_chunks; late_battle = true; if (m_chunks <= 1 && m_result.moved_chunks <= 1){ m_result.origin = WalkOrigin::MAY_PRECEDE_LEG; @@ -477,6 +530,9 @@ class WalkUntil{ if (m_result.moved_chunks > 1){ m_result.origin = WalkOrigin::CURRENT_LEG; } + if (m_result.wild_battles > 0){ + m_result.moved_after_battle = m_result.moved_chunks - m_moved_at_battle; + } m_stream.log("[Walk] " + m_spec.label + ": " + walk_stop_name(stop) + " after " + std::to_string(m_result.walked.count()) + "ms, " + std::to_string(m_result.moved_chunks) + " moved chunks, " + @@ -494,6 +550,7 @@ class WalkUntil{ VideoSnapshot m_before_still; size_t m_unchanged = 0; size_t m_chunks = 0; + size_t m_moved_at_battle = 0; double m_largest = 0; LocationBannerWatcher m_banner; }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h index 24c0e3b993..280a7b11a0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h @@ -21,6 +21,7 @@ #include "Controllers/Joystick.h" #include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" #include "Pokemon/AutoStory/Pokemon_SceneJudges.h" +#include "Pokemon/AutoStory/Pokemon_ScreenEvents.h" #include "Pokemon/AutoStory/Pokemon_Transitions.h" namespace PokemonAutomation{ @@ -83,6 +84,17 @@ struct WalkResult{ // The fade that ended a TRANSITION, and any location banner read on // the way (#268 slice 2: walkers report warps). WarpSeen warp = {}; + // What the walk's watchers saw, timed, from its first input through + // the watch after it: an encounter's flash, a battle, a box, a fade. + // The shared judges read it (Pokemon::classify_interrupt()) to tell + // what stopped the walk and when its step was taken. Empty where + // nothing recorded it. + Pokemon::ScreenEventLog events = {}; + // When the walk's first input went in; `events` are timed against it. + WallClock started = {}; + // Chunks that moved after the last wild battle was run from: the taps + // the walk went on with. + size_t moved_after_battle = 0; }; // Holds the left stick at `stick` until one of the WalkStop events. Wild battles on @@ -160,7 +172,11 @@ WalkResult walk_expecting( // After a move that may have walked onto an encounter tile (a tap in a cave // or in tall grass): waits briefly for a wild battle and runs from it. // Returns whether there was one; we may then be off the place we expected. -bool run_from_late_encounter(VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label); +// `events`, if given, records what was seen, timed. +bool run_from_late_encounter( + VideoStream& stream, ProControllerContext& context, const WildBattleEscape& escape, const std::string& label, + Pokemon::ScreenEventLog* events = nullptr +); // walk_until() toward a wall the route was measured to end at. Seeing the // stop only ends the hold early: holding on for the whole `limit` also From e6fd052a96d48e68d6940fa35573aca81f23cec1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 03:21:02 -0800 Subject: [PATCH 35/39] BDSP: place a wild battle on an open leg's taps by its screen events place_leg_battle() reads a walk's timed events with the shared classifier: for one wild encounter it takes the tap its first timed event's step fell in, adds the taps that moved after the battle was run from, and bounds the player to a tile either side. An UNSETTLED verdict (a fade too recent to tell a battle from a warp), two battles, or no events leave it unplaced, and leg_battle_line() says why. BDSP's lags are not measured on the console yet (flash 500 ms, battle box 4500 ms after the tap); they are data in LegBattleTiming and flagged there. record_tap_battle() records the same events for the sim. Unit test on synthetic walks; the walker does not use it yet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../PokemonBDSP_AutoStory_Walk_Tests.cpp | 44 ++++++ .../Utils/PokemonBDSP_AutoStory_LegBattle.cpp | 126 ++++++++++++++++++ .../Utils/PokemonBDSP_AutoStory_LegBattle.h | 95 +++++++++++++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 4 files changed, 267 insertions(+) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.h diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp index 1816cc9094..9bbadd61e9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.cpp @@ -5,6 +5,7 @@ */ #include +#include #include #include #include "Common/Cpp/Filesystem/Filesystem.h" @@ -12,6 +13,7 @@ #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "Utils/PokemonBDSP_AutoStory_Indoors.h" +#include "Utils/PokemonBDSP_AutoStory_LegBattle.h" #include "Utils/PokemonBDSP_AutoStory_Location.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "Utils/PokemonBDSP_AutoStory_Walk.h" @@ -271,6 +273,44 @@ struct AutoStoryWalkCase{ AutoStoryWalkExpectation expectation; }; +// place_leg_battle() on synthetic walks of 10 taps, their events recorded +// as record_tap_battle() records the console's. +struct LegBattleCase{ + std::string name; + size_t battles; + size_t tap; // the tap (from 0) that set the battle off + size_t moved_after; // taps that moved after it was run from + bool settled; // false: a white fade with no battle yet + std::optional expected; +}; +UnitTestResult test_leg_battle_place(Logger& logger){ + const LegBattleCase CASES[] = { + {"mid-leg, the taps went on", 1, 2, 7, true, TileSpan{9, 10}}, + {"seen after the hold", 1, 5, 0, true, TileSpan{5, 7}}, + {"on the first tap", 1, 0, 0, true, TileSpan{0, 2}}, + {"two battles", 2, 2, 0, true, std::nullopt}, + {"unsettled: a white fade, no battle yet", 1, 3, 0, false, std::nullopt}, + }; + for (const LegBattleCase& item : CASES){ + WalkResult walk{.stop = WalkStop::LIMIT, .walked = Milliseconds(8000), .wild_battles = item.battles}; + walk.moved_after_battle = item.moved_after; + walk.events.record({}, walk.started); + if (item.settled){ + record_tap_battle(walk, item.tap); + }else{ + walk.events.record({Pokemon::ScreenEvent::FADE_WHITE}, walk.started + Milliseconds(800 * item.tap + 500)); + } + const LegBattlePlace place = place_leg_battle(walk, 10); + logger.log(leg_battle_line(item.name, walk, place)); + const bool same = place.moved.has_value() == item.expected.has_value() && + (!place.moved || (place.moved->min == item.expected->min && place.moved->max == item.expected->max)); + if (!same){ + return item.name + ": " + leg_battle_line(item.name, walk, place); + } + } + return true; +} + class Test_AutoStoryWalk : public UnitTest{ public: explicit Test_AutoStoryWalk(AutoStoryWalkCase test_case) @@ -321,6 +361,10 @@ const AutoStoryWalkCase AUTO_STORY_WALK_CASES[] = { .input = {"location banner showing", test_location_banner_showing}, .expectation = {}, }, + { + .input = {"a wild battle on an open leg's taps, placed by its screen events", test_leg_battle_place}, + .expectation = {}, + }, }; void add_tests_AutoStory_Walk(UnitTestDatabase& database){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.cpp new file mode 100644 index 0000000000..51a690dd6a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.cpp @@ -0,0 +1,126 @@ +/* BDSP AutoStory - Leg Battle + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonBDSP_AutoStory_LegBattle.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; +using Pokemon::InterruptKind; +using Pokemon::ScreenEvent; +using Pokemon::TilePos; + + +const LegBattleTiming& leg_battle_timing(){ + static const LegBattleTiming TIMING{ + .lags = { + {ScreenEvent::FADE_WHITE, Milliseconds(500)}, + {ScreenEvent::BATTLE_STARTED, Milliseconds(4500)}, + }, + }; + return TIMING; +} + +namespace{ + +// The tile of the leg (from 1) that a step begun at `step` reached: the +// taps begin `tap_time` apart from `started`. At most `tiles`. +size_t tile_reached(WallClock step, WallClock started, Milliseconds tap_time, size_t tiles){ + if (step < started || tap_time.count() <= 0){ + return 0; + } + const auto since = std::chrono::duration_cast(step - started); + return std::min(tiles, (size_t)(since / tap_time) + 1); +} + +// Why a battle on the leg was not placed. +std::string not_placed(const WalkResult& walk, const LegBattlePlace& place){ + if (walk.wild_battles != 1){ + return std::format("{} wild battles", walk.wild_battles); + } + switch (place.verdict.kind){ + case InterruptKind::UNSETTLED: + return "a fade too recent to tell a battle from a warp"; + case InterruptKind::WILD_ENCOUNTER: + return "no timed event"; + case InterruptKind::NONE: + return "no events"; + default: + return std::string("not a wild battle"); + } +} + +} // namespace + +LegBattlePlace place_leg_battle(const WalkResult& walk, size_t tiles, const LegBattleTiming& timing){ + LegBattlePlace out; + const WallClock end = walk.events.last_frame().value_or(walk.started); + out.verdict = Pokemon::classify_interrupt(walk.events, {walk.started, end}, timing.interrupts); + if (walk.wild_battles != 1 || out.verdict.kind != InterruptKind::WILD_ENCOUNTER){ + return out; + } + const std::optional step = Pokemon::interrupt_step_time(out.verdict, timing.lags); + if (!step){ + return out; + } + const size_t reached = tile_reached(*step, walk.started, timing.tap_time, tiles); + const size_t at = std::min(tiles, reached + walk.moved_after_battle); + out.moved = TileSpan{at > timing.slack ? at - timing.slack : 0, std::min(tiles, at + timing.slack)}; + return out; +} + +std::string leg_battle_line(const std::string& label, const WalkResult& walk, const LegBattlePlace& place){ + std::string out = std::format("{}: battle on the leg: {}", label, Pokemon::describe(place.verdict, walk.started)); + if (place.moved){ + return out + std::format("; {} to {} tiles in.", place.moved->min, place.moved->max); + } + return out + "; not placed (" + not_placed(walk, place) + "): any tile of the leg."; +} + +std::vector tiles_moved( + const StoryWorld& world, const std::vector& from, + Direction direction, TileSpan span +){ + std::vector out; + for (TilePos at : from){ + for (size_t count = 0; count <= span.max; count++){ + const auto next = count < span.max ? step(world, at, direction) : std::nullopt; + const bool kept = std::find(out.begin(), out.end(), at) != out.end(); + if ((count >= span.min || !next) && !kept){ + out.push_back(at); + } + if (!next){ + break; + } + at = *next; + } + } + return out; +} + +void record_tap_battle(WalkResult& walk, size_t tap, const LegBattleTiming& timing){ + const WallClock step = walk.started + timing.tap_time * tap; + WallClock last = walk.events.last_frame().value_or(walk.started); + std::vector lags = timing.lags; + std::sort(lags.begin(), lags.end(), [](const Pokemon::EventLag& x, const Pokemon::EventLag& y){ + return x.lag < y.lag; + }); + for (const Pokemon::EventLag& lag : lags){ + last = std::max(last, step + lag.lag); + walk.events.record({lag.event}, last); + } + walk.events.record({}, last + timing.escape_time); +} + + + +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.h new file mode 100644 index 0000000000..6f759e3ee3 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_LegBattle.h @@ -0,0 +1,95 @@ +/* BDSP AutoStory - Leg Battle + * + * From: https://github.com/PokemonAutomation/ + * + * Where a wild battle on an open leg's taps left the player. The walk + * records what its watchers saw, timed (WalkResult::events); the shared + * classifier (Pokemon::classify_interrupt()) says what stopped the leg, + * and a wild battle is placed by when its first timed event showed: the + * tap that set it off, and the taps that moved after it was run from. + * Without that the walker took the player to be anywhere on the leg. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_LegBattle_H +#define PokemonAutomation_PokemonBDSP_AutoStory_LegBattle_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "Pokemon/AutoStory/Pokemon_Interrupts.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// BDSP's timing for a battle on an open leg's taps, as data. +struct LegBattleTiming{ + Pokemon::InterruptKnobs interrupts = {}; + // How long after its tap went in each of a wild battle's events shows. + // NOT MEASURED on BDSP's console yet: estimates from the walk's own + // waits (a tap is held 160 ms, a step takes about 270 ms; the battle's + // box shows within WHITE_FLASH_BATTLE_WAIT). Measure them from a lab-B + // walk log before trusting a placement to the tile. + std::vector lags = {}; + // A tap a tile: walk_tiles_dpad()'s chunk. + Milliseconds tap_time = Milliseconds(800); + // Tiles either side of the placed one the player may be on, for the + // unmeasured lags. + size_t slack = 1; + // How long running from a battle takes, for the sim's events. + Milliseconds escape_time = Milliseconds(6000); +}; +// BDSP's timing. +const LegBattleTiming& leg_battle_timing(); + +// Tiles moved along a leg, from `min` to `max`. +struct TileSpan{ + size_t min = 0; + size_t max = 0; +}; + +// What stopped an open leg's taps, and how far along the leg it left the +// player. +struct LegBattlePlace{ + Pokemon::InterruptVerdict verdict = {}; + // Set for one wild battle whose step the events time. + std::optional moved = {}; +}; + +// For `walk`, the taps of an open leg of `tiles` tiles that a wild battle +// interrupted: what its events show stopped the player, and for one wild +// battle, the tiles moved: the tile its tap reached plus the taps that +// moved after it, give or take `timing.slack`. Not placed for more than +// one battle, for an UNSETTLED verdict (a fade too recent to tell a +// battle from a warp) or any other kind, or for a walk with no events. +LegBattlePlace place_leg_battle(const WalkResult& walk, size_t tiles, const LegBattleTiming& timing = leg_battle_timing()); + +// "