diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 979c88c3f0..258b5b7789 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -21,6 +21,8 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h" #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" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -294,6 +296,8 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_MapGrids(database); add_tests_AutoStory(database); add_tests_AutoStory_Segment00(database); + add_tests_AutoStory_StoryTables(database); + add_tests_AutoStory_Indoors(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..d3762d2c1e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.cpp @@ -0,0 +1,195 @@ +/* BDSP AutoStory Indoors Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#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" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +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 +// evening outside (warm all over), a room (warm, black along the bottom), +// a fade (black all over), and warm with a thin dark edge (a shadow). +struct IndoorsCase{ + std::string name; + uint32_t fill; + double dark_bottom; // how much of the screen's height, from the bottom, is black + bool indoors; +}; + +class Test_IndoorsAnyLight : public UnitTest{ +public: + Test_IndoorsAnyLight() + : UnitTest("PokemonBDSP::AutoStory - indoors() under the evening tint") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "evening outside, orange all over", .fill = 0xffe0a040, .dark_bottom = 0, .indoors = false}, + {.name = "a room: orange, black below its floor", .fill = 0xffe0a040, .dark_bottom = 0.10, .indoors = true}, + {.name = "a fade: black all over", .fill = 0xff000000, .dark_bottom = 0, .indoors = false}, + {.name = "orange with a thin shadow at the bottom", .fill = 0xffe0a040, .dark_bottom = 0.01, .indoors = false}, + {.name = "daytime grass", .fill = 0xff60b040, .dark_bottom = 0.10, .indoors = false}, + }; + for (const IndoorsCase& item : cases){ + ImageRGB32 screen(1920, 1080); + const size_t dark_from = (size_t)(1080 * (1 - item.dark_bottom)); + for (size_t y = 0; y < 1080; y++){ + for (size_t x = 0; x < 1920; x++){ + screen.pixel(x, y) = y >= dark_from ? 0xff000000 : item.fill; + } + } + if (indoors(screen) != item.indoors){ + return item.name + (item.indoors ? ": read as outside." : ": read as inside."); + } + } + return true; + } +}; + +// door_fade_look(): out of a door is a dark look, then a bright one that is +// not a room. r259 turned round in Sandgem's lit Center, read "not a room" +// once at full brightness, and walked east inside it as if outside. +struct DoorLook{ + double brightness; + bool room; +}; +struct DoorCase{ + std::string name; + std::vector looks; + // The look (1-based) that reads out, or 0 for none. + size_t out_at; +}; + +class Test_DoorFade : public UnitTest{ +public: + Test_DoorFade() + : UnitTest("PokemonBDSP::AutoStory - out of a door needs its fade") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::vector cases{ + {.name = "r259: a bright look in the Center that misses the room", .looks = {{458, true}, {440, false}, {457, true}}, .out_at = 0}, + {.name = "the fade, then the town", .looks = {{458, true}, {300, true}, {60, false}, {310, false}}, .out_at = 4}, + {.name = "the fade, then the room again", .looks = {{458, true}, {60, false}, {420, true}}, .out_at = 0}, + {.name = "the fade, still dim", .looks = {{60, false}, {150, false}, {320, false}}, .out_at = 3}, + }; + for (const DoorCase& item : cases){ + DoorFadeWatch watch; + size_t out_at = 0; + for (size_t i = 0; i < item.looks.size() && out_at == 0; i++){ + if (door_fade_look(watch, item.looks[i].brightness, item.looks[i].room)){ + out_at = i + 1; + } + } + if (out_at != item.out_at){ + return item.name + ": out at look " + std::to_string(out_at) + ", expected " + std::to_string(item.out_at) + "."; + } + } + return true; + } +}; + +// 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 +// 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. +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::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"}, + {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"}, + }; + // 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; + }else{ + const std::optional next = maps.world().step(at, leg.direction); + if (!next){ + return leg.name + ": blocked at " + maps.describe(at) + "."; + } + at = *next; + } + // 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 ""; + } +}; + +} + + +void add_tests_AutoStory_Indoors(UnitTestDatabase& database){ + database.add(); + database.add(); + 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_StoryTables_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp new file mode 100644 index 0000000000..4584f48280 --- /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.04c", "00.04d", "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_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 99aece50f1..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 @@ -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,15 +623,13 @@ 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", - "Walk back to Route 202's save spot and save", + "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 (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", }; @@ -683,27 +682,48 @@ namespace PokemonAutomation { VideoStream& stream, ProControllerContext& context, const MapSet& maps, std::vector& trainers, const RouteTrainerFight& fight ) { + // 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_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"); battle_1_pokemon(stream, context); + fail_if_blacked_out(stream, context, "Training battle " + std::to_string(battle + 1)); } } - // Into Sandgem down the road, then the town plan into the Center's sign (one walk - // found no plan, r252 ended two tiles short), and in by sight. Returns: below the door. + // 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 ) { 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, walk_route(stream, context, maps, story_after_training_area(), - [&](const TilePos& pos) { return pos == entry; }, trainers, fight, "Into Sandgem Town", {}, Pokemon::RobustPlanCosts()), + 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()); + // 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"); + 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); + } heal_at_pokemon_center(stream, context, "Sandgem Town Pokemon Center"); return story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); } @@ -883,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( @@ -1003,45 +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()). - static void tutorial_part_3_after_catch_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { + // 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; + // 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); + 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 { - 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); 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); + 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); } @@ -1393,12 +1350,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 +1363,94 @@ 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); - }, - true - ); + static void verify_settings_step(StoryConsole& c) { + verify_tutorial_settings(c.env.console, c.context); + c.context.wait_for_all_requests(); } - 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); - }, - true - ); + 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 void checkpoint_0003( - SingleSwitchProgramEnvironment& env, - ProControllerContext& context, - AutoStoryOptions options, - AutoStoryStats& stats + + static StoryCheckpointSpec segment_00_row( + std::string id, std::string title, StoryFailure on_failure, std::vector steps ) { - checkpoint_reattempt_loop(env, context, options, stats, - [&](size_t /*attempt*/) { - // Segment 00-002: Pokedex acquisition - tutorial_part_2_navigation(env, env.console, context); - }, - true - ); + return {.id = std::move(id), .title = std::move(title), .on_failure = on_failure, .steps = std::move(steps)}; } - 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); - }, - 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); - }, - true - ); + // 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); }}, + }), + 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", 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); }}, + }), + 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 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); }}, + }), + 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", + [](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); }}, + }), + }; + } + + 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. 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.04d"}); } // ======================== @@ -1531,7 +1466,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 +1483,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 +1501,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 +1517,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.04d"}); } // ======================== // Checkpoint 0005 Wrapper @@ -1598,7 +1533,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..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 @@ -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,78 @@ 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 - ); + +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); }}, + }), + }; } + +// 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)); } @@ -1629,17 +1543,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 +1557,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 +1570,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 +1585,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 +1600,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 +1613,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 +1626,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 +1639,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 +1652,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 +1665,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 +1678,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 +1691,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_PokemonCenter.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.cpp index 2e08cd3a96..148241df7e 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,61 @@ 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); } + } + } + if (count == 0 || warm * 4 <= count){ + return false; + } + 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); } - return count > 0 && warm * 4 > count; } 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..6826d4dd09 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PokemonCenter.h @@ -42,11 +42,22 @@ 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); +// 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). 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..f1ac20cb6e 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,54 +843,69 @@ 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 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) || !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); + 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); } - // 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(); - out = walk.stop == WalkStop::TRANSITION || (now && !indoors(*now.frame)); + out = walk.stop == WalkStop::TRANSITION || 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..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,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, 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 +// 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); + } 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..5544ede503 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp @@ -0,0 +1,202 @@ +/* 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; +} + + +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) +{} + +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..842290c25a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h @@ -0,0 +1,117 @@ +/* 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); + +// A segment table of `rows`, in order. +Pokemon::Segment story_segment(StoryStepRunner& runner, std::string id, std::vector rows); + +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 625af88a54..915951bc3d 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1580,8 +1580,12 @@ 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_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 @@ -1600,6 +1604,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