From c919d8041286e01a3255daec63c08791cfc265f2 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 01:30:00 -0800 Subject: [PATCH 01/19] BDSP AutoStory: segments 00 and 01 as tables of event checkpoints Each checkpoint 00.00 to 01.11 is a row of a Pokemon::Segment table (make_segment_00, make_segment_01) whose events are the step functions it called, in the same order; run_segment runs them. The checkpoint_reattempt_loop wrappers go: a row saves first (skipped on a retry), and a failed step takes the loop's screenshot, counts a reset and is retried from a reset to that save, up to the same 20 attempts. 00.04 is two rows, 00.04 and 00.04b, for its two saves. 00.00 neither saves nor retries, as before. The program's options are unchanged: each checkpoint and segment runs its rows through run_story_table(). Test_StoryTables builds both tables with a describe-only runner, checks their rows and first events, lints and describe()s every checkpoint. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + ...okemonBDSP_AutoStory_StoryTables_Tests.cpp | 141 ++++++++++ .../PokemonBDSP_AutoStory_StoryTables_Tests.h | 27 ++ .../PokemonBDSP_AutoStory_Segment_00.cpp | 176 ++++++------- .../PokemonBDSP_AutoStory_Segment_00.h | 5 + .../PokemonBDSP_AutoStory_Segment_01.cpp | 242 +++++------------- .../PokemonBDSP_AutoStory_Segment_01.h | 4 + .../PokemonBDSP_AutoStory_StoryTables.cpp | 192 ++++++++++++++ .../Utils/PokemonBDSP_AutoStory_StoryTables.h | 114 +++++++++ SerialPrograms/cmake/SourceFiles.cmake | 4 + 10 files changed, 626 insertions(+), 281 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 3f7412b76b..f162f8d051 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -20,6 +20,7 @@ #include "Inference/PokemonBDSP_MenuDetector.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -292,6 +293,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_MenuDetector(database); add_tests_AutoStory(database); add_tests_AutoStory_Segment00(database); + add_tests_AutoStory_StoryTables(database); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp new file mode 100644 index 0000000000..909c0ad7c4 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp @@ -0,0 +1,141 @@ +/* BDSP AutoStory Story Tables Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" +#include "Utils/PokemonBDSP_AutoStory_StoryTables.h" +#include "PokemonBDSP_AutoStory_StoryTables_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +// Builds tables without a console; nothing in them runs. +class DescribeOnlyRunner : public StoryStepRunner{ +public: + virtual bool saved_at_start(const std::string&) const override{ + return false; + } + virtual Pokemon::Outcome save_at_start(const std::string&) override{ + return Pokemon::outcome_failed("not_run"); + } + virtual Pokemon::Outcome run_step(const std::string&, StoryFailure, const StoryStep&) override{ + return Pokemon::outcome_failed("not_run"); + } +}; + +struct TableCase{ + std::string name; + std::function make; + std::string segment_id; + std::vector checkpoint_ids; + // Rows whose first event is not the save (they neither save nor retry). + std::vector unsaved; +}; + +const std::vector& table_cases(){ + static const std::vector cases{ + { + .name = "Segment 00", + .make = make_segment_00, + .segment_id = "00", + .checkpoint_ids = {"00.00", "00.01", "00.02", "00.03", "00.04", "00.04b", "00.05"}, + .unsaved = {"00.00"}, + }, + { + .name = "Segment 01", + .make = make_segment_01, + .segment_id = "01", + .checkpoint_ids = {"01.01", "01.02", "01.03", "01.04", "01.05", "01.06", "01.07", "01.08", "01.09", "01.10", "01.11"}, + .unsaved = {}, + }, + }; + return cases; +} + +// An empty string when `table` matches `expected`; else what differs. +std::string check_table(Pokemon::Segment& table, const TableCase& expected){ + if (table.id != expected.segment_id){ + return std::format("{}: id is \"{}\", expected \"{}\".", expected.name, table.id, expected.segment_id); + } + std::vector ids; + for (const Pokemon::CheckpointSpec& spec : table.checkpoints){ + ids.push_back(spec.id); + } + if (ids != expected.checkpoint_ids){ + std::string read; + for (const std::string& id : ids){ + read += " " + id; + } + return std::format("{}: {} rows,{}; expected {}.", expected.name, ids.size(), read, expected.checkpoint_ids.size()); + } + for (Pokemon::CheckpointSpec& spec : table.checkpoints){ + Pokemon::Checkpoint checkpoint = spec.build(); + if (checkpoint.id != spec.id){ + return std::format("{}: row {} builds checkpoint \"{}\".", expected.name, spec.id, checkpoint.id); + } + if (checkpoint.events.empty()){ + return std::format("{}: {} has no events.", expected.name, spec.id); + } + const bool unsaved = std::find(expected.unsaved.begin(), expected.unsaved.end(), spec.id) != expected.unsaved.end(); + const bool saves_first = checkpoint.events[0].name == "save"; + if (saves_first == unsaved){ + return std::format("{}: {}'s first event is \"{}\", expected {}.", + expected.name, spec.id, checkpoint.events[0].name, unsaved ? "a step" : "the save"); + } + const std::vector problems = Pokemon::lint_checkpoint(checkpoint); + if (!problems.empty()){ + return std::format("{}: {} lint: {}", expected.name, spec.id, problems[0]); + } + } + return ""; +} + +class Test_StoryTables : public UnitTest{ +public: + Test_StoryTables() + : UnitTest("PokemonBDSP::AutoStory - Story tables: rows, events and describe()") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + for (const TableCase& expected : table_cases()){ + DescribeOnlyRunner runner; + Pokemon::Segment table = expected.make(runner); + const std::string problem = check_table(table, expected); + if (!problem.empty()){ + return problem; + } + const std::string text = Pokemon::describe(table); + for (const std::string& id : expected.checkpoint_ids){ + if (text.find(id) == std::string::npos){ + return std::format("{}: describe() does not name {}:\n{}", expected.name, id, text); + } + } + logger.log(text); + } + return true; + } +}; + +} + + +void add_tests_AutoStory_StoryTables(UnitTestDatabase& database){ + database.add(); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h new file mode 100644 index 0000000000..c8066e5016 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Story Tables Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// Offline checks of the Segment 00 and 01 tables: their rows in order, +// each checkpoint built and described without a console, none linted. +void add_tests_AutoStory_StoryTables(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 36507d2f66..99b83e3ed5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -1393,12 +1393,7 @@ namespace PokemonAutomation { context.wait_for_all_requests(); env.console.log("Start Segment " + name(), COLOR_ORANGE); - AutoStory_Checkpoint_0000().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0001().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0002().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0003().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0004().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0005().run_checkpoint(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_00, {}); context.wait_for_all_requests(); env.console.log("End Segment " + name(), COLOR_GREEN); @@ -1411,111 +1406,86 @@ namespace PokemonAutomation { SingleSwitchProgramEnvironment& env, ProControllerContext& context, AutoStoryOptions options, AutoStoryStats& stats ) const { - // Before any route movement or checkpoint save: fail visibly if - // options cannot be read, rather than navigate with unknown settings. - verify_tutorial_settings(env.console, context); - context.wait_for_all_requests(); + run_story_table(env, context, options, stats, make_segment_00, {"00.00", "00.00"}); } - static void checkpoint_0001( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) { - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - // Segment 00-00: Intro and starter selection - tutorial_part_1_navigation(env, env.console, context); + Pokemon::Segment make_segment_00(StoryStepRunner& runner) { + Pokemon::Segment segment{"00", {}}; + auto row = [&](StoryCheckpointSpec spec) { + segment.checkpoints.push_back(story_checkpoint(runner, std::move(spec))); + }; + row({ + .id = "00.00", .title = "Verify Settings", .on_failure = StoryFailure::STOP, + .steps = { + {"verify_settings", "before any route movement or checkpoint save: fail visibly if options cannot be read, rather than navigate with unknown settings", + [](StoryConsole& c) { + verify_tutorial_settings(c.env.console, c.context); + c.context.wait_for_all_requests(); + }}, }, - true - ); - } - static void checkpoint_0002( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) { - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - // Segment 00-01: Starter acquisition - starter_nav(env.console, context); - // Segment 00-00 starter selection - switch (options.starter_choice) { - case StarterChoice::TURTWIG: select_turtwig(env.console, context); break; - case StarterChoice::CHIMCHAR: select_chimchar(env.console, context); break; - case StarterChoice::PIPLUP: select_piplup(env.console, context); break; - } - fight_starly(env.console, context); - post_starly_clenaup(env.console, context); + }); + row({ + .id = "00.01", .title = "After TV", .on_failure = StoryFailure::RELOAD_AND_RETRY, + .steps = { + {"tutorial_part_1_navigation", "intro and starter selection: Butters' recorded stick holds to the lake", + [](StoryConsole& c) { tutorial_part_1_navigation(c.env, c.env.console, c.context); }}, }, - true - ); - } - static void checkpoint_0003( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) { - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - // Segment 00-002: Pokedex acquisition - tutorial_part_2_navigation(env, env.console, context); + }); + row({ + .id = "00.02", .title = "Outside Verity before starter", .on_failure = StoryFailure::RELOAD_AND_RETRY, + .steps = { + {"starter_nav", "into the lake's scene", [](StoryConsole& c) { starter_nav(c.env.console, c.context); }}, + {"select_starter", "the starter from the program's option", + [](StoryConsole& c) { + switch (c.options.starter_choice) { + case StarterChoice::TURTWIG: select_turtwig(c.env.console, c.context); break; + case StarterChoice::CHIMCHAR: select_chimchar(c.env.console, c.context); break; + case StarterChoice::PIPLUP: select_piplup(c.env.console, c.context); break; + } + }}, + {"fight_starly", "the Starly battle", [](StoryConsole& c) { fight_starly(c.env.console, c.context); }}, + {"post_starly_cleanup", "the scene after the battle", [](StoryConsole& c) { post_starly_clenaup(c.env.console, c.context); }}, }, - true - ); - } - static void checkpoint_0004( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) { - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - // Segment 00-003: Poketech acquisition - tutorial_part_3_navigation(env, env.console, context); + }); + row({ + .id = "00.03", .title = "Outside Verity after starter", .on_failure = StoryFailure::RELOAD_AND_RETRY, + .steps = { + {"tutorial_part_2_navigation", "Pokedex acquisition: back through Route 201 to Rowan's lab", + [](StoryConsole& c) { tutorial_part_2_navigation(c.env, c.env.console, c.context); }}, }, - true - ); - - // Save and advance progress at the catch boundary. Retries of this - // section must start from the caught-Pokemon save, not the old route. - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - tutorial_part_3_after_catch_navigation(env, env.console, context); + }); + // 00.04 saves twice: a retry after the catch starts from the + // caught-Pokemon save, not the old route, so it is two rows. + row({ + .id = "00.04", .title = "After Pokedex, to the catch", .on_failure = StoryFailure::RELOAD_AND_RETRY, + .steps = { + {"tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", + [](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); }}, }, - true - ); + }); + row({ + .id = "00.04b", .title = "After the catch, to Dawn in Jubilife", .on_failure = StoryFailure::RELOAD_AND_RETRY, + .steps = { + {"tutorial_part_3_after_catch_navigation", "train, heal and buy Potions in Sandgem, save on Route 202, walk to Jubilife and meet Dawn", + [](StoryConsole& c) { tutorial_part_3_after_catch_navigation(c.env, c.env.console, c.context); }}, + }, + }); + row({ + .id = "00.05", .title = "Jubilife After Dawn", .on_failure = StoryFailure::RELOAD_AND_RETRY, + .steps = { + {"tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC", + [](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); }}, + }, + }); + return segment; } + // DIAGNOSTIC ONLY: Remove before pushing to production. void diagnostic_checkpoint_0004_after_catch( SingleSwitchProgramEnvironment& env, ProControllerContext& context, AutoStoryOptions options, AutoStoryStats& stats ) { - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - tutorial_part_3_after_catch_navigation(env, env.console, context); - }, - true - ); - } - - static void checkpoint_0005( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats - ) { - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - // Segment 00-004: Poketech acquisition - tutorial_part_4_navigation(env, env.console, context); - }, - true - ); + run_story_table(env, context, options, stats, make_segment_00, {"00.04b", "00.04b"}); } // ======================== @@ -1531,7 +1501,7 @@ namespace PokemonAutomation { AutoStoryOptions options, AutoStoryStats& stats ) const { - checkpoint_0001(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_00, {"00.01", "00.01"}); } // ======================== @@ -1548,7 +1518,7 @@ namespace PokemonAutomation { AutoStoryOptions options, AutoStoryStats& stats ) const { - checkpoint_0002(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_00, {"00.02", "00.02"}); } @@ -1566,7 +1536,7 @@ namespace PokemonAutomation { AutoStoryOptions options, AutoStoryStats& stats ) const { - checkpoint_0003(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_00, {"00.03", "00.03"}); } // ======================== // Checkpoint 0004 Wrapper @@ -1582,7 +1552,7 @@ namespace PokemonAutomation { AutoStoryOptions options, AutoStoryStats& stats ) const { - checkpoint_0004(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_00, {"00.04", "00.04b"}); } // ======================== // Checkpoint 0005 Wrapper @@ -1598,7 +1568,7 @@ namespace PokemonAutomation { AutoStoryOptions options, AutoStoryStats& stats ) const { - checkpoint_0005(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_00, {"00.05", "00.05"}); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h index 48c6be9f59..4ff7d50b0a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h @@ -11,12 +11,17 @@ #include #include "Pokemon/Navigation/Pokemon_Grid.h" #include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" namespace PokemonAutomation { namespace NintendoSwitch { namespace PokemonBDSP { +// Segment 00 as a table of checkpoints: 00.00 to 00.05, with 00.04 in two +// rows (00.04 to the catch, 00.04b after it), each saving at its start. +Pokemon::Segment make_segment_00(StoryStepRunner& runner); + // DIAGNOSTIC ONLY: Remove before pushing to production. // Starts at the post-catch save and runs only the rest of checkpoint 00.04. void diagnostic_checkpoint_0004_after_catch(SingleSwitchProgramEnvironment& env, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp index fb94058964..5cc258cc83 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp @@ -1398,132 +1398,6 @@ void route204_to_floaroma(VideoStream& stream, ProControllerContext& context){ } -void checkpoint_0101( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - jubilife_to_barry(env.console, context); - }, - true - ); -} - -void checkpoint_0103( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - oreburgh_to_roark(env.console, context); - }, - true - ); -} - -void checkpoint_0102( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - route203_to_oreburgh(env.console, context); - }, - true - ); -} - -void checkpoint_0104( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - oreburgh_gym(env.console, context); - }, - true - ); -} - -void checkpoint_0105( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - oreburgh_to_route203(env.console, context); - }, - true - ); -} - -void checkpoint_0106( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - route203_to_jubilife(env.console, context); - }, - true - ); -} - -void checkpoint_0107( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - jubilife_to_rowan(env.console, context); - }, - true - ); -} - -void checkpoint_0108( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - jubilife_to_ravaged_path(env.console, context); - }, - true - ); -} - -void checkpoint_0109( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - ravaged_path(env.console, context); - }, - true - ); -} - // 01.11: the old couple in Floaroma's flower field, left of the path in // from Route 204. Walked by hand on 2026-10-31 from 01.10's save: west 8 // along the row (the field's bottom, purple flowers), north until the old @@ -1581,38 +1455,60 @@ void floaroma_old_couple(VideoStream& stream, ProControllerContext& context){ talk("the old man"); } -void checkpoint_0110( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - route204_to_floaroma(env.console, context); - // Save on arrival: 01.11 starts here, and so does run 2's - // Floaroma work. - checkpoint_save(env, context, options.notif_status_update, stats, options.checkpoint_save_mode); - }, - true - ); } -void checkpoint_0111( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats -){ - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/){ - floaroma_old_couple(env.console, context); - // The run's last checkpoint: save where it ends. - checkpoint_save(env, context, options.notif_status_update, stats, options.checkpoint_save_mode); - }, - true - ); -} + +// Segment 01 as a table of checkpoints, each saving at its start. +Pokemon::Segment make_segment_01(StoryStepRunner& runner){ + Pokemon::Segment segment{"01", {}}; + auto row = [&](std::string id, std::string title, std::vector steps){ + segment.checkpoints.push_back(story_checkpoint(runner, StoryCheckpointSpec{ + .id = std::move(id), .title = std::move(title), + .on_failure = StoryFailure::RELOAD_AND_RETRY, .steps = std::move(steps), + })); + }; + // Saves where a checkpoint ends, inside its attempt: a failed save is + // a failed attempt. + const StoryStepSpec save_here{"save_here", "the next checkpoint starts here", + [](StoryConsole& c){ + checkpoint_save(c.env, c.context, c.options.notif_status_update, c.stats, c.options.checkpoint_save_mode); + }}; + row("01.01", "Jubilife to Barry", { + {"jubilife_to_barry", "from the Center's PC out to Barry on Route 203", [](StoryConsole& c){ jubilife_to_barry(c.env.console, c.context); }}, + }); + row("01.02", "Route 203 to Oreburgh", { + {"route203_to_oreburgh", "east through the Oreburgh Gate into Oreburgh City", [](StoryConsole& c){ route203_to_oreburgh(c.env.console, c.context); }}, + }); + row("01.03", "Oreburgh Mine to Roark", { + {"oreburgh_to_roark", "into the Mine to Roark and back to the Center", [](StoryConsole& c){ oreburgh_to_roark(c.env.console, c.context); }}, + }); + row("01.04", "Oreburgh Gym", { + {"oreburgh_gym", "the Gym's trainers and Roark for the Coal Badge", [](StoryConsole& c){ oreburgh_gym(c.env.console, c.context); }}, + }); + row("01.05", "Oreburgh to Route 203", { + {"oreburgh_to_route203", "heal, then west out of the Oreburgh Gate", [](StoryConsole& c){ oreburgh_to_route203(c.env.console, c.context); }}, + }); + row("01.06", "Route 203 to Jubilife", { + {"route203_to_jubilife", "west along Route 203 into Jubilife", [](StoryConsole& c){ route203_to_jubilife(c.env.console, c.context); }}, + }); + row("01.07", "Jubilife through Rowan's scene", { + {"jubilife_to_rowan", "to the north street, Rowan's scene and the tag battle", [](StoryConsole& c){ jubilife_to_rowan(c.env.console, c.context); }}, + }); + row("01.08", "Jubilife to the Ravaged Path", { + {"jubilife_to_ravaged_path", "up Route 204's south half into the Ravaged Path", [](StoryConsole& c){ jubilife_to_ravaged_path(c.env.console, c.context); }}, + }); + row("01.09", "Ravaged Path", { + {"ravaged_path", "through the cave with Rock Smash, out onto Route 204 North", [](StoryConsole& c){ ravaged_path(c.env.console, c.context); }}, + }); + row("01.10", "Route 204 to Floaroma", { + {"route204_to_floaroma", "the human's line north into Floaroma", [](StoryConsole& c){ route204_to_floaroma(c.env.console, c.context); }}, + save_here, + }); + row("01.11", "Floaroma's old couple", { + {"floaroma_old_couple", "the old woman and the old man in the flower field", [](StoryConsole& c){ floaroma_old_couple(c.env.console, c.context); }}, + save_here, + }); + return segment; } @@ -1629,17 +1525,7 @@ void AutoStory_Segment_01::run_segment( stats.m_segment++; env.update_stats(); env.console.log("Start Segment " + name(), COLOR_ORANGE); - AutoStory_Checkpoint_0101().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0102().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0103().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0104().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0105().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0106().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0107().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0108().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0109().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0110().run_checkpoint(env, context, options, stats); - AutoStory_Checkpoint_0111().run_checkpoint(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {}); env.console.log("End Segment " + name(), COLOR_GREEN); } @@ -1653,7 +1539,7 @@ void AutoStory_Checkpoint_0101::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0101(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.01", "01.01"}); } std::string AutoStory_Checkpoint_0102::name() const { return "01.02 - Route 203 to Oreburgh"; } @@ -1666,7 +1552,7 @@ void AutoStory_Checkpoint_0102::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0102(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.02", "01.02"}); } @@ -1681,7 +1567,7 @@ void AutoStory_Checkpoint_0103::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0103(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.03", "01.03"}); } @@ -1696,7 +1582,7 @@ void AutoStory_Checkpoint_0104::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0104(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.04", "01.04"}); } std::string AutoStory_Checkpoint_0105::name() const { return "01.05 - Oreburgh to Route 203"; } @@ -1709,7 +1595,7 @@ void AutoStory_Checkpoint_0105::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0105(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.05", "01.05"}); } std::string AutoStory_Checkpoint_0106::name() const { return "01.06 - Route 203 to Jubilife"; } @@ -1722,7 +1608,7 @@ void AutoStory_Checkpoint_0106::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0106(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.06", "01.06"}); } std::string AutoStory_Checkpoint_0107::name() const { return "01.07 - Jubilife through Rowan's scene"; } @@ -1735,7 +1621,7 @@ void AutoStory_Checkpoint_0107::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0107(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.07", "01.07"}); } std::string AutoStory_Checkpoint_0108::name() const { return "01.08 - Jubilife to the Ravaged Path"; } @@ -1748,7 +1634,7 @@ void AutoStory_Checkpoint_0108::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0108(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.08", "01.08"}); } std::string AutoStory_Checkpoint_0109::name() const { return "01.09 - Ravaged Path"; } @@ -1761,7 +1647,7 @@ void AutoStory_Checkpoint_0109::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0109(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.09", "01.09"}); } std::string AutoStory_Checkpoint_0110::name() const { return "01.10 - Route 204 to Floaroma"; } @@ -1774,7 +1660,7 @@ void AutoStory_Checkpoint_0110::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0110(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.10", "01.10"}); } std::string AutoStory_Checkpoint_0111::name() const { return "01.11 - Floaroma's old couple"; } @@ -1787,7 +1673,7 @@ void AutoStory_Checkpoint_0111::run_checkpoint( AutoStoryOptions options, AutoStoryStats& stats ) const{ - checkpoint_0111(env, context, options, stats); + run_story_table(env, context, options, stats, make_segment_01, {"01.11", "01.11"}); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h index f2570256cd..1236c9d995 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h @@ -9,12 +9,16 @@ #define PokemonAutomation_PokemonBDSP_AutoStory_Segment01_H #include "../PokemonBDSP_AutoStoryTools.h" +#include "../Utils/PokemonBDSP_AutoStory_StoryTables.h" namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ +// Segment 01 as a table of checkpoints: 01.01 to 01.11. +Pokemon::Segment make_segment_01(StoryStepRunner& runner); + class AutoStory_Segment_01 : public AutoStory_Segment{ public: virtual std::string name() const override; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp new file mode 100644 index 0000000000..d71e73fade --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp @@ -0,0 +1,192 @@ +/* BDSP AutoStory - Story tables + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/PrettyPrint.h" +#include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/Globals.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "PokemonBDSP/Programs/PokemonBDSP_GameEntry.h" +#include "PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Checkpoint; +using Pokemon::Event; +using Pokemon::Outcome; +using Pokemon::RunState; + + +namespace{ + +// The attempts checkpoint_reattempt_loop() gives a checkpoint. +constexpr size_t STORY_ATTEMPTS = 20; + +Event save_event(StoryStepRunner& runner, const std::string& id){ + Event event; + event.name = "save"; + event.why = "the save a failed step resets to"; + event.skip_if.push_back(Pokemon::condition("saved at the start", [&runner, id](RunState&){ + return runner.saved_at_start(id); + })); + event.act = [&runner, id](RunState&){ + return runner.save_at_start(id); + }; + return event; +} + +Event step_event(StoryStepRunner& runner, const std::string& id, StoryFailure on_failure, StoryStepSpec step){ + Event event; + event.name = std::move(step.name); + event.why = std::move(step.why); + event.act = [&runner, id, on_failure, run = std::move(step.step)](RunState&){ + return runner.run_step(id, on_failure, run); + }; + return event; +} + +} + + +Pokemon::CheckpointSpec story_checkpoint(StoryStepRunner& runner, StoryCheckpointSpec spec){ + Pokemon::CheckpointSpec row; + row.id = spec.id; + row.title = spec.title; + row.build = [&runner, spec = std::move(spec)](){ + Checkpoint checkpoint; + checkpoint.id = spec.id; + if (spec.on_failure == StoryFailure::RELOAD_AND_RETRY){ + checkpoint.events.push_back(save_event(runner, spec.id)); + } + for (const StoryStepSpec& step : spec.steps){ + checkpoint.events.push_back(step_event(runner, spec.id, spec.on_failure, step)); + } + return checkpoint; + }; + return row; +} + + +ConsoleStoryRunner::ConsoleStoryRunner(StoryConsole console) + : m_console(console) +{} + +bool ConsoleStoryRunner::saved_at_start(const std::string& id) const{ + return m_saved.contains(id); +} + +// A save that fails is a failed first attempt, as in the loop: the retry +// resets and does not save again. +Outcome ConsoleStoryRunner::save_at_start(const std::string& id){ + m_saved.insert(id); + return run_step(id, StoryFailure::RELOAD_AND_RETRY, [](StoryConsole& console){ + checkpoint_save(console.env, console.context, console.options.notif_status_update, console.stats, console.options.checkpoint_save_mode); + console.stats.m_first_run = false; + }); +} + +Outcome ConsoleStoryRunner::run_step(const std::string& id, StoryFailure on_failure, const StoryStep& step){ + if (on_failure == StoryFailure::STOP){ + step(m_console); + return Pokemon::outcome_ok(); + } + SingleSwitchProgramEnvironment& env = m_console.env; + try{ + step(m_console); + return Pokemon::outcome_ok(); + }catch (OperationFailedException& e){ + // The fake-save mode's loop around the whole range retries it. + if (m_console.options.checkpoint_retry_scope == CheckpointRetryScope::DEFER_TO_SELECTED_RANGE){ + throw; + } + const size_t attempt = m_failures[id]++; + // The lab's run watcher reads this line as a failed attempt. + env.console.log(std::format("checkpoint_reattempt_loop: attempt {} failed: {}: {}", attempt, id, e.message()), COLOR_RED); + // Keep what the screen showed at the failure; the retry resets the game. + if (VideoSnapshot screen = env.console.video().snapshot()){ + const std::string folder = SCREENSHOTS_PATH() + "AutoStory/failures/"; + Filesystem::create_directories(Filesystem::Path(folder)); + const std::string path = std::format("{}{}-attempt{}.png", folder, now_to_filestring(), attempt); + screen.frame->save(path); + env.console.log("Failure screenshot: " + path); + } + m_console.stats.m_reset++; + env.update_stats(); + if (attempt + 1 >= STORY_ATTEMPTS){ + throw; + } + if (g_stop_at_first_failure){ + env.console.log("Stopping at the first failure; the game stays where it failed.", COLOR_ORANGE); + throw; + } + return Pokemon::outcome_failed("operation_failed", e.message()); + } +} + +Outcome ConsoleStoryRunner::reload(){ + SingleSwitchProgramEnvironment& env = m_console.env; + env.console.log("Resetting game from HOME before retry...", COLOR_ORANGE); + if (!reset_game_from_home(env, env.console, m_console.context, true, 1000ms)){ + throw OperationFailedExceptionWithScreenshot( + ErrorReportMode::SEND_ERROR_REPORT, + "Failed to reset game from HOME during checkpoint retry.", + env.console + ); + } + return Pokemon::outcome_ok(); +} + +void ConsoleStoryRunner::run(Pokemon::Segment& segment, const Pokemon::SegmentRange& range){ + SingleSwitchProgramEnvironment& env = m_console.env; + Pokemon::SegmentHooks hooks; + hooks.save = [](RunState&){ + return Pokemon::outcome_failed("no_save_hook", "story checkpoints save at their start"); + }; + hooks.reload = [this](RunState&){ + return reload(); + }; + hooks.log = [&env](const std::string& line){ + env.log(line, COLOR_BLUE); + }; + Pokemon::SegmentPolicy policy; + policy.attempts_per_checkpoint = STORY_ATTEMPTS; + RunState state; + const Pokemon::SegmentReport report = Pokemon::run_segment(state, segment, range, policy, hooks); + if (!report.completed){ + throw OperationFailedExceptionWithScreenshot( + ErrorReportMode::SEND_ERROR_REPORT, + std::format( + "AutoStory segment {}: failed at {} {}: {}", + segment.id, report.failed_checkpoint, report.failed_event, Pokemon::to_string(report.failure) + ), + env.console + ); + } +} + + +void run_story_table( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const AutoStoryOptions& options, AutoStoryStats& stats, + const StorySegmentFactory& make_segment, const Pokemon::SegmentRange& range +){ + ConsoleStoryRunner runner(StoryConsole{env, context, options, stats}); + Pokemon::Segment segment = make_segment(runner); + runner.run(segment, range); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h new file mode 100644 index 0000000000..6b9b2a500c --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h @@ -0,0 +1,114 @@ +/* BDSP AutoStory - Story tables + * + * From: https://github.com/PokemonAutomation/ + * + * A segment of the story as a table of checkpoints on the shared engine + * (Pokemon_StorySegments.h). Each checkpoint is the list of step functions + * it called in order before it was a table, each step one event. The + * checkpoint saves first, as checkpoint_reattempt_loop() did before its + * first attempt; a step that fails is answered the way that loop answered + * it: a screenshot, a reset to the save, and the checkpoint again from its + * first step, up to the same 20 attempts. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTables_H + +#include +#include +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// What a step runs on. +struct StoryConsole{ + SingleSwitchProgramEnvironment& env; + ProControllerContext& context; + const AutoStoryOptions& options; + AutoStoryStats& stats; +}; +using StoryStep = std::function; + +struct StoryStepSpec{ + std::string name; + std::string why; + StoryStep step; +}; + +// What a checkpoint does when a step fails. +enum class StoryFailure{ + // Save before the first step; on a failure reset to that save and run + // the checkpoint again (checkpoint_reattempt_loop with save=true). + RELOAD_AND_RETRY, + // No save and no retry: the failure ends the program (00.00). + STOP, +}; + +struct StoryCheckpointSpec{ + std::string id; + std::string title; + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY; + std::vector steps; +}; + +// How a table's events reach the game: the console runner below, or a +// fake in the tests, which only describe the tables. +class StoryStepRunner{ +public: + virtual ~StoryStepRunner() = default; + // Whether checkpoint `id` has saved at its start in this run; a retry + // starts from that save and does not save again. + virtual bool saved_at_start(const std::string& id) const = 0; + virtual Pokemon::Outcome save_at_start(const std::string& id) = 0; + virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryStep& step) = 0; +}; + +// A row of a segment table: builds the checkpoint's events from `spec`. +Pokemon::CheckpointSpec story_checkpoint(StoryStepRunner& runner, StoryCheckpointSpec spec); + +using StorySegmentFactory = std::function; + + +// Runs tables on the Switch. +class ConsoleStoryRunner : public StoryStepRunner{ +public: + explicit ConsoleStoryRunner(StoryConsole console); + + virtual bool saved_at_start(const std::string& id) const override; + virtual Pokemon::Outcome save_at_start(const std::string& id) override; + virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryStep& step) override; + + // Runs the rows of `segment` in `range`; throws as the checkpoint + // loop did once a checkpoint has used up its attempts. + void run(Pokemon::Segment& segment, const Pokemon::SegmentRange& range); + +private: + Pokemon::Outcome reload(); + + StoryConsole m_console; + std::set m_saved; + std::map m_failures; +}; + +// Builds the table with a console runner and runs the checkpoints from +// `range.from` to `range.to` (the whole segment when both are unset). +void run_story_table( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const AutoStoryOptions& options, AutoStoryStats& stats, + const StorySegmentFactory& make_segment, const Pokemon::SegmentRange& range +); + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index c4fd2ae39d..a66f890396 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1578,6 +1578,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.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_NewGame.cpp @@ -1596,6 +1598,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/PokemonBDSP_Tests.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.cpp From d12a31755cb3fd327bdf492b0678ff40192625d7 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 01:32:56 -0800 Subject: [PATCH 02/19] BDSP AutoStory: the story maps as JSON files in the shared map schema devtools/maps/bdsp_maps.py writes the 16 C++ tile tables as PokemonBDSP/Maps/.json in Pokemon_MapData's schema ('"' as '~', a standing 'N' as an npc object on open ground). The program loads them through parse_map() when it starts, checks each against its table cell for cell, and StoryMaps plans on the loaded grids (story_grid()); the offline tests, which have no resources, plan on the tables as before. The tables stay until the files are the source. Tests: the glyph mapping (CI), and the files equal to the tables when the resources have them (skipped in CI, which has none). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + .../Data/PokemonBDSP_AutoStory_MapFiles.cpp | 175 ++++++++++++++++++ .../Data/PokemonBDSP_AutoStory_MapFiles.h | 64 +++++++ .../Data/PokemonBDSP_AutoStory_StoryMaps.cpp | 28 +-- .../AutoStory/PokemonBDSP_AutoStory.cpp | 7 + .../PokemonBDSP_AutoStory_MapFiles_Tests.cpp | 130 +++++++++++++ .../PokemonBDSP_AutoStory_MapFiles_Tests.h | 27 +++ SerialPrograms/cmake/SourceFiles.cmake | 4 + devtools/maps/bdsp_maps.py | 125 +++++++++++++ 9 files changed, 550 insertions(+), 12 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h create mode 100644 devtools/maps/bdsp_maps.py diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index f162f8d051..9b32406385 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -21,6 +21,7 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -294,6 +295,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory(database); add_tests_AutoStory_Segment00(database); add_tests_AutoStory_StoryTables(database); + add_tests_AutoStory_MapFiles(database); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp new file mode 100644 index 0000000000..677bfc6701 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp @@ -0,0 +1,175 @@ +/* BDSP AutoStory - Map files + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonValue.h" +#include "CommonFramework/GlobalAutoPaths.h" +#include "PokemonBDSP_AutoStory_FloaromaTown.h" +#include "PokemonBDSP_AutoStory_JubilifeCity.h" +#include "PokemonBDSP_AutoStory_LakeVerity.h" +#include "PokemonBDSP_AutoStory_OreburghCity.h" +#include "PokemonBDSP_AutoStory_OreburghGate.h" +#include "PokemonBDSP_AutoStory_OreburghGym.h" +#include "PokemonBDSP_AutoStory_OreburghMine.h" +#include "PokemonBDSP_AutoStory_RavagedPath.h" +#include "PokemonBDSP_AutoStory_Route201.h" +#include "PokemonBDSP_AutoStory_Route202.h" +#include "PokemonBDSP_AutoStory_Route203.h" +#include "PokemonBDSP_AutoStory_Route204.h" +#include "PokemonBDSP_AutoStory_Route204South.h" +#include "PokemonBDSP_AutoStory_SandgemTown.h" +#include "PokemonBDSP_AutoStory_TwinleafTown.h" +#include "PokemonBDSP_AutoStory_VerityLakefront.h" +#include "PokemonBDSP_AutoStory_MapFiles.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::MapData; +using Pokemon::TileGrid; +using Pokemon::TileKind; +using Pokemon::TilePos; +using Pokemon::TileType; + + +const std::vector& story_map_files(){ + static const std::vector files{ + {"TwinleafTown", TwinleafTown::grid}, + {"VerityLakefront", VerityLakefront::grid}, + {"LakeVerity", LakeVerity::grid}, + {"Route201", Route201::grid}, + {"SandgemTown", SandgemTown::grid}, + {"Route202", Route202::grid}, + {"JubilifeCity", JubilifeCity::grid}, + {"Route203", Route203::grid}, + {"OreburghGate", OreburghGate::grid}, + {"OreburghCity", OreburghCity::grid}, + {"OreburghMine", OreburghMine::grid}, + {"OreburghGym", OreburghGym::grid}, + {"Route204South", Route204South::grid}, + {"RavagedPath", RavagedPath::grid}, + {"Route204North", Route204::north_grid}, + {"FloaromaTown", FloaromaTown::grid}, + }; + return files; +} + +std::string story_map_folder(){ + return RESOURCE_PATH() + "PokemonBDSP/Maps/"; +} + + +namespace{ + +std::optional tile_type_of(TileKind kind){ + switch (kind){ + case TileKind::WALL: return TileType::BLOCKED; + case TileKind::OPEN: return TileType::WALKABLE; + case TileKind::TALL_GRASS: return TileType::TALL_GRASS; + case TileKind::LEDGE_SOUTH: return TileType::LEDGE; + case TileKind::DOOR: return TileType::DOOR; + case TileKind::SIGN: return TileType::SIGN; + default: return std::nullopt; + } +} + +} + +TileGrid tile_grid_from_map(const MapData& map){ + std::vector rows; + for (int y = 0; y < map.tiles.height; y++){ + std::string row; + for (int x = 0; x < map.tiles.width; x++){ + const TileKind kind = map.tiles.at(TilePos{x, y}); + const std::optional type = tile_type_of(kind); + if (!type){ + throw ParseException(std::format( + "{}: glyph '{}' at {} has no BDSP tile.", map.name, Pokemon::glyph_of(kind), Pokemon::to_string(TilePos{x, y}) + )); + } + row += Pokemon::tile_char(*type); + } + rows.push_back(std::move(row)); + } + for (const Pokemon::MapObject& object : map.objects){ + if (object.kind != Pokemon::ObjectKind::NPC || object.wander){ + throw ParseException(std::format("{}: object {} is not a person standing still.", map.name, object.name)); + } + rows[object.y][object.x] = Pokemon::tile_char(TileType::NPC); + } + return TileGrid(rows); +} + +std::optional grid_difference(const TileGrid& here, const TileGrid& there){ + if (here.width() != there.width() || here.height() != there.height()){ + return std::format("{} x {} here, {} x {} there", here.width(), here.height(), there.width(), there.height()); + } + for (int y = 0; y < here.height(); y++){ + for (int x = 0; x < here.width(); x++){ + const TilePos pos{x, y}; + if (here.at(pos) != there.at(pos)){ + return std::format( + "{}: '{}' here, '{}' there", + Pokemon::to_string(pos), Pokemon::tile_char(here.at(pos)), Pokemon::tile_char(there.at(pos)) + ); + } + } + } + return std::nullopt; +} + + +namespace{ + +std::map& loaded_grids(){ + static std::map grids; + return grids; +} + +} + +void load_story_map_files(const std::string& folder){ + std::map grids; + for (const StoryMapFile& file : story_map_files()){ + const std::string path = folder + file.name + ".json"; + const MapData map = Pokemon::parse_map(load_json_file(path), path); + if (map.name != file.name){ + throw ParseException(std::format("{}: name is \"{}\", not \"{}\".", path, map.name, file.name)); + } + TileGrid grid = tile_grid_from_map(map); + if (const std::optional difference = grid_difference(grid, file.table())){ + throw ParseException(std::format("{}: differs from the C++ table at {}.", path, *difference)); + } + grids.emplace(file.name, std::move(grid)); + } + // Once only: StoryMaps keeps copies of what it was handed first, and a + // second load can only find the same files equal to the same tables. + if (loaded_grids().empty()){ + loaded_grids() = std::move(grids); + } +} + +const TileGrid& story_grid(const std::string& name){ + const std::map& grids = loaded_grids(); + if (auto iter = grids.find(name); iter != grids.end()){ + return iter->second; + } + for (const StoryMapFile& file : story_map_files()){ + if (file.name == name){ + return file.table(); + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No BDSP story map named \"" + name + "\"."); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h new file mode 100644 index 0000000000..905496865f --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h @@ -0,0 +1,64 @@ +/* BDSP AutoStory - Map files + * + * From: https://github.com/PokemonAutomation/ + * + * The story's maps as JSON resources, PokemonBDSP/Maps/.json in the + * shared map schema (Pokemon_MapData.h), written from the C++ tile tables + * by devtools/maps/bdsp_maps.py. The program loads them when it starts, + * checks each against its table cell for cell, and plans on the loaded + * grids; the offline tests, which have no resources, plan on the tables. + * Once the files are the source, the tables go. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_H +#define PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_H + +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_MapData.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +struct StoryMapFile{ + // The file's name without ".json", and the map's "name" in it. + const char* name; + const Pokemon::TileGrid& (*table)(); +}; +const std::vector& story_map_files(); + +// Where the program looks for the files: RESOURCE_PATH() + "PokemonBDSP/Maps/". +std::string story_map_folder(); + +// A map's tiles as a tile grid: walls, open ground, tall grass, south +// ledges, doors and signs; a standing person ("npc" object) is an NPC tile. +// Throws ParseException, naming the map, for what BDSP's grids have no +// tile for (water, ledges other than south, items, wanderers). +Pokemon::TileGrid tile_grid_from_map(const Pokemon::MapData& map); + +// "(x, y): 'N' here, '.' there", or nothing when the grids are the same +// size and equal cell for cell. +std::optional grid_difference(const Pokemon::TileGrid& here, const Pokemon::TileGrid& there); + +// Reads every file in `folder` through the shared loader and checks it +// against its table. Throws FileException or ParseException naming the +// file when one is missing, malformed or differs; else story_grid() hands +// out the loaded grids from then on. +void load_story_map_files(const std::string& folder); + +// The loaded grid of the file `name`, or its C++ table before +// load_story_map_files() has run. Throws InternalProgramError for a name +// not in story_map_files(). +const Pokemon::TileGrid& story_grid(const std::string& name); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp index 0521675c23..55793a32f6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp @@ -15,6 +15,7 @@ #include "PokemonBDSP_AutoStory_Route201.h" #include "PokemonBDSP_AutoStory_Route202.h" #include "PokemonBDSP_AutoStory_SandgemTown.h" +#include "PokemonBDSP_AutoStory_MapFiles.h" #include "PokemonBDSP_AutoStory_StoryMaps.h" namespace PokemonAutomation{ @@ -37,6 +38,9 @@ const char* const MAP_ROUTE_204_SOUTH = "Route 204 South"; const char* const MAP_ROUTE_203 = "Route 203"; +// The grids are story_grid()'s: the JSON map files once the program has +// loaded them, the C++ tables in the offline tests. +// // Offsets are the maps' places in the game's world grid (the decomp's // cells): Route 201 at world (96,839), Sandgem Town at (160,832), Route 202 // at (159,800). Route 201's row 4 is Sandgem's row 11, below the doors, and @@ -44,9 +48,9 @@ const char* const MAP_ROUTE_203 = "Route 203"; const MapSet& story_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_ROUTE_201, Route201::grid(), {0, 0}); - maps.add(MAP_SANDGEM_TOWN, SandgemTown::grid(), {64, -7}); - maps.add(MAP_ROUTE_202, Route202::grid(), {63, -39}); + maps.add(MAP_ROUTE_201, story_grid("Route201"), {0, 0}); + maps.add(MAP_SANDGEM_TOWN, story_grid("SandgemTown"), {64, -7}); + maps.add(MAP_ROUTE_202, story_grid("Route202"), {63, -39}); return maps; }(); return maps; @@ -55,7 +59,7 @@ const MapSet& story_maps(){ const MapSet& jubilife_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_JUBILIFE_CITY, JubilifeCity::grid(), {0, 0}); + maps.add(MAP_JUBILIFE_CITY, story_grid("JubilifeCity"), {0, 0}); return maps; }(); return maps; @@ -66,7 +70,7 @@ const MapSet& jubilife_maps(){ const MapSet& route203_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_ROUTE_203, Route203::grid(), {0, 0}); + maps.add(MAP_ROUTE_203, story_grid("Route203"), {0, 0}); return maps; }(); return maps; @@ -85,7 +89,7 @@ const int JUBILIFE_EAST_BOTTOM = 25; } const MapSet& jubilife_east_maps(){ static const MapSet maps = []{ - const TileGrid& city = JubilifeCity::grid(); + const TileGrid& city = story_grid("JubilifeCity"); const TilePos corner = JUBILIFE_EAST_CORNER; std::vector rows; for (int y = corner.y; y <= JUBILIFE_EAST_BOTTOM; y++){ @@ -128,14 +132,14 @@ bool on_jubilife_north_road(const TilePos& pos){ const MapSet& oreburgh_mine_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_OREBURGH_MINE, OreburghMine::grid(), {0, 0}); + maps.add(MAP_OREBURGH_MINE, story_grid("OreburghMine"), {0, 0}); return maps; }(); return maps; } MapSet ravaged_path_maps(const std::vector& smashed){ - TileGrid grid = RavagedPath::grid(); + TileGrid grid = story_grid("RavagedPath"); for (const TilePos& rock : smashed){ grid.set(rock, TileType::WALKABLE); } @@ -147,7 +151,7 @@ MapSet ravaged_path_maps(const std::vector& smashed){ const MapSet& oreburgh_city_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_OREBURGH_CITY, OreburghCity::grid(), {0, 0}); + maps.add(MAP_OREBURGH_CITY, story_grid("OreburghCity"), {0, 0}); return maps; }(); return maps; @@ -156,7 +160,7 @@ const MapSet& oreburgh_city_maps(){ const MapSet& floaroma_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_FLOAROMA_TOWN, FloaromaTown::grid(), {0, 0}); + maps.add(MAP_FLOAROMA_TOWN, story_grid("FloaromaTown"), {0, 0}); return maps; }(); return maps; @@ -165,7 +169,7 @@ const MapSet& floaroma_maps(){ const MapSet& route204_north_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_ROUTE_204, Route204::north_grid(), {0, 0}); + maps.add(MAP_ROUTE_204, story_grid("Route204North"), {0, 0}); return maps; }(); return maps; @@ -174,7 +178,7 @@ const MapSet& route204_north_maps(){ const MapSet& route204_south_maps(){ static const MapSet maps = []{ MapSet maps; - maps.add(MAP_ROUTE_204_SOUTH, Route204South::grid(), {0, 0}); + maps.add(MAP_ROUTE_204_SOUTH, story_grid("Route204South"), {0, 0}); return maps; }(); return maps; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index 324ae32dc2..b911d61b0e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -1,7 +1,9 @@ #include +#include #include #include "PokemonBDSP_AutoStory.h" +#include "Data/PokemonBDSP_AutoStory_MapFiles.h" #include "Utils/PokemonBDSP_AutoStory_NewGame.h" #include "CommonFramework/Globals.h" @@ -540,6 +542,11 @@ void BDSPAutoStory::program( AutoStoryStats& stats = env.current_stats(); g_stop_at_first_failure = STOP_AT_FIRST_FAILURE; + // The maps every walk plans on, from their JSON files: stop here, before + // the game is touched, if one is missing or differs from its C++ table. + load_story_map_files(story_map_folder()); + env.log(std::format("[AutoStory] Maps: {} JSON files from {}", story_map_files().size(), story_map_folder())); + AutoStoryOptions options{ Language::English, STARTERCHOICE, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp new file mode 100644 index 0000000000..5bf5f5b137 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp @@ -0,0 +1,130 @@ +/* BDSP AutoStory Map Files Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Filesystem/Filesystem.h" +#include "Common/Cpp/Json/JsonValue.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Data/PokemonBDSP_AutoStory_MapFiles.h" +#include "PokemonBDSP_AutoStory_MapFiles_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::TileGrid; + +struct GlyphCase{ + std::string name; + std::string json; + // The grid's rows, or empty when reading it must throw. + std::vector rows; +}; + +const std::vector& glyph_cases(){ + static const std::vector cases{ + { + .name = "every BDSP tile, and a person standing", + .json = R"({"name": "T", "source": {"decomp_file": "f", "revision": "r", "date": "d"}, + "tiles": ["#.~", "vDS"], + "objects": [{"name": "npc_1_0", "kind": "npc", "x": 1, "y": 0}]})", + .rows = {"#N\"", "vDS"}, + }, + { + .name = "water has no BDSP tile", + .json = R"({"name": "T", "source": {"decomp_file": "f", "revision": "r", "date": "d"}, + "tiles": ["#w"]})", + .rows = {}, + }, + { + .name = "a wanderer has no BDSP tile", + .json = R"({"name": "T", "source": {"decomp_file": "f", "revision": "r", "date": "d"}, + "tiles": ["#.."], + "objects": [{"name": "lass", "kind": "npc", "x": 1, "y": 0, "wander": {"x0": 1, "y0": 0, "x1": 2, "y1": 0}}]})", + .rows = {}, + }, + }; + return cases; +} + +class Test_MapFileGlyphs : public UnitTest{ +public: + Test_MapFileGlyphs() + : UnitTest("PokemonBDSP::AutoStory - Map files: JSON glyphs as tile grids") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + for (const GlyphCase& item : glyph_cases()){ + std::optional grid; + std::string error; + try{ + grid = tile_grid_from_map(Pokemon::parse_map(parse_json(item.json), item.name)); + }catch (ParseException& e){ + error = e.message(); + } + if (item.rows.empty()){ + if (grid){ + return std::format("{}: read a grid, expected a ParseException.", item.name); + } + continue; + } + if (!grid){ + return std::format("{}: expected a grid, read: {}", item.name, error); + } + if (const std::optional difference = grid_difference(*grid, TileGrid(item.rows))){ + return std::format("{}: differs from the expected rows at {}.", item.name, *difference); + } + } + return true; + } +}; + +// The files are resources, not in this repository: CI has none and skips. +class Test_MapFilesEqualTables : public UnitTest{ +public: + Test_MapFilesEqualTables() + : UnitTest("PokemonBDSP::AutoStory - Map files: equal to the C++ tables cell for cell") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const std::string folder = story_map_folder(); + if (!Filesystem::exists(Filesystem::Path(folder + story_map_files()[0].name + ".json"))){ + logger.log("No BDSP map files in " + folder + "; skipped."); + return true; + } + for (const StoryMapFile& file : story_map_files()){ + const std::string path = folder + file.name + ".json"; + try{ + const TileGrid grid = tile_grid_from_map(Pokemon::parse_map(load_json_file(path), path)); + if (const std::optional difference = grid_difference(grid, file.table())){ + return std::format("{}: differs from the C++ table at {}.", path, *difference); + } + }catch (Exception& e){ + return std::format("{}: {}: {}", path, e.name(), e.message()); + } + logger.log(std::format("{}: equal to the C++ table.", path)); + } + return true; + } +}; + +} + + +void add_tests_AutoStory_MapFiles(UnitTestDatabase& database){ + database.add(); + database.add(); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h new file mode 100644 index 0000000000..fbdc3343ae --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Map Files Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// The JSON maps' glyphs as tile grids, and the map files in the resources, +// when there are any, equal to the C++ tables cell for cell. +void add_tests_AutoStory_MapFiles(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index a66f890396..f84630a0e9 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1563,6 +1563,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_TwinleafTown.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_LakeVerity.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_LakeVerity.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_VerityLakefront.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_VerityLakefront.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_OreburghGym.cpp @@ -1576,6 +1578,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp diff --git a/devtools/maps/bdsp_maps.py b/devtools/maps/bdsp_maps.py new file mode 100644 index 0000000000..bc74690a0f --- /dev/null +++ b/devtools/maps/bdsp_maps.py @@ -0,0 +1,125 @@ +"""Writes the BDSP AutoStory maps as JSON, from their C++ tile tables. + +Usage: + python3 devtools/maps/bdsp_maps.py --out Packages/Resources/PokemonBDSP/Maps + +Reads each map's rows from SerialPrograms/Source/PokemonBDSP/Programs/ +AutoStory/Data/PokemonBDSP_AutoStory_.cpp (the R"(...)" rows, in file +order) and writes /.json in the shared map schema +(SerialPrograms/Source/Pokemon/Navigation/Pokemon_MapData.h). The C++ tables +were derived from pret/pokediamond (each file's header says how); `source` +names the table and the commit it was read at, until this script reads the +decompilation itself. + +Glyphs: the tables' '"' (tall grass) is the schema's '~'; a table's 'N' (a +person standing still) is an open tile with an "npc" object on it, which +the program's loader turns back into 'N'. The other glyphs ('#', '.', 'v', +'S', 'D') are the same in both. +""" +import argparse +import datetime +import json +import os +import re +import subprocess + +DATA = 'SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data' + +# (file name, C++ source file stem): the map files the program loads, in +# the order of story_map_files() in PokemonBDSP_AutoStory_MapFiles.cpp. +MAPS = [ + ('TwinleafTown', 'TwinleafTown'), + ('VerityLakefront', 'VerityLakefront'), + ('LakeVerity', 'LakeVerity'), + ('Route201', 'Route201'), + ('SandgemTown', 'SandgemTown'), + ('Route202', 'Route202'), + ('JubilifeCity', 'JubilifeCity'), + ('Route203', 'Route203'), + ('OreburghGate', 'OreburghGate'), + ('OreburghCity', 'OreburghCity'), + ('OreburghMine', 'OreburghMine'), + ('OreburghGym', 'OreburghGym'), + ('Route204South', 'Route204South'), + ('RavagedPath', 'RavagedPath'), + ('Route204North', 'Route204'), + ('FloaromaTown', 'FloaromaTown'), +] + +ROW = re.compile(r'R"\((.*?)\)"') +TABLE_GLYPHS = set('#."vNSD') + + +def read_rows(path): + with open(path, encoding='utf-8') as f: + rows = ROW.findall(f.read()) + if not rows: + raise SystemExit(f'{path}: no rows') + width = len(rows[0]) + for y, row in enumerate(rows): + if len(row) != width: + raise SystemExit(f'{path}: row {y} is {len(row)} wide, not {width}') + unknown = set(row) - TABLE_GLYPHS + if unknown: + raise SystemExit(f'{path}: row {y} has glyphs {sorted(unknown)}') + return rows + + +def to_map(name, rows, source): + tiles = [] + objects = [] + for y, row in enumerate(rows): + line = '' + for x, glyph in enumerate(row): + if glyph == 'N': + objects.append({'name': f'npc_{x}_{y}', 'kind': 'npc', 'x': x, 'y': y}) + glyph = '.' + elif glyph == '"': + glyph = '~' + line += glyph + tiles.append(line) + data = {'name': name, 'source': source, 'tiles': tiles} + if objects: + data['objects'] = objects + return data + + +def write_map(path, data): + # One row per line, so a diff of two versions shows the changed rows. + lines = ['{'] + lines.append(f' "name": {json.dumps(data["name"])},') + lines.append(f' "source": {json.dumps(data["source"])},') + lines.append(' "tiles": [') + lines += [f' {json.dumps(row)},' for row in data['tiles']] + lines[-1] = lines[-1].rstrip(',') + objects = data.get('objects', []) + lines.append(' ]' + (',' if objects else '')) + if objects: + lines.append(' "objects": [') + lines += [f' {json.dumps(item)},' for item in objects] + lines[-1] = lines[-1].rstrip(',') + lines.append(' ]') + lines.append('}') + with open(path, 'w', encoding='utf-8', newline='\n') as f: + f.write('\n'.join(lines) + '\n') + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.split('\n')[0]) + parser.add_argument('--out', required=True, help='folder for the .json files') + parser.add_argument('--repo', default='.', help='Arduino-Source checkout') + args = parser.parse_args() + revision = subprocess.run(['git', '-C', args.repo, 'rev-parse', '--short=12', 'HEAD'], + capture_output=True, text=True, check=True).stdout.strip() + date = datetime.date.today().isoformat() + os.makedirs(args.out, exist_ok=True) + for name, stem in MAPS: + cpp = f'{DATA}/PokemonBDSP_AutoStory_{stem}.cpp' + rows = read_rows(os.path.join(args.repo, cpp)) + source = {'decomp_file': cpp, 'revision': revision, 'date': date} + write_map(os.path.join(args.out, f'{name}.json'), to_map(name, rows, source)) + print(f'{name}: {len(rows[0])} x {len(rows)}') + + +if __name__ == '__main__': + main() From c2a53bf9300a50603bf96dcc881baf55f7a9a01c Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 01:35:35 -0800 Subject: [PATCH 03/19] BDSP AutoStory: the segment tables as row lists under the code-health limits make_segment_00 and make_segment_01 build their tables from two row lists each through story_segment(), which keeps every function under 50 lines and four levels of braces; program() loads the maps through one helper call, so it does not grow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonBDSP_AutoStory.cpp | 14 ++- .../PokemonBDSP_AutoStory_Segment_00.cpp | 112 ++++++++--------- .../PokemonBDSP_AutoStory_Segment_01.cpp | 118 ++++++++++-------- .../PokemonBDSP_AutoStory_StoryTables.cpp | 10 ++ .../Utils/PokemonBDSP_AutoStory_StoryTables.h | 3 + 5 files changed, 145 insertions(+), 112 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index b911d61b0e..1f6190b9a2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -535,18 +535,20 @@ void BDSPAutoStory::on_config_value_changed(void*){ // Program logic // --------------------------------------------------------------------------- +// The maps every walk plans on, from their JSON files: stop here, before +// the game is touched, if one is missing or differs from its C++ table. +static void load_story_maps(SingleSwitchProgramEnvironment& env){ + load_story_map_files(story_map_folder()); + env.log(std::format("[AutoStory] Maps: {} JSON files from {}", story_map_files().size(), story_map_folder())); +} + void BDSPAutoStory::program( SingleSwitchProgramEnvironment& env, ProControllerContext& context ){ AutoStoryStats& stats = env.current_stats(); g_stop_at_first_failure = STOP_AT_FIRST_FAILURE; - - // The maps every walk plans on, from their JSON files: stop here, before - // the game is touched, if one is missing or differs from its C++ table. - load_story_map_files(story_map_folder()); - env.log(std::format("[AutoStory] Maps: {} JSON files from {}", story_map_files().size(), story_map_folder())); - + load_story_maps(env); AutoStoryOptions options{ Language::English, STARTERCHOICE, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 99b83e3ed5..aa42f201e4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -1409,75 +1409,75 @@ namespace PokemonAutomation { run_story_table(env, context, options, stats, make_segment_00, {"00.00", "00.00"}); } - Pokemon::Segment make_segment_00(StoryStepRunner& runner) { - Pokemon::Segment segment{"00", {}}; - auto row = [&](StoryCheckpointSpec spec) { - segment.checkpoints.push_back(story_checkpoint(runner, std::move(spec))); - }; - row({ - .id = "00.00", .title = "Verify Settings", .on_failure = StoryFailure::STOP, - .steps = { - {"verify_settings", "before any route movement or checkpoint save: fail visibly if options cannot be read, rather than navigate with unknown settings", - [](StoryConsole& c) { - verify_tutorial_settings(c.env.console, c.context); - c.context.wait_for_all_requests(); - }}, - }, - }); - row({ - .id = "00.01", .title = "After TV", .on_failure = StoryFailure::RELOAD_AND_RETRY, - .steps = { + static void verify_settings_step(StoryConsole& c) { + verify_tutorial_settings(c.env.console, c.context); + c.context.wait_for_all_requests(); + } + static void select_starter_step(StoryConsole& c) { + switch (c.options.starter_choice) { + case StarterChoice::TURTWIG: select_turtwig(c.env.console, c.context); break; + case StarterChoice::CHIMCHAR: select_chimchar(c.env.console, c.context); break; + case StarterChoice::PIPLUP: select_piplup(c.env.console, c.context); break; + } + } + + static StoryCheckpointSpec segment_00_row( + std::string id, std::string title, StoryFailure on_failure, std::vector steps + ) { + return {.id = std::move(id), .title = std::move(title), .on_failure = on_failure, .steps = std::move(steps)}; + } + + // 00.00 to 00.03: the settings, the lake and the Pokedex. + static std::vector segment_00_intro_rows() { + const StoryFailure retry = StoryFailure::RELOAD_AND_RETRY; + return { + segment_00_row("00.00", "Verify Settings", StoryFailure::STOP, { + {"verify_settings", "before any route movement or checkpoint save: fail visibly if options cannot be read, rather than navigate with unknown settings", verify_settings_step}, + }), + segment_00_row("00.01", "After TV", retry, { {"tutorial_part_1_navigation", "intro and starter selection: Butters' recorded stick holds to the lake", [](StoryConsole& c) { tutorial_part_1_navigation(c.env, c.env.console, c.context); }}, - }, - }); - row({ - .id = "00.02", .title = "Outside Verity before starter", .on_failure = StoryFailure::RELOAD_AND_RETRY, - .steps = { + }), + segment_00_row("00.02", "Outside Verity before starter", retry, { {"starter_nav", "into the lake's scene", [](StoryConsole& c) { starter_nav(c.env.console, c.context); }}, - {"select_starter", "the starter from the program's option", - [](StoryConsole& c) { - switch (c.options.starter_choice) { - case StarterChoice::TURTWIG: select_turtwig(c.env.console, c.context); break; - case StarterChoice::CHIMCHAR: select_chimchar(c.env.console, c.context); break; - case StarterChoice::PIPLUP: select_piplup(c.env.console, c.context); break; - } - }}, + {"select_starter", "the starter from the program's option", select_starter_step}, {"fight_starly", "the Starly battle", [](StoryConsole& c) { fight_starly(c.env.console, c.context); }}, {"post_starly_cleanup", "the scene after the battle", [](StoryConsole& c) { post_starly_clenaup(c.env.console, c.context); }}, - }, - }); - row({ - .id = "00.03", .title = "Outside Verity after starter", .on_failure = StoryFailure::RELOAD_AND_RETRY, - .steps = { + }), + segment_00_row("00.03", "Outside Verity after starter", retry, { {"tutorial_part_2_navigation", "Pokedex acquisition: back through Route 201 to Rowan's lab", [](StoryConsole& c) { tutorial_part_2_navigation(c.env, c.env.console, c.context); }}, - }, - }); - // 00.04 saves twice: a retry after the catch starts from the - // caught-Pokemon save, not the old route, so it is two rows. - row({ - .id = "00.04", .title = "After Pokedex, to the catch", .on_failure = StoryFailure::RELOAD_AND_RETRY, - .steps = { + }), + }; + } + + // 00.04 and 00.05: the catch, Sandgem, and Jubilife to the Poketch. + // 00.04 saves twice: a retry after the catch starts from the + // caught-Pokemon save, not the old route, so it is two rows. + static std::vector segment_00_town_rows() { + const StoryFailure retry = StoryFailure::RELOAD_AND_RETRY; + return { + segment_00_row("00.04", "After Pokedex, to the catch", retry, { {"tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", [](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); }}, - }, - }); - row({ - .id = "00.04b", .title = "After the catch, to Dawn in Jubilife", .on_failure = StoryFailure::RELOAD_AND_RETRY, - .steps = { + }), + segment_00_row("00.04b", "After the catch, to Dawn in Jubilife", retry, { {"tutorial_part_3_after_catch_navigation", "train, heal and buy Potions in Sandgem, save on Route 202, walk to Jubilife and meet Dawn", [](StoryConsole& c) { tutorial_part_3_after_catch_navigation(c.env, c.env.console, c.context); }}, - }, - }); - row({ - .id = "00.05", .title = "Jubilife After Dawn", .on_failure = StoryFailure::RELOAD_AND_RETRY, - .steps = { + }), + segment_00_row("00.05", "Jubilife After Dawn", retry, { {"tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC", [](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); }}, - }, - }); - return segment; + }), + }; + } + + Pokemon::Segment make_segment_00(StoryStepRunner& runner) { + std::vector rows = segment_00_intro_rows(); + for (StoryCheckpointSpec& row : segment_00_town_rows()) { + rows.push_back(std::move(row)); + } + return story_segment(runner, "00", std::move(rows)); } // DIAGNOSTIC ONLY: Remove before pushing to production. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp index 5cc258cc83..418cd19abf 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp @@ -1458,57 +1458,75 @@ void floaroma_old_couple(VideoStream& stream, ProControllerContext& context){ } -// Segment 01 as a table of checkpoints, each saving at its start. -Pokemon::Segment make_segment_01(StoryStepRunner& runner){ - Pokemon::Segment segment{"01", {}}; - auto row = [&](std::string id, std::string title, std::vector steps){ - segment.checkpoints.push_back(story_checkpoint(runner, StoryCheckpointSpec{ - .id = std::move(id), .title = std::move(title), - .on_failure = StoryFailure::RELOAD_AND_RETRY, .steps = std::move(steps), - })); +namespace{ + +// Saves where a checkpoint ends, inside its attempt: a failed save is a +// failed attempt. +StoryStepSpec save_here(){ + return {"save_here", "the next checkpoint starts here", [](StoryConsole& c){ + checkpoint_save(c.env, c.context, c.options.notif_status_update, c.stats, c.options.checkpoint_save_mode); + }}; +} + +StoryCheckpointSpec segment_01_row(std::string id, std::string title, std::vector steps){ + return {.id = std::move(id), .title = std::move(title), .on_failure = StoryFailure::RELOAD_AND_RETRY, .steps = std::move(steps)}; +} + +// 01.01 to 01.05: Jubilife to Oreburgh, the Mine, the Gym and back out. +std::vector segment_01_oreburgh_rows(){ + return { + segment_01_row("01.01", "Jubilife to Barry", { + {"jubilife_to_barry", "from the Center's PC out to Barry on Route 203", [](StoryConsole& c){ jubilife_to_barry(c.env.console, c.context); }}, + }), + segment_01_row("01.02", "Route 203 to Oreburgh", { + {"route203_to_oreburgh", "east through the Oreburgh Gate into Oreburgh City", [](StoryConsole& c){ route203_to_oreburgh(c.env.console, c.context); }}, + }), + segment_01_row("01.03", "Oreburgh Mine to Roark", { + {"oreburgh_to_roark", "into the Mine to Roark and back to the Center", [](StoryConsole& c){ oreburgh_to_roark(c.env.console, c.context); }}, + }), + segment_01_row("01.04", "Oreburgh Gym", { + {"oreburgh_gym", "the Gym's trainers and Roark for the Coal Badge", [](StoryConsole& c){ oreburgh_gym(c.env.console, c.context); }}, + }), + segment_01_row("01.05", "Oreburgh to Route 203", { + {"oreburgh_to_route203", "heal, then west out of the Oreburgh Gate", [](StoryConsole& c){ oreburgh_to_route203(c.env.console, c.context); }}, + }), }; - // Saves where a checkpoint ends, inside its attempt: a failed save is - // a failed attempt. - const StoryStepSpec save_here{"save_here", "the next checkpoint starts here", - [](StoryConsole& c){ - checkpoint_save(c.env, c.context, c.options.notif_status_update, c.stats, c.options.checkpoint_save_mode); - }}; - row("01.01", "Jubilife to Barry", { - {"jubilife_to_barry", "from the Center's PC out to Barry on Route 203", [](StoryConsole& c){ jubilife_to_barry(c.env.console, c.context); }}, - }); - row("01.02", "Route 203 to Oreburgh", { - {"route203_to_oreburgh", "east through the Oreburgh Gate into Oreburgh City", [](StoryConsole& c){ route203_to_oreburgh(c.env.console, c.context); }}, - }); - row("01.03", "Oreburgh Mine to Roark", { - {"oreburgh_to_roark", "into the Mine to Roark and back to the Center", [](StoryConsole& c){ oreburgh_to_roark(c.env.console, c.context); }}, - }); - row("01.04", "Oreburgh Gym", { - {"oreburgh_gym", "the Gym's trainers and Roark for the Coal Badge", [](StoryConsole& c){ oreburgh_gym(c.env.console, c.context); }}, - }); - row("01.05", "Oreburgh to Route 203", { - {"oreburgh_to_route203", "heal, then west out of the Oreburgh Gate", [](StoryConsole& c){ oreburgh_to_route203(c.env.console, c.context); }}, - }); - row("01.06", "Route 203 to Jubilife", { - {"route203_to_jubilife", "west along Route 203 into Jubilife", [](StoryConsole& c){ route203_to_jubilife(c.env.console, c.context); }}, - }); - row("01.07", "Jubilife through Rowan's scene", { - {"jubilife_to_rowan", "to the north street, Rowan's scene and the tag battle", [](StoryConsole& c){ jubilife_to_rowan(c.env.console, c.context); }}, - }); - row("01.08", "Jubilife to the Ravaged Path", { - {"jubilife_to_ravaged_path", "up Route 204's south half into the Ravaged Path", [](StoryConsole& c){ jubilife_to_ravaged_path(c.env.console, c.context); }}, - }); - row("01.09", "Ravaged Path", { - {"ravaged_path", "through the cave with Rock Smash, out onto Route 204 North", [](StoryConsole& c){ ravaged_path(c.env.console, c.context); }}, - }); - row("01.10", "Route 204 to Floaroma", { - {"route204_to_floaroma", "the human's line north into Floaroma", [](StoryConsole& c){ route204_to_floaroma(c.env.console, c.context); }}, - save_here, - }); - row("01.11", "Floaroma's old couple", { - {"floaroma_old_couple", "the old woman and the old man in the flower field", [](StoryConsole& c){ floaroma_old_couple(c.env.console, c.context); }}, - save_here, - }); - return segment; +} + +// 01.06 to 01.11: back through Jubilife, north to Floaroma. +std::vector segment_01_floaroma_rows(){ + return { + segment_01_row("01.06", "Route 203 to Jubilife", { + {"route203_to_jubilife", "west along Route 203 into Jubilife", [](StoryConsole& c){ route203_to_jubilife(c.env.console, c.context); }}, + }), + segment_01_row("01.07", "Jubilife through Rowan's scene", { + {"jubilife_to_rowan", "to the north street, Rowan's scene and the tag battle", [](StoryConsole& c){ jubilife_to_rowan(c.env.console, c.context); }}, + }), + segment_01_row("01.08", "Jubilife to the Ravaged Path", { + {"jubilife_to_ravaged_path", "up Route 204's south half into the Ravaged Path", [](StoryConsole& c){ jubilife_to_ravaged_path(c.env.console, c.context); }}, + }), + segment_01_row("01.09", "Ravaged Path", { + {"ravaged_path", "through the cave with Rock Smash, out onto Route 204 North", [](StoryConsole& c){ ravaged_path(c.env.console, c.context); }}, + }), + segment_01_row("01.10", "Route 204 to Floaroma", { + {"route204_to_floaroma", "the human's line north into Floaroma", [](StoryConsole& c){ route204_to_floaroma(c.env.console, c.context); }}, + save_here(), + }), + segment_01_row("01.11", "Floaroma's old couple", { + {"floaroma_old_couple", "the old woman and the old man in the flower field", [](StoryConsole& c){ floaroma_old_couple(c.env.console, c.context); }}, + save_here(), + }), + }; +} + +} + +Pokemon::Segment make_segment_01(StoryStepRunner& runner){ + std::vector rows = segment_01_oreburgh_rows(); + for (StoryCheckpointSpec& row : segment_01_floaroma_rows()){ + rows.push_back(std::move(row)); + } + return story_segment(runner, "01", std::move(rows)); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp index d71e73fade..5544ede503 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp @@ -77,6 +77,16 @@ Pokemon::CheckpointSpec story_checkpoint(StoryStepRunner& runner, StoryCheckpoin } +Pokemon::Segment story_segment(StoryStepRunner& runner, std::string id, std::vector rows){ + Pokemon::Segment segment; + segment.id = std::move(id); + for (StoryCheckpointSpec& row : rows){ + segment.checkpoints.push_back(story_checkpoint(runner, std::move(row))); + } + return segment; +} + + ConsoleStoryRunner::ConsoleStoryRunner(StoryConsole console) : m_console(console) {} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h index 6b9b2a500c..842290c25a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h @@ -74,6 +74,9 @@ class StoryStepRunner{ // A row of a segment table: builds the checkpoint's events from `spec`. Pokemon::CheckpointSpec story_checkpoint(StoryStepRunner& runner, StoryCheckpointSpec spec); +// A segment table of `rows`, in order. +Pokemon::Segment story_segment(StoryStepRunner& runner, std::string id, std::vector rows); + using StorySegmentFactory = std::function; From 9eca24be83a15cb89ef6b5797d86519987d13b31 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 01:58:11 -0800 Subject: [PATCH 04/19] BDSP AutoStory: copy Jubilife's grid for its east corner, for gcc -Wdangling-reference Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp index 55793a32f6..752297fb3c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp @@ -89,7 +89,7 @@ const int JUBILIFE_EAST_BOTTOM = 25; } const MapSet& jubilife_east_maps(){ static const MapSet maps = []{ - const TileGrid& city = story_grid("JubilifeCity"); + const TileGrid city = story_grid("JubilifeCity"); const TilePos corner = JUBILIFE_EAST_CORNER; std::vector rows; for (int y = corner.y; y <= JUBILIFE_EAST_BOTTOM; y++){ From cd3d0b30afab2ab86a843bc82fc2a25cbc9ed178 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 04:02:20 -0800 Subject: [PATCH 05/19] BDSP AutoStory 00.04: into Sandgem by the road, then the town plan to the Center r252 (v3, 2026-11-01) failed in 00.04's heal: from the catch grass the route costs found no plan within the cap, and the default costs' plan went round the houses south of the Mart (stopped early among the wandering townspeople), then walked west 9 along the doors' row and ended about two tiles short, near (22, 11), halfway between the Center's door (17, 10) and the Mart's (27, 10); lining up by sight then flipped between the two doors. The grid is right: door, sign and Mart are where the screen shows them, as on the grid run 2 passed with. The walk is now two: down the road with the default costs to where it stops above the Mart (SandgemTown::ROUTE_202_ENTRY_STOP, (29, 5)), then the Sandgem Center events' town plan, which ends walking north into the Center's sign (west 9, east 1, south 7, west 1, north 2; tested leg by leg). The tile grid plans test checks the first walk in place of the one-walk plan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_SandgemTown.cpp | 1 + .../Data/PokemonBDSP_AutoStory_SandgemTown.h | 3 +++ .../AutoStory/PokemonBDSP_AutoStory_Tests.cpp | 10 ++++----- ...emonBDSP_AutoStory_SandgemCenterEvents.cpp | 2 +- .../PokemonBDSP_AutoStory_Segment_00.cpp | 21 +++++++++++++------ 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.cpp index cc9da31866..d2aeaa1a2d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.cpp @@ -100,6 +100,7 @@ const TileGrid& grid(){ } +const TilePos ROUTE_202_ENTRY_STOP{29, 5}; // The lab's door, between its two front windows. const TilePos LAB_DOOR{8, 10}; // The Pokemon Center's glass door. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h index 60e5f03340..cb5d223828 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h @@ -20,6 +20,9 @@ namespace SandgemTown{ const Pokemon::TileGrid& grid(); +// Where the walk south from Route 202 down the road's east side stops, +// above the Poke Mart where the road narrows. +extern const Pokemon::TilePos ROUTE_202_ENTRY_STOP; extern const Pokemon::TilePos LAB_DOOR; extern const Pokemon::TilePos POKEMON_CENTER_DOOR; // The sign east of the Pokemon Center's door. Walking north from row 14 in diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp index ebe224c5a9..9c117daa5a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp @@ -515,9 +515,9 @@ class Test_TileGridPlans : public UnitTest{ // for 00.04's walks, the Sandgem Center events' default costs for // walk() and town costs for walk_in_town(). The planner gives up // after costs.max_searched sets, and the costs decide whether a - // walk gets a plan: after training to the Center the route costs - // find none on the game's grid and walk_route() falls back to the - // default costs, so that is the plan that runs and is checked here. + // walk gets a plan: after training, 00.04 walks to Sandgem's entry + // with the default costs (the route costs find none within the cap), + // then to the Center with the town costs, checked leg by leg below. const struct{ const char* name; std::vector from; @@ -527,8 +527,8 @@ class Test_TileGridPlans : public UnitTest{ } walks[] = { {"Save spots to the catch grass", story_save_spots(), [&](const TilePos& pos){ return std::find(grass.begin(), grass.end(), pos) != grass.end(); }, false, route_plan_costs()}, - {"After training to the Pokemon Center", story_after_training_area(), - [&](const TilePos& pos){ return pos == below_sign; }, false, Pokemon::RobustPlanCosts()}, + {"After training to Sandgem's entry", story_after_training_area(), + [&](const TilePos& pos){ return pos == sandgem_entry_stop(); }, false, Pokemon::RobustPlanCosts()}, // The Mart's door takes a tile either side: enter_lit_door() lines up by sight. {"Pokemon Center to the Poke Mart", story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR), [&](const TilePos& pos){ return pos.y == below_mart.y && std::abs(pos.x - below_mart.x) <= 1; }, true, route_plan_costs()}, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp index b81c722979..b3974652cd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp @@ -65,7 +65,7 @@ using Pokemon::pose_known; const char* const ROUTE202_TO_SANDGEM_CENTER_NAME = "Route 202 to Sandgem's Pokemon Center (events)"; TilePos sandgem_entry_stop(){ - return story_maps().to_world(MAP_SANDGEM_TOWN, {29, 5}); + return story_maps().to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); } TilePos below_sandgem_center_sign(){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index aa42f201e4..c696497169 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -693,17 +693,26 @@ namespace PokemonAutomation { } } - // South into Sandgem Town, to the tile below the Pokemon Center's sign, - // which the house west of its column and the sign pin (r75 walked this - // and healed). The Center's door is three tiles west; the heal lines up - // with it by sight. Returns where we stand after: below the door. + // South into Sandgem Town, to the tile below the Pokemon Center's sign. + // The Center's door is three tiles west; the heal lines up with it by + // sight. Returns where we stand after: below the door. In two walks: + // down the road to where it stops above the Mart, then the Sandgem + // Center events' town plan from there, which ends walking north into + // the sign. From the catch grass in one walk the route costs find no + // plan within the cap, and the default costs' plan went round the + // houses south of the Mart, where townspeople wander, and its last + // leg west 9 along the doors' row ended two tiles short, halfway + // between the Center's and the Mart's doors (r252, 2026-11-01). static std::vector heal_at_sandgem_center( VideoStream& stream, ProControllerContext& context, const MapSet& maps, std::vector& trainers, const RouteTrainerFight& fight ) { + const TilePos entry = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); const TilePos below_sign = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Pokemon::Direction::SOUTH)); - walk_route(stream, context, maps, story_after_training_area(), - [&](const TilePos& pos) { return pos == below_sign; }, trainers, fight, "To the Pokemon Center"); + const std::vector at = walk_route(stream, context, maps, story_after_training_area(), + [&](const TilePos& pos) { return pos == entry; }, trainers, fight, "Into Sandgem Town", {}, Pokemon::RobustPlanCosts()); + walk_route(stream, context, maps, at, + [&](const TilePos& pos) { return pos == below_sign; }, trainers, fight, "To the Pokemon Center", {}, town_plan_costs()); heal_at_pokemon_center(stream, context, "Sandgem Town Pokemon Center"); return story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); } From bba98e9723ad5dc093ce3797d3af4d2ff0293a98 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 04:27:20 -0800 Subject: [PATCH 06/19] BDSP AutoStory: Route 202's catch patch is three grass rows, with trees below r253 (v3, 2026-11-01) failed 00.04's training: the walk to the catch grass planned west 10 along row 29 onto (20, 29), stopped a tile short beside trees, and walked up and down the path for three minutes without a battle. The decomp has the patch at (15..20, 25..29); in BDSP its two bottom rows are trees (the screenshot shows three grass rows above a tree row, and the walk west stopped against them), as the image-traced grid had it and as encounter_one_wild_pokemon() assumes ("three rows between tree walls"). The grid marks (15..20, 28..29) as trees, and CATCH_SPOT (19, 27) is the patch's bottom row. Test_Route202CatchGrass: every patch column runs rows 25..27 between trees, and the planned walk from the save spots ends in the grass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Data/PokemonBDSP_AutoStory_Route202.cpp | 20 +++++---- .../PokemonBDSP_AutoStory_Segment00_Tests.cpp | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp index f78bc80261..85e931aa2f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp @@ -21,7 +21,9 @@ using namespace Pokemon; // trees at (21,24), the sign a row below at (24,25), the save spot // (30,24) against the east trees, Natalie's challenge at (22,22), east // along the row above the ledge stops at column 30 and column 30 runs -// north to row 4 (r75), the catch spot's walls (2026-10-29/30). +// north to row 4 (r75), the catch spot's walls (2026-10-29/30); trees, +// not grass, at (15..20, 28..29) below the catch patch (r253: west +// along row 29 stopped at column 21, beside the trees). // Status: no checkpoint has run on this grid yet. 00.04 passed on the // image-traced grid it replaces (26 rows for 32) and must be re-run; // its legs south of the catch spot are three rows longer here. @@ -46,7 +48,9 @@ using namespace Pokemon; // Tristan (7,13), Lass Natalie (22,18) and Youngster Logan (26,4), all // facing south, with the sight ranges the events give; a Potion ball (3,6), // a wall until picked up; the signs (17,4) to Jubilife, (15,16) Trainer Tips -// and (24,25) to Sandgem. A Youngster who is not a trainer wanders along row +// and (24,25) to Sandgem. BDSP has trees where Diamond's catch patch has +// its two bottom rows, (15..20, 28..29): the console's patch is three rows +// between tree rows, as the image-traced grid had it. A Youngster who is not a trainer wanders along row // 8, columns 9..11, and is left off the grid (the image-traced grid had him // as a fourth trainer facing east). Dawn's tile (15,27) is hidden by a // story flag. @@ -82,8 +86,8 @@ const TileGrid& grid(){ R"(###""""""......""""""...S......#####)", // 25 R"(###""""""......""""""..........#####)", // 26 R"(###""""""......""""""..........#####)", // 27 - R"(###""""""......""""""..........#####)", // 28 - R"(###""""""......""""""..........#####)", // 29 + R"(###""""""......######..........#####)", // 28 + R"(###""""""......######..........#####)", // 29 R"(#####################..........#####)", // 30 R"(#####################..........#####)", // 31 }); @@ -91,11 +95,11 @@ const TileGrid& grid(){ } -// Where 00.03's save after the catch stands: the middle row of the -// tall-grass patch at (15..20, 25..29), world (178,827). On the console the +// Where 00.03's save after the catch stands: the bottom row of the +// tall-grass patch at (15..20, 25..27), world (178,827). On the console the // sign at (24,25) is 5 columns right and 2 rows up, walking east stops at -// the trees after 11 tiles and north at the trees after 2; the patch goes -// on two rows below. +// the trees after 11 tiles and north at the trees after 2; south are trees +// (the decomp's two more grass rows are trees in BDSP, r253). const TilePos CATCH_SPOT{19, 27}; // Where 00.04 saves once it has Potions, before Route 202's trainers (the // console's saves from r113a..r117a stand here too): under the south ledge, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp index 6256a5e6dd..bdd0049bfb 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp @@ -9,6 +9,7 @@ #include "Pokemon/Navigation/Pokemon_MapSet.h" #include "Pokemon/Navigation/Pokemon_TileGrid.h" #include "Data/PokemonBDSP_AutoStory_OreburghGym.h" +#include "Data/PokemonBDSP_AutoStory_Route202.h" #include "Data/PokemonBDSP_AutoStory_Segment00Lines.h" #include "Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" @@ -252,12 +253,53 @@ class Test_OreburghGymGrid : public UnitTest{ } }; +// 00.04's training walks up and down the catch patch, three grass rows +// between tree rows, and needs the walk there to end in its grass: r253 +// planned onto (20, 29), a tree in BDSP, stopped a tile short on the path +// and walked up and down it for three minutes without a battle. +class Test_Route202CatchGrass : public UnitTest{ +public: + Test_Route202CatchGrass() + : UnitTest("PokemonBDSP::AutoStory - Route 202: the catch patch and the walk to it") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const TileGrid& route = Route202::grid(); + for (int x = 15; x <= 20; x++){ + const Pokemon::WallHit up = Pokemon::walk_to_wall(route, {x, 27}, Direction::NORTH); + const Pokemon::WallHit down = Pokemon::walk_to_wall(route, {x, 25}, Direction::SOUTH); + if (!(up.stop == TilePos{x, 25}) || !(down.stop == TilePos{x, 27})){ + return "Catch patch column " + std::to_string(x) + ": north from row 27 stops at " + + Pokemon::to_string(up.stop) + ", south from row 25 at " + Pokemon::to_string(down.stop) + + "; expected rows 25 and 27, between tree rows."; + } + } + const MapSet& maps = story_maps(); + const std::vector area = story_after_training_area(); + const auto in_area = [&](const TilePos& pos){ return std::find(area.begin(), area.end(), pos) != area.end(); }; + auto legs = Pokemon::plan_robust_route(maps.world(), story_save_spots(), in_area, route_plan_costs()); + if (!legs){ + legs = Pokemon::plan_robust_route(maps.world(), story_save_spots(), in_area, Pokemon::RobustPlanCosts()); + } + if (!legs || legs->empty()){ + return std::string("Save spots to the catch grass: no plan."); + } + for (const TilePos& pos : legs->back().after){ + if (maps.world().at(pos) != TileType::TALL_GRASS){ + return "Save spots to the catch grass: the walk may end on " + maps.describe(pos) + ", not in the grass."; + } + } + logger.log("Save spots to the catch grass: " + std::to_string(legs->size()) + " legs, ending in the grass."); + return true; + } +}; + } void add_tests_AutoStory_Segment00(UnitTestDatabase& database){ database.add(); database.add(); + database.add(); } From bf23b778cb174af9832c0f36049f42f0f5e64c71 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 04:46:23 -0800 Subject: [PATCH 07/19] BDSP AutoStory 00.04b: train from where the catch's save stands, into the grass The catch's save (00.04b's start) loads in the pocket east of the catch patch behind the tree row, (21, 29) by the screen (the 202 sign 3 columns right and 4 rows up, the grass's bottom row above-left), not on CATCH_SPOT. Planning from {CATCH_SPOT, SAVE_SPOT} sent r253 and r254 the wrong way: r254's first leg south was blocked at once, then "too many plans". The pocket is on the grid and reachable from the grass up the lawn column 21. - story_catch_save_area(): the patch (15..20, 25..27) and column 21 from row 25 down to the pocket; the training walk starts there (a save on SAVE_SPOT has Potions and resumes past training), and its goal is a tall-grass tile of the training column: north to the trees, east to the sign, taps west. - Route 202 grid: a tree at (21, 30), south of the pocket (r254). - Test_Route202CatchGrass plans to the grass from the whole area and from the pocket alone, with the route costs and else the default costs, as walk_route() does, and checks every end tile is grass. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Data/PokemonBDSP_AutoStory_Route202.cpp | 6 ++- .../Data/PokemonBDSP_AutoStory_StoryMaps.cpp | 14 ++++++ .../Data/PokemonBDSP_AutoStory_StoryMaps.h | 7 +++ .../PokemonBDSP_AutoStory_Segment00_Tests.cpp | 44 +++++++++++++------ .../PokemonBDSP_AutoStory_Segment_00.cpp | 11 ++++- 5 files changed, 64 insertions(+), 18 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp index 85e931aa2f..e52e11c6ab 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route202.cpp @@ -23,7 +23,9 @@ using namespace Pokemon; // along the row above the ledge stops at column 30 and column 30 runs // north to row 4 (r75), the catch spot's walls (2026-10-29/30); trees, // not grass, at (15..20, 28..29) below the catch patch (r253: west -// along row 29 stopped at column 21, beside the trees). +// along row 29 stopped at column 21, beside the trees); a tree at +// (21, 30), south of the pocket at (21, 29) where the catch's save +// loads (r254: south from there was blocked at once). // Status: no checkpoint has run on this grid yet. 00.04 passed on the // image-traced grid it replaces (26 rows for 32) and must be re-run; // its legs south of the catch spot are three rows longer here. @@ -88,7 +90,7 @@ const TileGrid& grid(){ R"(###""""""......""""""..........#####)", // 27 R"(###""""""......######..........#####)", // 28 R"(###""""""......######..........#####)", // 29 - R"(#####################..........#####)", // 30 + R"(######################.........#####)", // 30 R"(#####################..........#####)", // 31 }); return grid; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp index 752297fb3c..b4ca9851dd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp @@ -206,6 +206,20 @@ std::vector story_door_front(const char* map, const TilePos& door){ return {below.next(Direction::WEST), below, below.next(Direction::EAST)}; } +std::vector story_catch_save_area(){ + const MapSet& maps = story_maps(); + std::vector area; + for (int y = 25; y <= 29; y++){ + for (int x = 15; x <= 21; x++){ + const TilePos pos = maps.to_world(MAP_ROUTE_202, {x, y}); + if (maps.world().standable(pos)){ + area.push_back(pos); + } + } + } + return area; +} + std::vector story_after_training_area(){ const TilePos catch_spot = story_maps().to_world(MAP_ROUTE_202, Route202::CATCH_SPOT); std::vector area; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h index 02cac7789b..fec06916c4 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h @@ -79,6 +79,13 @@ bool story_at_jubilife_exit(const Pokemon::TilePos& world); // ledge (00.04 saves there once it has Potions, before Route 202's trainers). std::vector story_save_spots(); +// Where 00.04b's save after the catch may stand, in world tiles: wherever +// the catch's walks in the grass ended, the patch (15..20, 25..27) and the +// lawn column east of it, (21, 25..29), down to the pocket behind the tree +// row where r252's catch ended and saved (the save loads there: (21, 29) +// by the screen, the 202 sign 3 columns right and 4 rows up). +std::vector story_catch_save_area(); + // Where a walk out of the door `door` of `map` may leave us, in world tiles: // the tile below it and its neighbours either side, since the door opening // is not lined up with one tile (Jubilife's Center, r85). diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp index bdd0049bfb..9829ee6b64 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp @@ -5,6 +5,8 @@ */ #include +#include +#include #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Pokemon/Navigation/Pokemon_MapSet.h" #include "Pokemon/Navigation/Pokemon_TileGrid.h" @@ -255,8 +257,9 @@ class Test_OreburghGymGrid : public UnitTest{ // 00.04's training walks up and down the catch patch, three grass rows // between tree rows, and needs the walk there to end in its grass: r253 -// planned onto (20, 29), a tree in BDSP, stopped a tile short on the path -// and walked up and down it for three minutes without a battle. +// and r254 loaded the catch's save in the pocket behind the trees at +// (21, 29), planned from the catch spot instead, and walked up and down +// the lawn east of the grass for three minutes without a battle. class Test_Route202CatchGrass : public UnitTest{ public: Test_Route202CatchGrass() @@ -275,20 +278,33 @@ class Test_Route202CatchGrass : public UnitTest{ } const MapSet& maps = story_maps(); const std::vector area = story_after_training_area(); - const auto in_area = [&](const TilePos& pos){ return std::find(area.begin(), area.end(), pos) != area.end(); }; - auto legs = Pokemon::plan_robust_route(maps.world(), story_save_spots(), in_area, route_plan_costs()); - if (!legs){ - legs = Pokemon::plan_robust_route(maps.world(), story_save_spots(), in_area, Pokemon::RobustPlanCosts()); - } - if (!legs || legs->empty()){ - return std::string("Save spots to the catch grass: no plan."); - } - for (const TilePos& pos : legs->back().after){ - if (maps.world().at(pos) != TileType::TALL_GRASS){ - return "Save spots to the catch grass: the walk may end on " + maps.describe(pos) + ", not in the grass."; + const auto in_grass = [&](const TilePos& pos){ + return maps.world().at(pos) == TileType::TALL_GRASS && std::find(area.begin(), area.end(), pos) != area.end(); + }; + // As walk_route() plans: the route costs, else the default costs. + // From the whole area the catch's save may stand in, and from the + // pocket behind the trees alone, where r252 saved. + const TilePos pocket = maps.to_world(MAP_ROUTE_202, {21, 29}); + const std::vector>> starts{ + {"the catch's save area", story_catch_save_area()}, + {"the pocket behind the trees", {pocket}}, + }; + std::optional> legs; + for (const auto& [name, from] : starts){ + legs = Pokemon::plan_robust_route(maps.world(), from, in_grass, route_plan_costs()); + if (!legs){ + legs = Pokemon::plan_robust_route(maps.world(), from, in_grass, Pokemon::RobustPlanCosts()); + } + if (!legs || legs->empty()){ + return "From " + name + " to the catch grass: no plan."; + } + for (const TilePos& pos : legs->back().after){ + if (!in_grass(pos)){ + return "From " + name + " to the catch grass: the walk may end on " + maps.describe(pos) + ", not in the grass."; + } } + logger.log("From " + name + " to the catch grass: " + std::to_string(legs->size()) + " legs, ending in the grass."); } - logger.log("Save spots to the catch grass: " + std::to_string(legs->size()) + " legs, ending in the grass."); return true; } }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index c696497169..8c6457db70 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -683,9 +683,16 @@ namespace PokemonAutomation { VideoStream& stream, ProControllerContext& context, const MapSet& maps, std::vector& trainers, const RouteTrainerFight& fight ) { + // From where the catch's save may stand (not the save spot under + // the ledge: a save there has Potions and resumes past training), + // into the grass of the training column: north to the trees, east + // to the 202 sign, and taps back west. const std::vector grass = story_after_training_area(); - walk_route(stream, context, maps, story_save_spots(), - [&](const TilePos& pos) { return std::find(grass.begin(), grass.end(), pos) != grass.end(); }, + walk_route(stream, context, maps, story_catch_save_area(), + [&](const TilePos& pos) { + return maps.world().at(pos) == Pokemon::TileType::TALL_GRASS && + std::find(grass.begin(), grass.end(), pos) != grass.end(); + }, trainers, fight, "To the catch grass"); for (size_t battle = 0; battle < 3; battle++) { route_status(stream, "Training battle " + std::to_string(battle + 1) + "/3"); From cb14b8fcbe82e6bd9626aa584540a0c0e7406d76 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 04:48:49 -0800 Subject: [PATCH 08/19] BDSP AutoStory: the training area keeps only standable tiles The catch patch's two bottom rows are trees now, and plan_robust_route() refuses starts on them (tile grid plans: "Route starts must be standable"). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp | 8 ++++++-- .../AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp index b4ca9851dd..a5434e00ab 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.cpp @@ -221,11 +221,15 @@ std::vector story_catch_save_area(){ } std::vector story_after_training_area(){ - const TilePos catch_spot = story_maps().to_world(MAP_ROUTE_202, Route202::CATCH_SPOT); + const MapSet& maps = story_maps(); + const TilePos catch_spot = maps.to_world(MAP_ROUTE_202, Route202::CATCH_SPOT); std::vector area; for (int y = catch_spot.y - 2; y <= catch_spot.y + 2; y++){ for (int x = catch_spot.x - 1; x <= catch_spot.x + 1; x++){ - area.push_back({x, y}); + // The rows below the catch spot are trees in BDSP. + if (maps.world().standable({x, y})){ + area.push_back({x, y}); + } } } return area; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h index fec06916c4..3e27b3dc7a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryMaps.h @@ -93,8 +93,8 @@ std::vector story_door_front(const char* map, const Pokemon::T // Where we may stand after training in the catch grass, in world tiles: the // training walks up and down the catch spot's column, between the tree walls -// north and south of the grass, so a battle may stop us on any of its five -// rows. A column either side allows for where the catch left us. +// north and south of the grass, so a battle may stop us on any of its +// three rows. A column either side allows for where the catch left us. std::vector story_after_training_area(); // Route 202's trainers, in world tiles of story_maps(). From d3749932aa7a800f94c9ccc2afa6900d8e6e3777 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 06:05:01 -0800 Subject: [PATCH 09/19] BDSP AutoStory 00.04: tap west below Sandgem's Center door before lining up r255 (v3, 2026-11-01) walked the town plan exactly: west 9 to the wall at (20, 5), east 1, south 7, west 1, north 2 into the Center's sign at (20, 11), the tile the plan ends on. Lining up by sight from there then read the Mart's sign, 5 tiles east and a row up, as a lit doorway ("doorway 4.6 tiles across", the same reading as r252's), stepped to it, walked into it, and went back and forth between the two signs' white panels for 12 tries. The grid matches the screen (Center door (17, 10), its sign (20, 10), the Mart's sign (25, 10) and door (27, 10)). From below the sign, three D-pad taps west put us below the door, where the door is straight ahead and the lining up starts at 0. Test_SandgemCenterApproach checks the taps' row and where they end. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Segment00_Tests.cpp | 39 +++++++++++++++++++ .../PokemonBDSP_AutoStory_Segment_00.cpp | 17 ++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp index 9829ee6b64..2e7afb08cd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp @@ -12,6 +12,7 @@ #include "Pokemon/Navigation/Pokemon_TileGrid.h" #include "Data/PokemonBDSP_AutoStory_OreburghGym.h" #include "Data/PokemonBDSP_AutoStory_Route202.h" +#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" #include "Data/PokemonBDSP_AutoStory_Segment00Lines.h" #include "Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" @@ -309,6 +310,43 @@ class Test_Route202CatchGrass : public UnitTest{ } }; +// 00.04's way to Sandgem's Pokemon Center ends below the Center's sign, +// walking north into it, then taps west along the doors' row to below the +// door: the row must be open there, the door straight north of where the +// taps end, and nothing else (a sign, the Mart's door) on the way. +class Test_SandgemCenterApproach : public UnitTest{ +public: + Test_SandgemCenterApproach() + : UnitTest("PokemonBDSP::AutoStory - Sandgem Town: below the Center's sign to below its door") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const TileGrid& town = SandgemTown::grid(); + const TilePos sign = SandgemTown::POKEMON_CENTER_SIGN; + const TilePos door = SandgemTown::POKEMON_CENTER_DOOR; + if (town.at(sign) != TileType::SIGN || town.at(door) != TileType::DOOR || door.y != sign.y || door.x >= sign.x){ + return "The Center's sign " + Pokemon::to_string(sign) + " and door " + Pokemon::to_string(door) + + " are not a sign and a door west of it on one row."; + } + TilePos pos = sign.next(Direction::SOUTH); + for (int tap = 0; tap < sign.x - door.x; tap++){ + const std::optional next = town.step(pos, Direction::WEST); + if (!next){ + return "West from " + Pokemon::to_string(pos) + " is blocked."; + } + if (town.at(next->next(Direction::NORTH)) != TileType::BLOCKED && *next != door.next(Direction::SOUTH)){ + return "Above " + Pokemon::to_string(*next) + " is not the Center's wall."; + } + pos = *next; + } + if (!(pos == door.next(Direction::SOUTH))){ + return "The taps end at " + Pokemon::to_string(pos) + ", not below the door " + Pokemon::to_string(door) + "."; + } + logger.log("Sandgem: below the sign " + Pokemon::to_string(sign.next(Direction::SOUTH)) + ", " + + std::to_string(sign.x - door.x) + " taps west to below the door " + Pokemon::to_string(door) + "."); + return true; + } +}; + } @@ -316,6 +354,7 @@ void add_tests_AutoStory_Segment00(UnitTestDatabase& database){ database.add(); database.add(); database.add(); + database.add(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 8c6457db70..12c6ce0e3b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -700,9 +700,9 @@ namespace PokemonAutomation { } } - // South into Sandgem Town, to the tile below the Pokemon Center's sign. - // The Center's door is three tiles west; the heal lines up with it by - // sight. Returns where we stand after: below the door. In two walks: + // South into Sandgem Town, to the tile below the Pokemon Center's sign, + // then three taps west to below its door, where the heal lines up with + // it by sight. Returns where we stand after: below the door. In two walks: // down the road to where it stops above the Mart, then the Sandgem // Center events' town plan from there, which ends walking north into // the sign. From the catch grass in one walk the route costs find no @@ -720,6 +720,17 @@ namespace PokemonAutomation { [&](const TilePos& pos) { return pos == entry; }, trainers, fight, "Into Sandgem Town", {}, Pokemon::RobustPlanCosts()); walk_route(stream, context, maps, at, [&](const TilePos& pos) { return pos == below_sign; }, trainers, fight, "To the Pokemon Center", {}, town_plan_costs()); + // Below the door by whole D-pad taps before lining up by sight: + // from below the sign, the lit-doorway finder read the signs' + // white panels as doorways, the Mart's 5 tiles east ("doorway 4.6 + // tiles across") and the Center's, and stepped between them, and + // walked into the signs (r252, r255). + const size_t to_door = (size_t)(SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x); + const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, to_door, "west below the Center's door"); + if (west.moved_chunks < to_door) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Sandgem Town: the taps west below the Center's door moved " + std::to_string(west.moved_chunks) + " tiles.", stream); + } heal_at_pokemon_center(stream, context, "Sandgem Town Pokemon Center"); return story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); } From 9d2cf61d214cdbc2f6a28f85aae9fb30f28c1214 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 06:57:35 -0800 Subject: [PATCH 10/19] BDSP AutoStory 00.04: turn west before the taps below Sandgem's Center door r256 (v3, 2026-11-01) arrived below the Center's sign at (20, 11) facing north, and its three taps west moved two: the first only turned. Its screenshot has the player at (18, 11), one tile right of the door's middle and two left of the sign, so the door stays (17, 10), as the decomp has it; (17, 11) is open. Turn west first, then tap; two tiles moved is accepted, since the lining up by sight takes a door one tile off and dead ahead. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 12c6ce0e3b..cc91313abc 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -724,10 +724,16 @@ namespace PokemonAutomation { // from below the sign, the lit-doorway finder read the signs' // white panels as doorways, the Mart's 5 tiles east ("doorway 4.6 // tiles across") and the Center's, and stepped between them, and - // walked into the signs (r252, r255). + // walked into the signs (r252, r255). The walk north into the sign + // leaves us facing north, and a tap the other way only turns: turn + // west first (r256's first tap turned, and its three taps moved + // two). One tile short still leaves the door a tile off and dead + // ahead, which the lining up below takes from there. + route_turn(stream, context, Pokemon::Direction::WEST); const size_t to_door = (size_t)(SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x); const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, to_door, "west below the Center's door"); - if (west.moved_chunks < to_door) { + stream.log("[00.04] Taps west below the Center's door moved " + std::to_string(west.moved_chunks) + " of " + std::to_string(to_door) + " tiles."); + if (west.moved_chunks + 1 < to_door) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Sandgem Town: the taps west below the Center's door moved " + std::to_string(west.moved_chunks) + " tiles.", stream); } From 4a74ffcf9ef3a142fdff44a11146fb4d836a5948 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 06:57:55 -0800 Subject: [PATCH 11/19] BDSP AutoStory 00.04: shorter comment on the taps, under the file-length limit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Segment_00.cpp | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index cc91313abc..78455721a9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -720,15 +720,10 @@ namespace PokemonAutomation { [&](const TilePos& pos) { return pos == entry; }, trainers, fight, "Into Sandgem Town", {}, Pokemon::RobustPlanCosts()); walk_route(stream, context, maps, at, [&](const TilePos& pos) { return pos == below_sign; }, trainers, fight, "To the Pokemon Center", {}, town_plan_costs()); - // Below the door by whole D-pad taps before lining up by sight: - // from below the sign, the lit-doorway finder read the signs' - // white panels as doorways, the Mart's 5 tiles east ("doorway 4.6 - // tiles across") and the Center's, and stepped between them, and - // walked into the signs (r252, r255). The walk north into the sign - // leaves us facing north, and a tap the other way only turns: turn - // west first (r256's first tap turned, and its three taps moved - // two). One tile short still leaves the door a tile off and dead - // ahead, which the lining up below takes from there. + // Below the door by D-pad taps before lining up by sight: from below + // the sign, the finder read the signs' white panels as doorways and + // stepped between them (r252, r255). We face north, and a tap the + // other way only turns (r256): turn first. One tile short is fine. route_turn(stream, context, Pokemon::Direction::WEST); const size_t to_door = (size_t)(SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x); const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, to_door, "west below the Center's door"); From 59d88d93f749c826896b83ce36f396b2cc3d1638 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 07:30:43 -0800 Subject: [PATCH 12/19] BDSP AutoStory: indoors() holds under the evening tint; out the door stops at the fade r257 (v3, 2026-11-01) healed in Sandgem's Center, walked out, and walked on south past the road into a house: indoors() counted warm pixels only, and the evening tint (console clock ~5:20 pm) makes the outside 50..72% warm, over its 25% line; Oreburgh's ground by day is 58..69%. A room floats in black, which shows along an edge, so indoors() now also needs a near-black share of 15% in a strip 12% deep along one edge. Checked on the 1412 lab screenshots: the 54 the old rule called inside and the new one outside are all outside (Sandgem in the evening, Oreburgh by day, Jubilife at night); the Centers, the School and Barry's house stay inside. leave_door_south() takes the fade it stopped on as the door, rather than looking again and walking on. Test_IndoorsAnyLight: synthetic screens (the evening outside, a room with black below its floor, a fade, a thin shadow, grass). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Segment00_Tests.cpp | 44 +++++++++++++++++++ .../PokemonBDSP_AutoStory_PokemonCenter.cpp | 34 ++++++++++++-- .../PokemonBDSP_AutoStory_PokemonCenter.h | 10 +++-- .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 3 +- 4 files changed, 84 insertions(+), 7 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp index 2e7afb08cd..8847d73db9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp @@ -15,6 +15,8 @@ #include "Data/PokemonBDSP_AutoStory_SandgemTown.h" #include "Data/PokemonBDSP_AutoStory_Segment00Lines.h" #include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Segment00_Tests.h" @@ -347,6 +349,47 @@ class Test_SandgemCenterApproach : public UnitTest{ } }; +// 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; + } +}; + } @@ -355,6 +398,7 @@ void add_tests_AutoStory_Segment00(UnitTestDatabase& database){ database.add(); database.add(); database.add(); + database.add(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp index 2e08cd3a96..e40e6b9b92 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp @@ -228,17 +228,45 @@ bool pokemon_center_inside(const ImageViewRGB32& screen){ return image_stats(extract_box_reference(screen, ImageFloatBox(0.30, 0.55, 0.40, 0.15))).average.sum() > 250; } +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; - for (size_t y = 0; y < screen.height(); y += 8){ - for (size_t x = 0; x < screen.width(); x += 8){ + 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); } } } - return count > 0 && warm * 4 > count; + if (count == 0 || warm * 4 <= count){ + return false; + } + return mostly_dark(top) || mostly_dark(bottom) || mostly_dark(left) || mostly_dark(right); } std::optional find_lit_lamp_x(const ImageViewRGB32& screen, const ImageFloatBox& box){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h index cd7e082feb..b281da5600 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h @@ -42,9 +42,13 @@ bool pokemon_center_inside(const ImageViewRGB32& screen); std::optional find_lit_doorway(const ImageViewRGB32& screen); // Whether the screen shows a building's inside: its warm walls and floor -// fill much of the view wherever the camera is (Pokemon Centers, the -// Trainers' School: 42..65% of the screen), and outside, by day or by -// night, almost none (1..7%). +// 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); // A lit street lamp's head inside `box` (0 to 1 of the screen): the mean 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 a7625be3d9..d701acc615 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -878,7 +878,8 @@ void leave_door_south(VideoStream& stream, ProControllerContext& context, const pbf_wait(context, 1500ms); context.wait_for_all_requests(); VideoSnapshot now = stream.video().snapshot(); - out = walk.stop == WalkStop::TRANSITION || (now && !indoors(*now.frame)); + // The fade the walk stopped on is the door (r257 walked on past it). + out = walk.stop == WalkStop::TRANSITION || walk.stop == WalkStop::ARRIVED || (now && !indoors(*now.frame)); if (!out){ stream.log("[Route] " + label + ": still indoors; walking on."); } From d8afcee6da3ebc5937903137b368fcc475561059 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 08:32:50 -0800 Subject: [PATCH 13/19] BDSP AutoStory 00.04: stop training when the party faints and the game takes us home r258 (v3, 2026-11-01) lost its third training battle in Route 202's grass: Piplup used Tackle four times, nothing came for 16 s, the end-of-battle check fired on the blackout, and the player woke at home in Twinleaf. "Into Sandgem Town" then walked its plan there: out of the house's door, south (the 1850 ms of big screen changes), east 8 into the trees east of the town, and south into the trees above the pond, which is the r258 screenshot (the house with the mailbox, the pond at (12..19, 27..31), trees east and south). Route 202's grid is not involved; the step screenshots of the walk to the grass match it tile for tile ((30, 24) under the ledge, (22, 24) by the trees, the patch's corner at (20, 25)). fail_if_blacked_out() after each training battle: a room within 10 s means a blackout, and the step fails with a screenshot, so the checkpoint resets to its save before training instead of walking on from home. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Segment_00.cpp | 7 +++---- .../PokemonBDSP_AutoStory_PokemonCenter.cpp | 16 ++++++++++++++++ .../Utils/PokemonBDSP_AutoStory_PokemonCenter.h | 7 +++++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 78455721a9..2747bb164c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -683,10 +683,8 @@ namespace PokemonAutomation { VideoStream& stream, ProControllerContext& context, const MapSet& maps, std::vector& trainers, const RouteTrainerFight& fight ) { - // From where the catch's save may stand (not the save spot under - // the ledge: a save there has Potions and resumes past training), - // into the grass of the training column: north to the trees, east - // to the 202 sign, and taps back west. + // From where the catch's save may stand (a save on the save spot + // has Potions and skips training) into the training column's grass. const std::vector grass = story_after_training_area(); walk_route(stream, context, maps, story_catch_save_area(), [&](const TilePos& pos) { @@ -697,6 +695,7 @@ namespace PokemonAutomation { for (size_t battle = 0; battle < 3; battle++) { route_status(stream, "Training battle " + std::to_string(battle + 1) + "/3"); battle_1_pokemon(stream, context); + fail_if_blacked_out(stream, context, "Training battle " + std::to_string(battle + 1)); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp index e40e6b9b92..148241df7e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp @@ -269,6 +269,22 @@ bool indoors(const ImageViewRGB32& screen){ return mostly_dark(top) || mostly_dark(bottom) || mostly_dark(left) || mostly_dark(right); } +void fail_if_blacked_out(VideoStream& stream, ProControllerContext& context, const std::string& label){ + for (size_t look = 0; look < 10; look++){ + context.wait_for_all_requests(); + VideoSnapshot screen = stream.video().snapshot(); + if (screen && indoors(*screen.frame)){ + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + label + ": the party fainted; the game took us home (a room shows after the battle).", stream); + } + // The blackout's text and fade are dark; the grass is not. + if (screen && image_stats(*screen.frame).average.sum() > 150){ + return; + } + pbf_wait(context, 1000ms); + } +} + std::optional find_lit_lamp_x(const ImageViewRGB32& screen, const ImageFloatBox& box){ const size_t x0 = (size_t)(box.x * screen.width()), x1 = (size_t)((box.x + box.width) * screen.width()); const size_t y0 = (size_t)(box.y * screen.height()), y1 = (size_t)((box.y + box.height) * screen.height()); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h index b281da5600..6826d4dd09 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h @@ -51,6 +51,13 @@ std::optional find_lit_doorway(const ImageViewRGB32& screen); // dark everywhere but not warm, so it is not inside. bool indoors(const ImageViewRGB32& screen); +// After a wild battle out of doors: fails with a screenshot if the screen +// turns into a room within 10 s, which means the party fainted and the +// game took us home (r258 lost its third training battle in Route 202's +// grass, woke in Twinleaf, and walked the Sandgem plan there). Returns +// once the outside shows, not black and not a room. +void fail_if_blacked_out(VideoStream& stream, ProControllerContext& context, const std::string& label); + // A lit street lamp's head inside `box` (0 to 1 of the screen): the mean // x of its white core, or nothing if fewer than a lamp's worth of white // pixels show there (by day the lamps are not lit). From 50edfb0e9a9dfee025f4d49de870eaab73c2634f Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 08:33:16 -0800 Subject: [PATCH 14/19] BDSP AutoStory tests: plan into Sandgem from every tile of the training area Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Segment00_Tests.cpp | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp index 8847d73db9..3ed90d9066 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp @@ -390,6 +390,39 @@ class Test_IndoorsAnyLight : public UnitTest{ } }; +// After training, 00.04 walks from wherever the training left us into +// Sandgem, to where the road stops above the Mart, with the default costs +// (heal_at_sandgem_center()): from the whole area and from each of its +// tiles alone, a plan must exist and end there. +class Test_TrainingToSandgem : public UnitTest{ +public: + Test_TrainingToSandgem() + : UnitTest("PokemonBDSP::AutoStory - Route 202: from every tile of the training area into Sandgem") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const MapSet& maps = story_maps(); + const TilePos entry = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); + const std::vector area = story_after_training_area(); + std::vector> starts{area}; + for (const TilePos& pos : area){ + starts.push_back({pos}); + } + for (const std::vector& from : starts){ + const auto legs = Pokemon::plan_robust_route(maps.world(), from, + [&](const TilePos& pos){ return pos == entry; }, Pokemon::RobustPlanCosts()); + const std::string name = from.size() == 1 ? maps.describe(from[0]) : "the whole training area"; + if (!legs || legs->empty()){ + return "From " + name + " into Sandgem: no plan."; + } + if (legs->back().after.size() != 1 || !(legs->back().after[0] == entry)){ + return "From " + name + " into Sandgem: the plan does not end on " + maps.describe(entry) + "."; + } + logger.log("From " + name + " into Sandgem: " + std::to_string(legs->size()) + " legs."); + } + return true; + } +}; + } @@ -399,6 +432,7 @@ void add_tests_AutoStory_Segment00(UnitTestDatabase& database){ database.add(); database.add(); database.add(); + database.add(); } From 0d8a9d018c3139e93549578b457cac38985efd80 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 09:25:34 -0800 Subject: [PATCH 15/19] BDSP AutoStory tests: the grid tests live in #111, the indoors test in its own file Test_Route202CatchGrass, Test_SandgemCenterApproach and Test_TrainingToSandgem moved to #111 (PokemonBDSP_AutoStory_MapGrid_Tests, merged in), so Segment00_Tests.cpp is #112's again; Test_IndoorsAnyLight, v3's own, moves to PokemonBDSP_AutoStory_Indoors_Tests.cpp. No program code changes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + .../PokemonBDSP_AutoStory_Indoors_Tests.cpp | 72 +++++++ .../PokemonBDSP_AutoStory_Indoors_Tests.h | 27 +++ .../PokemonBDSP_AutoStory_Segment00_Tests.cpp | 175 ------------------ SerialPrograms/cmake/SourceFiles.cmake | 2 + 5 files changed, 103 insertions(+), 175 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 121846b840..6050d7d231 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -22,6 +22,7 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h" namespace PokemonAutomation{ @@ -297,6 +298,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory(database); add_tests_AutoStory_Segment00(database); add_tests_AutoStory_StoryTables(database); + add_tests_AutoStory_Indoors(database); add_tests_AutoStory_MapFiles(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp new file mode 100644 index 0000000000..302475bb08 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp @@ -0,0 +1,72 @@ +/* BDSP AutoStory Indoors Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" +#include "PokemonBDSP_AutoStory_Indoors_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; + } +}; + +} + + +void add_tests_AutoStory_Indoors(UnitTestDatabase& database){ + database.add(); +} + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h new file mode 100644 index 0000000000..9feb34f088 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Indoors Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Indoors_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Indoors_Tests_H + +#include "Common/Cpp/TestRunners/UnitTest.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// indoors() on synthetic screens under any light: the evening outside, +// a room, a fade, a thin shadow, grass. +void add_tests_AutoStory_Indoors(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp index 3ed90d9066..6256a5e6dd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp @@ -5,18 +5,12 @@ */ #include -#include -#include #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Pokemon/Navigation/Pokemon_MapSet.h" #include "Pokemon/Navigation/Pokemon_TileGrid.h" #include "Data/PokemonBDSP_AutoStory_OreburghGym.h" -#include "Data/PokemonBDSP_AutoStory_Route202.h" -#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" #include "Data/PokemonBDSP_AutoStory_Segment00Lines.h" #include "Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Segment00_Tests.h" @@ -258,181 +252,12 @@ class Test_OreburghGymGrid : public UnitTest{ } }; -// 00.04's training walks up and down the catch patch, three grass rows -// between tree rows, and needs the walk there to end in its grass: r253 -// and r254 loaded the catch's save in the pocket behind the trees at -// (21, 29), planned from the catch spot instead, and walked up and down -// the lawn east of the grass for three minutes without a battle. -class Test_Route202CatchGrass : public UnitTest{ -public: - Test_Route202CatchGrass() - : UnitTest("PokemonBDSP::AutoStory - Route 202: the catch patch and the walk to it") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const TileGrid& route = Route202::grid(); - for (int x = 15; x <= 20; x++){ - const Pokemon::WallHit up = Pokemon::walk_to_wall(route, {x, 27}, Direction::NORTH); - const Pokemon::WallHit down = Pokemon::walk_to_wall(route, {x, 25}, Direction::SOUTH); - if (!(up.stop == TilePos{x, 25}) || !(down.stop == TilePos{x, 27})){ - return "Catch patch column " + std::to_string(x) + ": north from row 27 stops at " + - Pokemon::to_string(up.stop) + ", south from row 25 at " + Pokemon::to_string(down.stop) + - "; expected rows 25 and 27, between tree rows."; - } - } - const MapSet& maps = story_maps(); - const std::vector area = story_after_training_area(); - const auto in_grass = [&](const TilePos& pos){ - return maps.world().at(pos) == TileType::TALL_GRASS && std::find(area.begin(), area.end(), pos) != area.end(); - }; - // As walk_route() plans: the route costs, else the default costs. - // From the whole area the catch's save may stand in, and from the - // pocket behind the trees alone, where r252 saved. - const TilePos pocket = maps.to_world(MAP_ROUTE_202, {21, 29}); - const std::vector>> starts{ - {"the catch's save area", story_catch_save_area()}, - {"the pocket behind the trees", {pocket}}, - }; - std::optional> legs; - for (const auto& [name, from] : starts){ - legs = Pokemon::plan_robust_route(maps.world(), from, in_grass, route_plan_costs()); - if (!legs){ - legs = Pokemon::plan_robust_route(maps.world(), from, in_grass, Pokemon::RobustPlanCosts()); - } - if (!legs || legs->empty()){ - return "From " + name + " to the catch grass: no plan."; - } - for (const TilePos& pos : legs->back().after){ - if (!in_grass(pos)){ - return "From " + name + " to the catch grass: the walk may end on " + maps.describe(pos) + ", not in the grass."; - } - } - logger.log("From " + name + " to the catch grass: " + std::to_string(legs->size()) + " legs, ending in the grass."); - } - return true; - } -}; - -// 00.04's way to Sandgem's Pokemon Center ends below the Center's sign, -// walking north into it, then taps west along the doors' row to below the -// door: the row must be open there, the door straight north of where the -// taps end, and nothing else (a sign, the Mart's door) on the way. -class Test_SandgemCenterApproach : public UnitTest{ -public: - Test_SandgemCenterApproach() - : UnitTest("PokemonBDSP::AutoStory - Sandgem Town: below the Center's sign to below its door") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const TileGrid& town = SandgemTown::grid(); - const TilePos sign = SandgemTown::POKEMON_CENTER_SIGN; - const TilePos door = SandgemTown::POKEMON_CENTER_DOOR; - if (town.at(sign) != TileType::SIGN || town.at(door) != TileType::DOOR || door.y != sign.y || door.x >= sign.x){ - return "The Center's sign " + Pokemon::to_string(sign) + " and door " + Pokemon::to_string(door) + - " are not a sign and a door west of it on one row."; - } - TilePos pos = sign.next(Direction::SOUTH); - for (int tap = 0; tap < sign.x - door.x; tap++){ - const std::optional next = town.step(pos, Direction::WEST); - if (!next){ - return "West from " + Pokemon::to_string(pos) + " is blocked."; - } - if (town.at(next->next(Direction::NORTH)) != TileType::BLOCKED && *next != door.next(Direction::SOUTH)){ - return "Above " + Pokemon::to_string(*next) + " is not the Center's wall."; - } - pos = *next; - } - if (!(pos == door.next(Direction::SOUTH))){ - return "The taps end at " + Pokemon::to_string(pos) + ", not below the door " + Pokemon::to_string(door) + "."; - } - logger.log("Sandgem: below the sign " + Pokemon::to_string(sign.next(Direction::SOUTH)) + ", " + - std::to_string(sign.x - door.x) + " taps west to below the door " + Pokemon::to_string(door) + "."); - 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 -// 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; - } -}; - -// After training, 00.04 walks from wherever the training left us into -// Sandgem, to where the road stops above the Mart, with the default costs -// (heal_at_sandgem_center()): from the whole area and from each of its -// tiles alone, a plan must exist and end there. -class Test_TrainingToSandgem : public UnitTest{ -public: - Test_TrainingToSandgem() - : UnitTest("PokemonBDSP::AutoStory - Route 202: from every tile of the training area into Sandgem") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const MapSet& maps = story_maps(); - const TilePos entry = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); - const std::vector area = story_after_training_area(); - std::vector> starts{area}; - for (const TilePos& pos : area){ - starts.push_back({pos}); - } - for (const std::vector& from : starts){ - const auto legs = Pokemon::plan_robust_route(maps.world(), from, - [&](const TilePos& pos){ return pos == entry; }, Pokemon::RobustPlanCosts()); - const std::string name = from.size() == 1 ? maps.describe(from[0]) : "the whole training area"; - if (!legs || legs->empty()){ - return "From " + name + " into Sandgem: no plan."; - } - if (legs->back().after.size() != 1 || !(legs->back().after[0] == entry)){ - return "From " + name + " into Sandgem: the plan does not end on " + maps.describe(entry) + "."; - } - logger.log("From " + name + " into Sandgem: " + std::to_string(legs->size()) + " legs."); - } - return true; - } -}; - } void add_tests_AutoStory_Segment00(UnitTestDatabase& database){ database.add(); database.add(); - database.add(); - database.add(); - database.add(); - database.add(); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index fbddd15d78..feeb2b1e2f 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1580,6 +1580,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp From c68a631a23536fa8721564f390ef5968dfa44344 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 09:37:30 -0800 Subject: [PATCH 16/19] BDSP AutoStory: out of a door needs the door's fade; log every look r259 (50edfb0e9) healed in Sandgem's Center, turned round to walk out, and at 800 ms one look read "not a room" (the lit Center missed its black edge) with a big screen change: leave_door_south() took that for the fade, and "To the Poke Mart" walked east inside the Center. Out is now a dark look (brightness under 120: the fade) followed by a bright one (200 and up) that is not a room; or, after a walk, three standing looks half a second apart, none a room. A big screen change no longer counts. Each look is logged with its brightness, the indoors() reading and whether the fade was seen. Test_DoorFade: r259's looks, the fade then the town, the fade then the room again, a dim fade-in. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Indoors_Tests.cpp | 44 +++++++++++ .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 77 +++++++++++-------- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 22 +++++- 3 files changed, 110 insertions(+), 33 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp index 302475bb08..c0dcea0cee 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp @@ -9,6 +9,7 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Indoors_Tests.h" namespace PokemonAutomation{ @@ -59,11 +60,54 @@ class Test_IndoorsAnyLight : public UnitTest{ } }; +// 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; + } +}; + } void add_tests_AutoStory_Indoors(UnitTestDatabase& database){ database.add(); + database.add(); } 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 d701acc615..6139da8172 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -6,10 +6,12 @@ #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" @@ -841,55 +843,70 @@ void enter_door_north(VideoStream& stream, ProControllerContext& context, const 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; +} + +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 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(); - VideoSnapshot inside = stream.video().snapshot(); - // The door's fade changes the whole screen between two looks, as walking - // never does (16 to 102 per look inside a Center; the fades read 121 to - // 192). Stop the walk there: a stick held through the fade-in walks on - // outside, and r124a stood ten rows south of the Mart's door after - // "walking out" until a wall stopped it. - ImageRGB32 last; - bool have_last = false; - // A fade, and not a Pokemon Center's inside: walking in Jubilife's - // Center changes the screen as much (r148c stopped on the mat twice). - auto fading = [&](const ImageViewRGB32& screen){ - const bool fade = have_last && walk_screen_change(last, screen) > WALK_SCENE_CHANGE && - !indoors(screen); - last = screen.copy(); - have_last = true; - return fade; + // Stop the walk at the fade: a stick held through the fade-in walks on + // outside (r124a stood ten rows south of the Mart's door; r257 walked + // into a house). + 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_fade_look(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 < 3 && !out; tries++){ - // Compare within this walk only. - have_last = false; - const WalkResult walk = walk_until(stream, context, 0, -1, 6000ms, label, fading); + const WalkResult walk = walk_until(stream, context, 0, -1, 6000ms, label, out_of_door); if (walk.stop == WalkStop::DIALOG){ pbf_mash_button(context, BUTTON_B, 2000ms); } - // Out is a black transition, or a fade after which the screen is no - // longer a building's inside. A fade guess alone fires on walking in - // Jubilife's Center (r143a, r148c), the Center check knew only the - // entrance mat (r151a), and no location banner shows after a door - // (r153, 6 s of looks). + // Let the fade-in finish, then look again standing still. pbf_wait(context, 1500ms); - context.wait_for_all_requests(); - VideoSnapshot now = stream.video().snapshot(); - // The fade the walk stopped on is the door (r257 walked on past it). - out = walk.stop == WalkStop::TRANSITION || walk.stop == WalkStop::ARRIVED || (now && !indoors(*now.frame)); + out = walk.stop == WalkStop::TRANSITION || walk.stop == WalkStop::ARRIVED || + outside_for_three_looks(stream, context, label); if (!out){ stream.log("[Route] " + label + ": still indoors; walking on."); } - // Let the fade-in finish. - pbf_wait(context, 1500ms); } 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 3d53dcf980..d7db5f66db 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -191,11 +191,27 @@ void finish_dialogue( void enter_door_north(VideoStream& stream, ProControllerContext& context, 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. The door's fade is not always -// black enough to read as a transition, so a whole new scene also counts. -// Fails with a screenshot if three walks south get nowhere. +// talk still up and walks south out of it. Out is the door's fade: a dark +// look, then a bright one that is not a room; or, after a walk, three looks +// in a row that are not a room. A big screen change alone is not: turning +// round in a lit Center reads as much (r259). Fails with a screenshot if +// three walks south get nowhere. void leave_door_south(VideoStream& stream, ProControllerContext& context, 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); + } From 82fec359b6f05cd6079d4eb310e06652d1ed01d5 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 09:42:03 -0800 Subject: [PATCH 17/19] BDSP AutoStory 00.04: Potions from the Poke Mart lady on Route 201, the clerk as a fallback Step 3 runs the Sandgem Center events' Mart lady checkpoint (the diagnostic_sandgem_mart_lady() path: the Lab's sign, west along the path to her grass, north to her row, her talk, Route 201's sign, east to the trees east of the Mart, Route 202's save spot, and the save there, which is step 4) for her 10 free Potions. She stands still: her object in the Diamond decomp has no wander, and the user has seen her stand on the console; the r113a..r120a misses were the walk, not her. After two failed runs (each fails the step, and the row resets to its save), the clerk's 5 and the old walk back and save are used instead. The Bag must have Potions after step 3 (bag_has_item() checks presence, not a count). Test_MartLadyWalk follows her walk's legs to their walls on the story maps, from below the Center's door to the front row's east end. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Indoors_Tests.cpp | 64 +++++++++++++++++++ .../PokemonBDSP_AutoStory_Segment_00.cpp | 40 ++++++------ 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp index c0dcea0cee..47aad121f7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp @@ -4,10 +4,16 @@ * */ +#include #include #include #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h" #include "Utils/PokemonBDSP_AutoStory_PokemonCenter.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Indoors_Tests.h" @@ -19,6 +25,10 @@ namespace PokemonBDSP{ namespace{ +using Pokemon::Direction; +using Pokemon::MapSet; +using Pokemon::TilePos; + // 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 @@ -102,12 +112,66 @@ class Test_DoorFade : public UnitTest{ } }; +// 00.04's Potions: the Mart lady's walk (sandgem_mart_lady_checkpoint()) +// is walks until stopped, so on the grid each leg ends at its wall: from +// below the Center's door west to the Lab's sign, a row south and west to +// her grass's edge, north to the trees on her row (she stands east of +// there), back a row south and east to Route 201's sign, a row south and +// east to the trees east of the Mart, and north to the front row's end. +struct LadyLeg{ + Direction direction; + size_t tiles; // 0: walk until the wall + TilePos end; // world tile + std::string name; +}; + +class Test_MartLadyWalk : public UnitTest{ +public: + Test_MartLadyWalk() + : UnitTest("PokemonBDSP::AutoStory - 00.04: the walk to the Poke Mart lady and back") + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + const MapSet& maps = story_maps(); + const auto sandgem = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; + const auto route201 = [&](const TilePos& pos){ return maps.to_world(MAP_ROUTE_201, pos); }; + const std::vector legs{ + {Direction::WEST, 0, east_of_lab_sign(), "west to the Lab's sign"}, + {Direction::SOUTH, 1, sandgem({6, 12}), "a row south onto the path"}, + {Direction::WEST, 0, route201({42, 5}), "west to her grass's edge"}, + {Direction::NORTH, 0, route201({42, 3}), "north to the trees, on her row"}, + {Direction::SOUTH, 1, route201({42, 4}), "a row south onto the signs' row"}, + {Direction::EAST, 0, route201_sign_west(), "east to Route 201's sign"}, + {Direction::SOUTH, 1, route201({54, 5}), "a row south onto the path"}, + {Direction::EAST, 0, sandgem({29, 12}), "east to the trees east of the Mart"}, + {Direction::NORTH, 0, sandgem(SandgemTown::FRONT_ROW_EAST_END), "north to the front row's end"}, + }; + TilePos at = sandgem(SandgemTown::POKEMON_CENTER_DOOR.next(Direction::SOUTH)); + for (const LadyLeg& leg : legs){ + if (leg.tiles == 0){ + at = Pokemon::walk_to_wall(maps.world(), at, leg.direction).stop; + }else{ + const std::optional next = maps.world().step(at, leg.direction); + if (!next){ + return leg.name + ": blocked at " + maps.describe(at) + "."; + } + at = *next; + } + if (!(at == leg.end)){ + return leg.name + ": ends at " + maps.describe(at) + ", not " + maps.describe(leg.end) + "."; + } + logger.log(leg.name + ": " + maps.describe(at)); + } + return true; + } +}; + } void add_tests_AutoStory_Indoors(UnitTestDatabase& database){ database.add(); database.add(); + database.add(); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 2747bb164c..68dd1d4d85 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -7,6 +7,7 @@ #include "CommonFramework/GlobalAutoPaths.h" #include "PokemonBDSP_AutoStory_Segment_00.h" +#include "PokemonBDSP_AutoStory_SandgemCenterEvents.h" #include "../Data/PokemonBDSP_AutoStory_JubilifeCity.h" #include "../Data/PokemonBDSP_AutoStory_Route202.h" #include "../Data/PokemonBDSP_AutoStory_SandgemTown.h" @@ -622,14 +623,12 @@ namespace PokemonAutomation { // What the post-catch half of 00.04 does, logged up front and as each // step starts. A Lv5 Piplup fainted to Route 202's trainers (r72), so: // training, a heal at the Center, and Potions (they also saved the - // Barry fight in r85) before Route 202's trainers. The Poke Mart lady - // on Route 201 gives 10 for free, but she wanders between her grass - // row and the open ground above it, and eight runs (r113a..r120a) - // never got her talk; the Mart's clerk stays put. + // Barry fight in r85) before Route 202's trainers: the Poke Mart lady's + // 10 on Route 201 (she stands still, as in the decomp), the clerk's 5 after two misses. static const char* const AFTER_CATCH_STEPS[] = { "Train: fight 3 wild Pokemon in the catch grass, with the best move", "Walk south into Sandgem Town and heal at its Pokemon Center", - "Buy 5 Potions at the Poke Mart next door, and check the Bag has them", + "Get 10 Potions from the Poke Mart lady on Route 201 (the clerk's 5 after two misses), and check the Bag", "Walk back to Route 202's save spot and save", "Walk north up Route 202 to the Jubilife exit, fighting trainers who see us; after each, use a Potion if HP is under half", "Enter Jubilife and get through Dawn's scene", @@ -699,16 +698,10 @@ namespace PokemonAutomation { } } - // South into Sandgem Town, to the tile below the Pokemon Center's sign, - // then three taps west to below its door, where the heal lines up with - // it by sight. Returns where we stand after: below the door. In two walks: - // down the road to where it stops above the Mart, then the Sandgem - // Center events' town plan from there, which ends walking north into - // the sign. From the catch grass in one walk the route costs find no - // plan within the cap, and the default costs' plan went round the - // houses south of the Mart, where townspeople wander, and its last - // leg west 9 along the doors' row ended two tiles short, halfway - // between the Center's and the Mart's doors (r252, 2026-11-01). + // South into Sandgem Town in two walks (down the road to where it stops + // above the Mart, then the town plan into the Center's sign; one walk + // went round the houses and ended two tiles short, r252), three taps + // west below the door, and in by sight. Returns: below the door. static std::vector heal_at_sandgem_center( VideoStream& stream, ProControllerContext& context, const MapSet& maps, std::vector& trainers, const RouteTrainerFight& fight @@ -1035,6 +1028,8 @@ namespace PokemonAutomation { // them, with the starter trained and healed, so Potions in the Bag // mean steps 1 to 4 are done; where that save stands is read off the // save screen (resume_with_potions()). + // Mart lady runs that failed this program run; the clerk after two. + static size_t g_mart_lady_misses = 0; static void tutorial_part_3_after_catch_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { log_after_catch_steps(stream); @@ -1053,10 +1048,17 @@ namespace PokemonAutomation { heal_at_sandgem_center(stream, context, maps, trainers, fight); after_catch_step(stream, 3); - const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); - - after_catch_step(stream, 4); - save_at_route_202_save_spot(env, stream, context, maps, at, trainers, fight); + if (g_mart_lady_misses < 2) { + // Her events walk on to Route 202's save spot and save there (step 4). + try { diagnostic_sandgem_mart_lady(env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } + } else { + const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); + after_catch_step(stream, 4); + save_at_route_202_save_spot(env, stream, context, maps, at, trainers, fight); + } + if (!bag_has_item(stream, context, "potion", 1)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after step 3.", stream); + } } after_catch_step(stream, 5); From dbad84ff3af58d712988bfe2b2e205b482d05adf Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 09:46:12 -0800 Subject: [PATCH 18/19] BDSP AutoStory: no map files from Packages at start; the built-in maps are the only copy The user's decision (2026-11-01): the maps stay files in this repository (#111, #112), built into the program; story_grid() reads only those. The start-up load of Packages' PokemonBDSP/Maps and its "differs from this build's copy" check go, with their test; the lab deploy no longer copies map files. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 - .../Data/PokemonBDSP_AutoStory_MapFiles.cpp | 51 -------------- .../Data/PokemonBDSP_AutoStory_MapFiles.h | 36 ---------- .../AutoStory/PokemonBDSP_AutoStory.cpp | 11 +-- .../PokemonBDSP_AutoStory_MapFiles_Tests.cpp | 68 ------------------- .../PokemonBDSP_AutoStory_MapFiles_Tests.h | 27 -------- SerialPrograms/cmake/SourceFiles.cmake | 4 -- 7 files changed, 1 insertion(+), 198 deletions(-) delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 6050d7d231..258b5b7789 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -23,7 +23,6 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h" -#include "Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -299,7 +298,6 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_Segment00(database); add_tests_AutoStory_StoryTables(database); add_tests_AutoStory_Indoors(database); - add_tests_AutoStory_MapFiles(database); } } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp deleted file mode 100644 index 8fa073c212..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* BDSP AutoStory - Map files - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/Json/JsonValue.h" -#include "CommonFramework/GlobalAutoPaths.h" -#include "PokemonBDSP_AutoStory_MapGrids.h" -#include "PokemonBDSP_AutoStory_Maps_TestData.h" -#include "PokemonBDSP_AutoStory_MapFiles.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using Pokemon::MapData; -using Pokemon::TileGrid; - - -std::string story_map_folder(){ - return RESOURCE_PATH() + "PokemonBDSP/Maps/"; -} - -size_t load_story_map_files(const std::string& folder){ - std::map grids; - for (const auto& [name, json] : test_map_json()){ - const std::string path = folder + name + ".json"; - const MapData map = Pokemon::parse_map(load_json_file(path), path); - if (map.name != name){ - throw ParseException(std::format("{}: name is \"{}\", not \"{}\".", path, map.name, name)); - } - TileGrid grid = tile_grid_from_map(map); - if (const std::optional difference = grid_difference(grid, built_in_grid(name))){ - throw ParseException(std::format("{}: differs from this build's copy at {}.", path, *difference)); - } - grids.emplace(name, std::move(grid)); - } - const size_t count = grids.size(); - install_story_grids(std::move(grids)); - return count; -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h deleted file mode 100644 index a1dcaa57eb..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h +++ /dev/null @@ -1,36 +0,0 @@ -/* BDSP AutoStory - Map files - * - * From: https://github.com/PokemonAutomation/ - * - * The program reads the story's maps from Packages' JSON files, - * PokemonBDSP/Maps/.json, when it starts, checks each against the - * built-in copy the unit tests run on (test_map_json()), and plans on the - * loaded grids from then on (install_story_grids()). - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_H -#define PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_H - -#include - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Where the program looks for the files: RESOURCE_PATH() + "PokemonBDSP/Maps/". -std::string story_map_folder(); - -// Reads every map in `folder` through the shared loader and checks it -// against the built-in copy. Throws FileException or ParseException naming -// the file when one is missing, malformed or differs; else story_grid() -// hands out the loaded grids from then on. Returns how many maps it read. -size_t load_story_map_files(const std::string& folder); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index 0f8f49fad0..324ae32dc2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -1,9 +1,7 @@ #include -#include #include #include "PokemonBDSP_AutoStory.h" -#include "Data/PokemonBDSP_AutoStory_MapFiles.h" #include "Utils/PokemonBDSP_AutoStory_NewGame.h" #include "CommonFramework/Globals.h" @@ -535,20 +533,13 @@ void BDSPAutoStory::on_config_value_changed(void*){ // Program logic // --------------------------------------------------------------------------- -// The maps every walk plans on, from their JSON files: stop here, before -// the game is touched, if one is missing or differs from its C++ table. -static void load_story_maps(SingleSwitchProgramEnvironment& env){ - const size_t maps = load_story_map_files(story_map_folder()); - env.log(std::format("[AutoStory] Maps: {} JSON files from {}", maps, story_map_folder())); -} - void BDSPAutoStory::program( SingleSwitchProgramEnvironment& env, ProControllerContext& context ){ AutoStoryStats& stats = env.current_stats(); g_stop_at_first_failure = STOP_AT_FIRST_FAILURE; - load_story_maps(env); + AutoStoryOptions options{ Language::English, STARTERCHOICE, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp deleted file mode 100644 index 6ae0b5e148..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* BDSP AutoStory Map Files Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/Filesystem/Filesystem.h" -#include "Common/Cpp/Json/JsonValue.h" -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "Data/PokemonBDSP_AutoStory_MapFiles.h" -#include "Data/PokemonBDSP_AutoStory_MapGrids.h" -#include "Data/PokemonBDSP_AutoStory_Maps_TestData.h" -#include "PokemonBDSP_AutoStory_MapFiles_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -using Pokemon::TileGrid; - -// Packages' files are resources, not in this repository: CI has none and -// skips. Where they are (a lab build), each must equal this build's copy. -class Test_MapFilesEqualBuiltIn : public UnitTest{ -public: - Test_MapFilesEqualBuiltIn() - : UnitTest("PokemonBDSP::AutoStory - Map files: Packages' equal the built-in copies") - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - const std::string folder = story_map_folder(); - if (!Filesystem::exists(Filesystem::Path(folder + test_map_json()[0].first + ".json"))){ - logger.log("No BDSP map files in " + folder + "; skipped."); - return true; - } - for (const auto& [name, json] : test_map_json()){ - const std::string path = folder + name + ".json"; - try{ - const TileGrid grid = tile_grid_from_map(Pokemon::parse_map(load_json_file(path), path)); - if (const std::optional difference = grid_difference(grid, built_in_grid(name))){ - return std::format("{}: differs from the built-in copy at {}.", path, *difference); - } - }catch (Exception& e){ - return std::format("{}: {}: {}", path, e.name(), e.message()); - } - logger.log(std::format("{}: equal to the built-in copy.", path)); - } - return true; - } -}; - -} - - -void add_tests_AutoStory_MapFiles(UnitTestDatabase& database){ - database.add(); -} - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h deleted file mode 100644 index d8a1ca3a62..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h +++ /dev/null @@ -1,27 +0,0 @@ -/* BDSP AutoStory Map Files Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_Tests_H -#define PokemonAutomation_PokemonBDSP_AutoStory_MapFiles_Tests_H - -#include "Common/Cpp/TestRunners/UnitTest.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - - -// The map files in Packages' resources, when there are any, equal to the -// built-in copies cell for cell. -void add_tests_AutoStory_MapFiles(UnitTestDatabase& database); - - - -} -} -} -#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 24210ba0bb..915951bc3d 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1563,8 +1563,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_TwinleafTown.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_LakeVerity.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_LakeVerity.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapFiles.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapGrids.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_MapGrids.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_VerityLakefront.cpp @@ -1584,8 +1582,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_MapFiles_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp From b08c418847daa2bef9ccf65bfe2b03ed98f0ec5b Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 09:55:01 -0800 Subject: [PATCH 19/19] BDSP AutoStory 00.04: post-catch rows 00.04b/c/d, each from a save; exit stops outside; onto the doors' row The user's rule: define checkpoints as you go and save as each completes. 00.04b ran six steps on one save, so every failure after training cost the training again. Now three rows, each saving at its start, which is the end of the one before: - 00.04b (from the catch's save): train in the catch grass, heal in Sandgem's Center. - 00.04c (from the save just out of the Center): Potions from the Mart lady (her checkpoint without its own save; the clerk after two misses), on to Route 202's save spot; the Bag must have Potions. - 00.04d (from the save on the save spot): north past Route 202's trainers into Jubilife and Dawn's scene. The resume-with-Potions branch and its Sandgem anchor go: no row starts from a save another row made mid-way any more. Checkpoint 00.04 and the post-catch diagnostic run 00.04b..00.04d. r261: out of the Center the walk saw no dark fade and held south for 2 s, three rows past the door, and the Mart lady's walk west ran into the trees. leave_door_south() now also stops at the first look that is not a room, then confirms with three standing looks (five tries); and 00.04c first turns east, taps under the Center's wall and walks north to its front, so the walk west starts on the doors' row whichever row the exit left us on. Tests: Test_StoryTables has the new rows; Test_MartLadyWalk starts one, two and three rows below the door. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_Indoors_Tests.cpp | 25 ++- ...okemonBDSP_AutoStory_StoryTables_Tests.cpp | 2 +- ...emonBDSP_AutoStory_SandgemCenterEvents.cpp | 16 +- ...okemonBDSP_AutoStory_SandgemCenterEvents.h | 5 + .../PokemonBDSP_AutoStory_Segment_00.cpp | 176 ++++++------------ .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 13 +- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 10 +- 7 files changed, 101 insertions(+), 146 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp index 47aad121f7..d3762d2c1e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp @@ -112,9 +112,10 @@ class Test_DoorFade : public UnitTest{ } }; -// 00.04's Potions: the Mart lady's walk (sandgem_mart_lady_checkpoint()) +// 00.04c's Potions: the Mart lady's walk (after_catch_potions() and the Mart lady checkpoint) // is walks until stopped, so on the grid each leg ends at its wall: from -// below the Center's door west to the Lab's sign, a row south and west to +// a tile to three below the Center's door, a tap east and north to the +// Center's front, west to the Lab's sign, a row south and west to // her grass's edge, north to the trees on her row (she stands east of // there), back a row south and east to Route 201's sign, a row south and // east to the trees east of the Mart, and north to the front row's end. @@ -135,6 +136,8 @@ class Test_MartLadyWalk : public UnitTest{ const auto sandgem = [&](const TilePos& pos){ return maps.to_world(MAP_SANDGEM_TOWN, pos); }; const auto route201 = [&](const TilePos& pos){ return maps.to_world(MAP_ROUTE_201, pos); }; const std::vector legs{ + {Direction::EAST, 1, sandgem({18, 0}), "a tap east under the Center's wall"}, + {Direction::NORTH, 0, sandgem({18, 11}), "north to the Center's front"}, {Direction::WEST, 0, east_of_lab_sign(), "west to the Lab's sign"}, {Direction::SOUTH, 1, sandgem({6, 12}), "a row south onto the path"}, {Direction::WEST, 0, route201({42, 5}), "west to her grass's edge"}, @@ -145,7 +148,18 @@ class Test_MartLadyWalk : public UnitTest{ {Direction::EAST, 0, sandgem({29, 12}), "east to the trees east of the Mart"}, {Direction::NORTH, 0, sandgem(SandgemTown::FRONT_ROW_EAST_END), "north to the front row's end"}, }; - TilePos at = sandgem(SandgemTown::POKEMON_CENTER_DOOR.next(Direction::SOUTH)); + // Out of the Center's door we stand a tile to three below it (r261). + for (int below = 1; below <= 3; below++){ + const std::string error = follow(maps, legs, sandgem({17, 10 + below}), logger); + if (!error.empty()){ + return "From " + std::to_string(below) + " below the door: " + error; + } + } + return true; + } + +private: + static std::string follow(const MapSet& maps, const std::vector& legs, TilePos at, Logger& logger){ for (const LadyLeg& leg : legs){ if (leg.tiles == 0){ at = Pokemon::walk_to_wall(maps.world(), at, leg.direction).stop; @@ -156,12 +170,13 @@ class Test_MartLadyWalk : public UnitTest{ } at = *next; } - if (!(at == leg.end)){ + // The tap east lands on whichever row we stood on. + if (&leg != &legs.front() && !(at == leg.end)){ return leg.name + ": ends at " + maps.describe(at) + ", not " + maps.describe(leg.end) + "."; } logger.log(leg.name + ": " + maps.describe(at)); } - return true; + return ""; } }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp index 909c0ad7c4..4584f48280 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp @@ -51,7 +51,7 @@ const std::vector& table_cases(){ .name = "Segment 00", .make = make_segment_00, .segment_id = "00", - .checkpoint_ids = {"00.00", "00.01", "00.02", "00.03", "00.04", "00.04b", "00.05"}, + .checkpoint_ids = {"00.00", "00.01", "00.02", "00.03", "00.04", "00.04b", "00.04c", "00.04d", "00.05"}, .unsaved = {"00.00"}, }, { diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp index b3974652cd..32c566f410 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.cpp @@ -761,14 +761,8 @@ Event on_doors_row(SandgemCenterActions& game){ } Checkpoint sandgem_mart_lady_checkpoint(MartLadyActions& game){ - // Measured on the console (2026-10-30) with walks that stop on their - // own: from below the Center's sign, west stops at the Lab's sign, which - // reads facing west. A row south is the path; west along it runs about - // 22 tiles to her grass's west edge. North from there stops at the trees - // 2 rows up, on her row. Back: a row south of her row is the signs' row, - // and east along it stops at Route 201's sign, which reads facing east. - // A row south again is the path; east along it stops at the trees east - // of the Mart, a row below the front row's east end. + // Walks that stop on their own, measured on the console (2026-10-30) and + // traced on the grid by Test_MartLadyWalk. Checkpoint checkpoint; checkpoint.id = "mart_lady"; checkpoint.interrupts.push_back(dialogue_interrupt(game)); @@ -820,6 +814,12 @@ Checkpoint sandgem_mart_lady_checkpoint(MartLadyActions& game){ return checkpoint; } +void mart_lady_without_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ + ConsoleMartLadyActions game(env, context); + Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); + checkpoint.save = {}; + run_on_console(env, checkpoint, SANDGEM_MART_LADY_NAME); +} void diagnostic_sandgem_mart_lady(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ ConsoleMartLadyActions game(env, context); Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h index 00d4eb0658..4415b1b61b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_SandgemCenterEvents.h @@ -196,6 +196,11 @@ Pokemon::Checkpoint sandgem_mart_lady_checkpoint(MartLadyActions& game); extern const char* const SANDGEM_MART_LADY_NAME; +// 00.04c's Potions: the Mart lady checkpoint, from the doors' row in front +// of Sandgem's Pokemon Center, without its save: it ends on Route 202's +// save spot, where 00.04d saves. +void mart_lady_without_save(SingleSwitchProgramEnvironment& env, ProControllerContext& context); + // DIAGNOSTIC ONLY: Remove before pushing to production. // Runs the Mart lady checkpoint on the console, from in front of Sandgem's // Pokemon Center door. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 68dd1d4d85..3ed2023a6b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -629,7 +629,7 @@ namespace PokemonAutomation { "Train: fight 3 wild Pokemon in the catch grass, with the best move", "Walk south into Sandgem Town and heal at its Pokemon Center", "Get 10 Potions from the Poke Mart lady on Route 201 (the clerk's 5 after two misses), and check the Bag", - "Walk back to Route 202's save spot and save", + "Walk back to Route 202's save spot (00.04d saves there)", "Walk north up Route 202 to the Jubilife exit, fighting trainers who see us; after each, use a Potion if HP is under half", "Enter Jubilife and get through Dawn's scene", }; @@ -903,83 +903,6 @@ namespace PokemonAutomation { [&](const TilePos& pos) { return pos == save_spot; }, trainers, fight, "Back to Route 202"); } - // Step 4: the walk above, then the save, which the save screen's own - // location must confirm (r124a saved beside the Mart, sure it stood - // on Route 202). A save in Sandgem would need the training walk to - // plan from town as well. - static void save_at_route_202_save_spot( - SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector at, std::vector& trainers, const RouteTrainerFight& fight - ) { - walk_to_route_202_save_spot(stream, context, maps, std::move(at), trainers, fight); - stream.log("[00.04] Saving with the Potions, at the save spot on Route 202."); - save_game(env, context, MAP_ROUTE_202); - } - - // From anywhere on Sandgem Town's open ground, to tiles the map can - // confirm: east until the trees or a wall stop us, then north until a - // wall. The columns under the north exit run on to Route 202, whose - // banner then says so; the others end on the Mart's row, where its - // lit doorway says which tile (r124a's save stands beside its door). - static std::vector anchor_in_sandgem(VideoStream& stream, ProControllerContext& context, const MapSet& maps) { - using Pokemon::Direction; - route_status(stream, "Sandgem Town: east to the trees, then north to the Mart's row."); - std::vector banners; - const WalkResult east = walk_until(stream, context, 1, 0, 6000ms, "Sandgem: east to the trees"); - const WalkResult north = walk_until(stream, context, 0, 1, 9000ms, "Sandgem: north to the Mart's row", banner_keeper(banners)); - for (const WalkResult& walk : {east, north}) { - if (walk.stop == WalkStop::TRANSITION || walk.stop == WalkStop::DIALOG) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Sandgem Town: " + std::string(walk_stop_name(walk.stop)) + " instead of a wall.", stream); - } - } - context.wait_for_all_requests(); - VideoSnapshot screen = stream.video().snapshot(); - if (!screen) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "Sandgem Town: no video frame.", stream); - } - banner_keeper(banners)(*screen.frame); - if (any_banner_names(banners, MAP_ROUTE_202)) { - std::vector ends; - for (int x : {25, 29}) { - ends.push_back(Pokemon::walk_to_wall(maps.world(), maps.to_world(MAP_SANDGEM_TOWN, {x, 0}), Direction::NORTH).stop); - } - route_status(stream, "Sandgem Town: Route 202's banner showed on the way north; on Route 202."); - return ends; - } - const TilePos door = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); - const std::optional doorway = find_lit_doorway(*screen.frame); - const std::vector at = doorway ? tiles_by_lit_doorway(*doorway, maps.world(), door) : std::vector{}; - if (at.empty()) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Sandgem Town: no Route 202 banner and no lit doorway in sight; cannot place the player.", stream); - } - route_status(stream, "Sandgem Town: the Mart's doorway is " + std::to_string(doorway->tiles_across) + - " tiles across and " + std::to_string(doorway->rows_up) + " rows up."); - return at; - } - - // With Potions in the Bag, steps 1 to 4 are done, but where the save - // stands is the save screen's to say: step 4's at Route 202's save - // spot, or r124a's in Sandgem Town. From town, walk back to the save - // spot first, without saving again. - static void resume_with_potions( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector& trainers, const RouteTrainerFight& fight - ) { - const std::string where = read_location_from_save_screen(stream, context); - if (location_banner_matches(where, MAP_ROUTE_202)) { - stream.log("[00.04] The Bag has Potions and the save is on Route 202: step 4's save; on to Route 202's trainers.", COLOR_BLUE); - return; - } - if (!location_banner_matches(where, MAP_SANDGEM_TOWN)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "The Bag has Potions, but the save screen reads \"" + where + "\": neither Route 202 nor Sandgem Town.", stream); - } - stream.log("[00.04] The Bag has Potions, but the save is in Sandgem Town: back to Route 202's save spot first, without saving.", COLOR_BLUE); - walk_to_route_202_save_spot(stream, context, maps, anchor_in_sandgem(stream, context, maps), trainers, fight); - } - // DIAGNOSTIC ONLY (the Mart lady checkpoint): 00.04's walk back to // Route 202's save spot, from below Sandgem Center's door, without saving. void walk_from_sandgem_center_to_route_202_save_spot( @@ -1023,54 +946,59 @@ namespace PokemonAutomation { pbf_mash_button(context, BUTTON_B, 5000ms); } - // Resumes from the save made after catching the second Pokemon, or - // from step 4's save. The catch save has no Potions; step 4's has - // them, with the starter trained and healed, so Potions in the Bag - // mean steps 1 to 4 are done; where that save stands is read off the - // save screen (resume_with_potions()). - // Mart lady runs that failed this program run; the clerk after two. + // 00.04b, 00.04c and 00.04d: the post-catch steps as three rows, each + // starting from the save the one before ends on. Mart lady runs that + // failed this program run: the clerk after two. static size_t g_mart_lady_misses = 0; - static void tutorial_part_3_after_catch_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { + // 00.04b, from the catch's save: steps 1 and 2. + static void after_catch_train_and_heal(VideoStream& stream, ProControllerContext& context) { log_after_catch_steps(stream); - const MapSet& maps = story_maps(); std::vector trainers = story_trainer_sights(); const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; pbf_wait(context, 1500ms); - - if (bag_has_item(stream, context, "potion", 1)) { - resume_with_potions(stream, context, maps, trainers, fight); - } else { - after_catch_step(stream, 1); - train_in_catch_grass(stream, context, maps, trainers, fight); - - after_catch_step(stream, 2); - heal_at_sandgem_center(stream, context, maps, trainers, fight); - - after_catch_step(stream, 3); - if (g_mart_lady_misses < 2) { - // Her events walk on to Route 202's save spot and save there (step 4). - try { diagnostic_sandgem_mart_lady(env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } - } else { - const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); - after_catch_step(stream, 4); - save_at_route_202_save_spot(env, stream, context, maps, at, trainers, fight); - } - if (!bag_has_item(stream, context, "potion", 1)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after step 3.", stream); + after_catch_step(stream, 1); + train_in_catch_grass(stream, context, maps, trainers, fight); + after_catch_step(stream, 2); + heal_at_sandgem_center(stream, context, maps, trainers, fight); + } + // 00.04c, from the save out of Sandgem's Center: steps 3 and 4, up to + // Route 202's save spot, where 00.04d's save goes. + static void after_catch_potions(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { + const MapSet& maps = story_maps(); + std::vector trainers = story_trainer_sights(); + const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; + after_catch_step(stream, 3); + if (g_mart_lady_misses < 2) { + // Onto the doors' row first: out of the door we stand a tile to + // three below it (r261 walked west into the trees from three). + route_turn(stream, context, Pokemon::Direction::EAST); + walk_tiles_dpad(stream, context, 1, 0, 1, "east under the Center's wall"); + const WalkResult up = walk_until(stream, context, 0, 1, 3000ms, "north to the Center's front"); + if (up.stop != WalkStop::BLOCKED) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + ".", stream); } + try { mart_lady_without_save(env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } + } else { + const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); + after_catch_step(stream, 4); + walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); } - + if (!bag_has_item(stream, context, "potion", 1)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after step 3.", stream); + } + } + // 00.04d, from the save on Route 202's save spot: steps 5 and 6. From + // both save spots at once the planner gives up (no plan within 400000 + // sets; 1300 from the one). + static void after_catch_to_jubilife(VideoStream& stream, ProControllerContext& context) { + const MapSet& maps = story_maps(); + std::vector trainers = story_trainer_sights(); + const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; after_catch_step(stream, 5); - // From the save spot under the ledge, where every way here ends: - // step 4 saves on it, and a resume with Potions is either that - // save or walks back to it first. From both save spots at once - // the planner gives up: the route costs' exact open legs move the - // pair in step without ever joining it, so the sets of two tiles - // run past its cap (no plan within 400000 sets; 1300 from here). const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); walk_route(stream, context, maps, {save_spot}, story_at_jubilife_exit, trainers, fight, "To Jubilife"); - after_catch_step(stream, 6); meet_dawn_in_jubilife(stream, context); } @@ -1490,9 +1418,17 @@ namespace PokemonAutomation { {"tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", [](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); }}, }), - segment_00_row("00.04b", "After the catch, to Dawn in Jubilife", retry, { - {"tutorial_part_3_after_catch_navigation", "train, heal and buy Potions in Sandgem, save on Route 202, walk to Jubilife and meet Dawn", - [](StoryConsole& c) { tutorial_part_3_after_catch_navigation(c.env, c.env.console, c.context); }}, + segment_00_row("00.04b", "After the catch: train and heal in Sandgem", retry, { + {"train_and_heal", "three wild battles in the catch grass, then Sandgem's Pokemon Center", + [](StoryConsole& c) { after_catch_train_and_heal(c.env.console, c.context); }}, + }), + segment_00_row("00.04c", "Potions, back to Route 202's save spot", retry, { + {"potions", "the Poke Mart lady's (the clerk's after two misses), then Route 202's save spot", + [](StoryConsole& c) { after_catch_potions(c.env, c.env.console, c.context); }}, + }), + segment_00_row("00.04d", "Route 202's trainers, Dawn in Jubilife", retry, { + {"to_jubilife", "north up Route 202 past its trainers, into Jubilife and Dawn's scene", + [](StoryConsole& c) { after_catch_to_jubilife(c.env.console, c.context); }}, }), segment_00_row("00.05", "Jubilife After Dawn", retry, { {"tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC", @@ -1514,7 +1450,7 @@ namespace PokemonAutomation { SingleSwitchProgramEnvironment& env, ProControllerContext& context, AutoStoryOptions options, AutoStoryStats& stats ) { - run_story_table(env, context, options, stats, make_segment_00, {"00.04b", "00.04b"}); + run_story_table(env, context, options, stats, make_segment_00, {"00.04b", "00.04d"}); } // ======================== @@ -1581,7 +1517,7 @@ namespace PokemonAutomation { AutoStoryOptions options, AutoStoryStats& stats ) const { - run_story_table(env, context, options, stats, make_segment_00, {"00.04", "00.04b"}); + run_story_table(env, context, options, stats, make_segment_00, {"00.04", "00.04d"}); } // ======================== // Checkpoint 0005 Wrapper 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 6139da8172..f1ac20cb6e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -875,28 +875,27 @@ void leave_door_south(VideoStream& stream, ProControllerContext& context, const // 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: a stick held through the fade-in walks on - // outside (r124a stood ten rows south of the Mart's door; r257 walked - // into a house). + // 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_fade_look(watch, brightness, room); + const bool out = door_fade_look(watch, brightness, room) || !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 < 3 && !out; tries++){ + for (size_t tries = 0; tries < 5 && !out; tries++){ const WalkResult walk = walk_until(stream, context, 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 || walk.stop == WalkStop::ARRIVED || - outside_for_three_looks(stream, context, label); + out = walk.stop == WalkStop::TRANSITION || outside_for_three_looks(stream, context, label); if (!out){ stream.log("[Route] " + label + ": still indoors; walking on."); } 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 d7db5f66db..5dc613496a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -191,11 +191,11 @@ void finish_dialogue( void enter_door_north(VideoStream& stream, ProControllerContext& context, 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. Out is the door's fade: a dark -// look, then a bright one that is not a room; or, after a walk, three looks -// in a row that are not a room. A big screen change alone is not: turning -// round in a lit Center reads as much (r259). Fails with a screenshot if -// three walks south get nowhere. +// 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 std::string& label); // Brightness (mean r + g + b) below which a look is the door's fade, and