From 33a6c4f72ae489de7e12b00160e2add7909393bb Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 19:43:07 -0800 Subject: [PATCH 1/4] FRLG AutoStory: 00.11 Route 3's trainers, 00.12 to the Magikarp salesman 00.10 now ends below Pewter's Pokemon Center door, facing south, where the by-hand walk stopped. 00.11 heals there and runs out of Pewter onto Route 3, stepping into the lines of Bug Catchers Colton, Greg and James one at a time to fight them, keeping out of the others' lines and the grass, and saves at (36, 8). 00.12 runs over Route 3 into Route 4 and its Pokemon Center, heals, and saves beside the Magikarp salesman, facing him: the seam for a Gift RNG program. The save card knows "Route 3" and "Route 4"; a map's place is matched as its name or a building in it, so Route24 isn't Route 2. A table test walks every route from the Running Shoes on and fails on a trainer's line the table doesn't fight, tall grass, or a warp to a map not loaded (Route 4's ways into Mt. Moon). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Menus/PokemonFRLG_SaveCardReader.cpp | 2 + .../Menus/PokemonFRLG_SaveCardReader.h | 7 +- .../AutoStory/PokemonFRLG_AutoStory.cpp | 2 +- .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 9 +- .../AutoStory/PokemonFRLG_AutoStoryEvents.h | 5 +- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 57 ++++++- .../AutoStory/PokemonFRLG_AutoStoryTable.h | 11 +- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 157 +++++++++++++++++- 8 files changed, 226 insertions(+), 24 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp index 702d314298..0793744ff9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp @@ -68,6 +68,8 @@ class PlaceNameMatcher : public OCR::SmallDictionaryMatcher{ {"pewter-city", "Pewter City"}, {"route-1", "Route 1"}, {"route-2", "Route 2"}, + {"route-3", "Route 3"}, + {"route-4", "Route 4"}, }){ JsonArray names; names.push_back(name); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.h index 8a037e0242..e7f81aa521 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.h @@ -4,8 +4,9 @@ * * Reads where the game would be saved: the place name in blue at the top * of the card the START menu's SAVE shows ("PALLET TOWN" over PLAYER, - * BADGES and TIME). It knows the places from a new game to Pewter City, - * in English only so far: in another language nothing is read. + * BADGES and TIME). It knows the places from a new game to Route 4, in + * English only so far: in another language nothing is read. Route 3 and + * Route 4 have no test capture yet. * */ @@ -30,7 +31,7 @@ namespace PokemonFRLG{ ImageFloatBox save_card_title_box(); // The place on the save card: "pallet-town", "viridian-city", -// "viridian-forest", "pewter-city", "route-1" or "route-2"; empty if it +// "viridian-forest", "pewter-city", "route-1" to "route-4"; empty if it // can't be read, as in a language the reader has no names for. std::string read_save_card_location(Logger& logger, Language language, const ImageViewRGB32& screen); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index bca45f1643..dba48205fa 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -54,7 +54,7 @@ std::unique_ptr AutoStory_Descriptor::make_stats() const{ namespace{ // The start and end dropdowns' entries: the table's checkpoints, 00.01 to -// 00.10. The options hold a reference to it. +// 00.12. The options hold a reference to it. const StringSelectDatabase& checkpoint_database(){ static const StringSelectDatabase database = Pokemon::story_checkpoint_database(story_checkpoints()); return database; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index e5b10dc178..8225ceb38c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -275,15 +275,18 @@ std::optional story_pose(const Pokemon::Pose& pose){ } std::string save_card_place(const std::string& map){ - // The longer names first: "ViridianForest" before "ViridianCity". - static const std::pair PLACES[] = { + // A map of the place, or a building in it ("Route4_PokemonCenter_1F" + // is on Route 4, "Route24" isn't on Route 2). + static const std::pair PLACES[] = { {"PalletTown", "pallet-town"}, {"ViridianForest", "viridian-forest"}, {"ViridianCity", "viridian-city"}, {"PewterCity", "pewter-city"}, + {"Route3", "route-3"}, + {"Route4", "route-4"}, }; for (const auto& [prefix, place] : PLACES){ - if (map.rfind(prefix, 0) == 0){ + if (map == prefix || map.rfind(prefix + "_", 0) == 0){ return place; } } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h index bcf4ee87ed..297120ba5a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h @@ -117,8 +117,9 @@ StoryRunPlan plan_story_run( ); -// The maps the story plans on: every map to Brock, with the objects the -// game hides after the Hall of Fame removed, as the lab's frlg_route does. +// The maps the story plans on: every map to Route 4's Pokemon Center, with +// the objects the game hides after the Hall of Fame removed, as the lab's +// frlg_route does. const std::vector& story_maps(); Pokemon::Pose to_pose(const StoryPose& pose); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 07a5055055..552bc29f1b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -29,6 +29,8 @@ const char* const FOREST_GATE = "Route2_ViridianForest_NorthEntrance"; const char* const PEWTER = "PewterCity"; const char* const PEWTER_CENTER = "PewterCity_PokemonCenter_1F"; const char* const GYM = "PewterCity_Gym"; +const char* const ROUTE3 = "Route3"; +const char* const ROUTE4_CENTER = "Route4_PokemonCenter_1F"; Pokemon::Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ return to_pose(StoryPose{map, {x, y}, facing}); @@ -101,9 +103,9 @@ StoryStep legs(std::string name, std::vector legs, std::string why, Wi return step; } -// `step`, a ROUTE or LEGS, run with B held: from the Running Shoes on. No -// checkpoint after 00.10 is in the table yet. -[[maybe_unused]] StoryStep running(StoryStep step){ +// `step`, a ROUTE or LEGS, run with B held: from the Running Shoes on. +// Indoors, where the game doesn't let the player run, it walks. +StoryStep running(StoryStep step){ step.variant = RUN_VARIANT; return step; } @@ -298,6 +300,53 @@ StoryCheckpoint running_shoes(){ // the player only turns up. He walks off and is removed; FLAG_SYS_B_DASH // is set (console B, 2026-11-02: about 20 B presses to the overworld). scene("shoes_from_aide", at(PEWTER, 46, 21, NORTH), "\"Oh, PLAYER!\" ... the Running Shoes; ends facing north"), + // Where the by-hand walk stopped to start 00.11 (console B, 2026-11-02). + route("below_pewter_center", at(PEWTER, 17, 26, SOUTH), "west along row 21 and down to below the Pokemon Center's door (17, 25)"), + }); +} + +// Route 3's trainers (Route3 map.json): the way east meets three of them. +// Bug Catcher Colton (12, 6) faces east, sight 3: the only way north past +// the row 7 ledges is the gap at x 14..15, below his line. Youngster Ben +// (17, 4) faces south, sight 2, so the way east keeps to row 3 past him. +// Bug Catcher Greg (25, 4) faces south, sight 2, over the way east along +// row 5. Bug Catcher James (32, 6) faces east, sight 4, over the way down +// at x 35..36. The others are kept out of: Lass Janice (19, 9) sees west +// along row 9 to x 14, Lass Sally (30, 3) west along row 3 to x 25, and +// Youngster Calvin (29, 10) west along row 10. Each line is stepped into on +// its own, so no leg is cut; the trainer walks up to the player, who stays +// put facing the way they stepped. +StoryCheckpoint route3(){ + return row("00.11", "Pewter Center, then Route 3's trainers", at(PEWTER, 17, 26, SOUTH), {}, StartMenu::FULL, { + running(route("pewter_nurse", at(PEWTER_CENTER, 7, 4, NORTH), "in by the door below the player")), + heal("heal_pewter"), + // Up from row 9 west of Janice's line, which ends at x 14. + running(route("west_of_janice_line", at(ROUTE3, 13, 8), "out of Pewter by row 21, east into Route 3, short of Janice's line")), + running(legs("below_colton_line", {{EAST, 1}, {NORTH, 1}}, "to (14, 7), in the ledges' gap")), + step_into("colton_line", NORTH, "onto (14, 6), in Colton's line"), + battle("bug_catcher_colton", at(ROUTE3, 14, 6, NORTH), false, "his Bug Pokemon; Colton stops at (13, 6)"), + running(legs("past_ben", {{NORTH, 3}, {EAST, 4}, {SOUTH, 2}, {EAST, 6}}, "up to row 3 above Ben, down at x 18 east of his line, to (24, 5)")), + step_into("greg_line", EAST, "onto (25, 5), below Greg"), + battle("bug_catcher_greg", at(ROUTE3, 25, 5, EAST), false, "his Bug Pokemon; Greg is already next to the player"), + running(legs("above_james_line", {{EAST, 11}}, "along row 5 to the wall at (36, 5)")), + step_into("james_line", SOUTH, "onto (36, 6), at the end of James's line"), + battle("bug_catcher_james", at(ROUTE3, 36, 6, SOUTH), false, "his Bug Pokemon; James stops at (35, 6)"), + // Saved here, the trainers behind, so a failure on the way to Route 4 + // doesn't fight them again. + running(legs("save_spot", {{SOUTH, 2}}, "down to (36, 8), beside the wall east of it")), + }); +} + +// Route 3's north exit (x 68..75) joins Route 4 (offset 60) at x 8..15, up +// which column 12 leads to the Pokemon Center's door (12, 5). Lass Robin +// (40, 11) wanders up and down at x 40 across the way east, looking north +// and south: if she sees the player, the walk fights her (story_interrupts()). +StoryCheckpoint magikarp_salesman(){ + return row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), {}, StartMenu::FULL, { + running(route("route4_nurse", at(ROUTE4_CENTER, 7, 4, NORTH), "east over Route 3, off the grass, north into Route 4 and its Pokemon Center")), + heal("heal_route4"), + // The seam for a Gift RNG program: the save faces the salesman (1, 3). + running(route("before_salesman", at(ROUTE4_CENTER, 2, 3, WEST), "beside the Magikarp salesman, facing him")), }); } @@ -307,7 +356,7 @@ StoryCheckpoint running_shoes(){ const std::vector& story_checkpoints(){ static const std::vector checkpoints{ new_game(), after_sid_helper(), leave_house(), oaks_trigger(), to_starter_ball(), starter(), parcel(), pokedex(), shopping(), forest(), brock(), - running_shoes(), + running_shoes(), route3(), magikarp_salesman(), }; return checkpoints; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h index e93c6058f9..e1123642c5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h @@ -2,10 +2,11 @@ * * From: https://github.com/PokemonAutomation/ * - * FireRed from a new game to the Boulder Badge and the Running Shoes, as - * story checkpoints 00.01 to 00.10, each saved at its end. After 00.10 - * every route and leg runs. Taken from a by-hand run on - * console B (2026-11-01) and the pokefirered map scripts. + * FireRed from a new game to the Boulder Badge, the Running Shoes and Route + * 3 to the Magikarp salesman in Route 4's Pokemon Center, as story + * checkpoints 00.01 to 00.12, each saved at its end. After 00.10 every + * route and leg runs. Taken from a by-hand run on console B (2026-11-01) + * and the pokefirered map scripts. * */ @@ -21,7 +22,7 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ -// Checkpoints 00.01 to 00.10, in order, with 00.01b, the way back in after +// Checkpoints 00.01 to 00.12, in order, with 00.01b, the way back in after // SID Helper, after 00.01, and 00.04 split in two at Squirtle's ball for // Starter RNG: 00.04 and 00.04b. const std::vector& story_checkpoints(); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp index 5f2acf200c..d89620b953 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -211,8 +212,8 @@ UnitTestResult check_checkpoint(const StoryCheckpoint& checkpoint, StoryPose& po UnitTestResult test_routes_and_chain(){ const std::vector& checkpoints = story_checkpoints(); - if (checkpoints.size() != 12 || checkpoints[0].start){ - return "Expected 12 checkpoints, the first from a new game."; + if (checkpoints.size() != 14 || checkpoints[0].start){ + return "Expected 14 checkpoints, the first from a new game."; } StoryPose pose; for (size_t c = 0; c < checkpoints.size(); c++){ @@ -335,6 +336,7 @@ UnitTestResult test_pewter_aide_walk(){ // Every ROUTE and LEGS after the Running Shoes (00.10) runs; none before. UnitTestResult test_runs_after_shoes(){ bool has_shoes = false; + size_t runs = 0; for (const StoryCheckpoint& checkpoint : story_checkpoints()){ for (const StoryStep& step : checkpoint.steps){ if (step.kind != StoryStepKind::ROUTE && step.kind != StoryStepKind::LEGS){ @@ -344,11 +346,149 @@ UnitTestResult test_runs_after_shoes(){ if (move_speed(step) != expected){ return checkpoint.id + "/" + step.name + ": expected to " + move_speed_name(expected) + ", it would " + move_speed_name(move_speed(step)) + "."; } + runs += has_shoes ? 1 : 0; } has_shoes = has_shoes || checkpoint.id == "00.10"; } - if (!has_shoes){ - return "No checkpoint 00.10, where the player gets the Running Shoes."; + if (!has_shoes || runs == 0){ + return "No checkpoint 00.10, where the player gets the Running Shoes, or no route after it."; + } + return true; +} + +// Whether `trainer` on `map` sees `tile`: within their sight, any way they +// look, short of a wall. +bool sees(const MapData& map, const MapObject& trainer, const TilePos& tile){ + for (Direction look : trainer.trainer->looks){ + TilePos seen{trainer.x, trainer.y}; + for (int c = 0; c < trainer.trainer->sight && !is_wall(map, seen.next(look)); c++){ + seen = seen.next(look); + if (seen == tile){ + return true; + } + } + } + return false; +} + +// The trainer on `map` who would see the player on `tile`, other than the +// `beaten`. Wanderers are left out: no route can plan around one, and the +// walk fights one who sees the player (story_interrupts()). +const MapObject* trainer_seeing(const MapData& map, const TilePos& tile, const std::set& beaten){ + for (const MapObject& object : map.objects){ + if (object.trainer && !object.wander && !beaten.contains(&object) && sees(map, object, tile)){ + return &object; + } + } + return nullptr; +} + +// The tiles a ROUTE or LEGS `step` from `pose` walks onto, in order. +std::vector tiles_walked(const StoryPose& pose, const StoryStep& step){ + const MapPosition start{pose.map, pose.tile}; + std::optional> route; + if (step.kind == StoryStepKind::LEGS){ + route = fixed_route(story_maps(), start, step.legs); + }else if (step.target){ + const MapData* map = find_map_named(pose.map); + const std::optional facing = pose.facing ? pose.facing : map ? wall_beside(*map, pose.tile) : std::nullopt; + const StoryPose target = one_tile(*step.target); + route = plan_overworld_route(story_maps(), start, RouteGoal{{target.map, target.tile}, target.facing}, default_route_costs(), facing); + } + std::vector tiles; + MapPosition from = start; + for (const RouteStep& leg : route ? *route : std::vector{}){ + if (leg.action == RouteAction::WALK){ + const std::vector passed = leg_tiles(story_maps(), from, leg); + tiles.insert(tiles.end(), passed.begin() + 1, passed.end()); + } + from = leg.end; + tiles.push_back(from); + } + return tiles; +} + +// Why the player shouldn't walk onto `tile`, or empty: tall grass, a warp +// to a map not loaded (Route 4's ways into Mt. Moon), a trainer's line. +std::string tile_problem(const MapPosition& tile, const std::set& beaten){ + const MapData* map = find_map_named(tile.map); + if (map == nullptr){ + return "off the story's maps at " + to_string(tile); + } + if (is_tall_grass(*map, tile.point)){ + return "in the grass at " + to_string(tile); + } + const MapWarp* warp = warp_at(*map, tile.point); + if (warp != nullptr && !warp->to){ + return "onto a warp to a map not loaded at " + to_string(tile); + } + const MapObject* trainer = trainer_seeing(*map, tile.point, beaten); + if (trainer != nullptr){ + return "in the line of the trainer at " + to_string(TilePos{trainer->x, trainer->y}) + " at " + to_string(tile); + } + return ""; +} + +// Moves `pose` past `step`, as check_checkpoint() does. +UnitTestResult advance(StoryPose& pose, const StoryStep& step){ + if (step.kind == StoryStepKind::ROUTE){ + return check_route(pose, step); + } + if (step.kind == StoryStepKind::LEGS){ + return check_legs(pose, step); + } + if (step.kind == StoryStepKind::STEP){ + pose = StoryPose{pose.map, pose.tile.next(step.direction), step.direction}; + } + if (step.pose_after){ + pose = one_tile(*step.pose_after); + } + return true; +} + +// `checkpoint`'s routes and legs keep to tiles with no tile_problem(); a +// step into a trainer's line is followed by the battle, which beats them. +UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set& beaten){ + StoryPose pose = one_tile(*checkpoint.start); + for (size_t c = 0; c < checkpoint.steps.size(); c++){ + const StoryStep& step = checkpoint.steps[c]; + const std::string where = checkpoint.id + "/" + step.name + ": "; + for (const MapPosition& tile : step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS ? tiles_walked(pose, step) : std::vector{}){ + if (const std::string problem = tile_problem(tile, beaten); !problem.empty()){ + return where + problem; + } + } + const MapData* map = find_map_named(pose.map); + const MapObject* trainer = step.kind == StoryStepKind::STEP && map ? trainer_seeing(*map, pose.tile.next(step.direction), beaten) : nullptr; + if (trainer != nullptr && (c + 1 == checkpoint.steps.size() || checkpoint.steps[c + 1].kind != StoryStepKind::BATTLE)){ + return where + "steps into a trainer's line with no battle after it."; + } + if (trainer != nullptr){ + beaten.insert(trainer); + } + if (UnitTestResult result = advance(pose, step); result.result != UnitTestResult::PASSED){ + return checkpoint.id + " " + result.message; + } + } + return true; +} + +// From the Running Shoes on, routes and legs keep out of the grass, off +// warps to maps not loaded and out of trainers' lines, but for the three +// Route 3 trainers the table steps in front of to fight. +UnitTestResult test_trainer_lines(){ + std::set beaten; + bool has_shoes = false; + for (const StoryCheckpoint& checkpoint : story_checkpoints()){ + if (has_shoes){ + if (UnitTestResult result = check_lines(checkpoint, beaten); result.result != UnitTestResult::PASSED){ + return result; + } + } + has_shoes = has_shoes || checkpoint.id == "00.10"; + } + if (beaten.size() != 3){ + return "Expected to fight Colton, Greg and James on Route 3; the table fights " + std::to_string(beaten.size()) + " trainers."; } return true; } @@ -367,7 +507,11 @@ UnitTestResult test_save_places(){ ){ return "Wrong save card places."; } - if (!save_card_place("Route22").empty()){ + if (save_card_place("Route3") != "route-3" || save_card_place("Route4_PokemonCenter_1F") != "route-4"){ + return "Wrong save card places on Route 3 and 4."; + } + // Not Route 2's or Route 4's: a map name is a place's or a building's in it. + if (!save_card_place("Route22").empty() || !save_card_place("Route24").empty() || !save_card_place("Route2").empty()){ return "A place for a map the table doesn't use."; } return true; @@ -595,7 +739,7 @@ class Test_RunPlan : public UnitTest{ }; void add_tests_RunPlan(UnitTestDatabase& database){ - const std::string TODAY = "00.01 00.02 00.03 00.04 00.05 00.06 00.07 00.08 00.09 00.10"; + const std::string TODAY = "00.01 00.02 00.03 00.04 00.05 00.06 00.07 00.08 00.09 00.10 00.11 00.12"; const RunPlanScenario scenarios[] = { {.name = "handoffs off: the story as before", .first = "00.01", .expected_ids = TODAY}, {.name = "handoffs off: a range", .first = "00.02", .last = "00.04", .expected_ids = "00.02 00.03 00.04"}, @@ -748,6 +892,7 @@ void add_tests_AutoStory(UnitTestDatabase& database){ database.add("Pewter Gym walks", test_pewter_gym_walks); database.add("Pewter: to the aide's trigger", test_pewter_aide_walk); database.add("routes run after the Running Shoes", test_runs_after_shoes); + database.add("Route 3: only the planned trainers' lines", test_trainer_lines); database.add("intro: names, the rival's from the list", test_intro_names); database.add("options: RIGHT presses from the shown value", test_option_presses); } From 6176325a471f05a8703cd67a253260da96f6e7e1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 20:17:39 -0800 Subject: [PATCH 2/4] FRLG AutoStory: the table test checks crossings of a wanderer's sight The Route 3 lines test skipped wandering trainers. It now checks that each route or legs leg crossing a wanderer's sight is one the walk can handle: a single tile in any trainer's sight, off the grass, not to a warp, so the walk knows where the trainer stopped the player. A new test pins 00.12's crossing of Lass Robin's sight to Route 3 (40, 9). The test uses the maps' trainer_may_see() instead of its own copy, and magikarp_salesman()'s comment says how the walk now handles Robin. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 10 +- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 133 ++++++++++++++---- 2 files changed, 114 insertions(+), 29 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 552bc29f1b..ca37e434db 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -339,8 +339,14 @@ StoryCheckpoint route3(){ // Route 3's north exit (x 68..75) joins Route 4 (offset 60) at x 8..15, up // which column 12 leads to the Pokemon Center's door (12, 5). Lass Robin -// (40, 11) wanders up and down at x 40 across the way east, looking north -// and south: if she sees the player, the walk fights her (story_interrupts()). +// (40, 11) wanders up and down at x 40, looking north and south, sight 3. +// Rock above (y <= 8) and grass below (y >= 12) leave every way east +// across her column at y 9..11, so whether she sees the player depends on +// her facing. The walk watches for her after the leg along row 9, which +// crosses her sight on (40, 9) only; if she saw the player, her message +// goes to story_interrupts(), which fight her, and the route goes on from +// (40, 9) (run_route(); console B, 2026-11-02, she stopped the player +// there and spoke after the leg's short watch). StoryCheckpoint magikarp_salesman(){ return row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), {}, StartMenu::FULL, { running(route("route4_nurse", at(ROUTE4_CENTER, 7, 4, NORTH), "east over Route 3, off the grass, north into Route 4 and its Pokemon Center")), diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp index d89620b953..5ee907e621 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -8,6 +8,7 @@ * */ +#include #include #include #include @@ -19,6 +20,7 @@ #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_OptionMenuReader.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_AutoStoryActions.h" #include "PokemonFRLG_AutoStoryTable.h" @@ -356,35 +358,21 @@ UnitTestResult test_runs_after_shoes(){ return true; } -// Whether `trainer` on `map` sees `tile`: within their sight, any way they -// look, short of a wall. -bool sees(const MapData& map, const MapObject& trainer, const TilePos& tile){ - for (Direction look : trainer.trainer->looks){ - TilePos seen{trainer.x, trainer.y}; - for (int c = 0; c < trainer.trainer->sight && !is_wall(map, seen.next(look)); c++){ - seen = seen.next(look); - if (seen == tile){ - return true; - } - } - } - return false; -} - // The trainer on `map` who would see the player on `tile`, other than the -// `beaten`. Wanderers are left out: no route can plan around one, and the -// walk fights one who sees the player (story_interrupts()). +// `beaten`. Wanderers are left out: no route can plan around one. +// wanderer_problem() checks the walk can handle each crossing of their +// sight instead. const MapObject* trainer_seeing(const MapData& map, const TilePos& tile, const std::set& beaten){ for (const MapObject& object : map.objects){ - if (object.trainer && !object.wander && !beaten.contains(&object) && sees(map, object, tile)){ + if (object.trainer && !object.wander && !beaten.contains(&object) && trainer_may_see(map, object, tile)){ return &object; } } return nullptr; } -// The tiles a ROUTE or LEGS `step` from `pose` walks onto, in order. -std::vector tiles_walked(const StoryPose& pose, const StoryStep& step){ +// The legs a ROUTE or LEGS `step` from `pose` walks; none if it plans none. +std::vector legs_walked(const StoryPose& pose, const StoryStep& step){ const MapPosition start{pose.map, pose.tile}; std::optional> route; if (step.kind == StoryStepKind::LEGS){ @@ -395,9 +383,65 @@ std::vector tiles_walked(const StoryPose& pose, const StoryStep& st const StoryPose target = one_tile(*step.target); route = plan_overworld_route(story_maps(), start, RouteGoal{{target.map, target.tile}, target.facing}, default_route_costs(), facing); } + return route ? *route : std::vector{}; +} + +// Whether a wandering trainer may see `tile`. +bool wanderer_sees(const MapPosition& tile){ + const MapData* map = find_map_named(tile.map); + if (map == nullptr){ + return false; + } + for (const MapObject& object : map->objects){ + if (object.wander && trainer_may_see(*map, object, tile.point)){ + return true; + } + } + return false; +} + +// A leg that crosses a wandering trainer's sight, and the crossing as the +// walk sees it (trainer_crossing()). +struct WandererCrossing{ + RouteStep leg; + TrainerCrossing crossing; +}; + +// The legs of a ROUTE or LEGS `step` from `pose` that cross a wandering +// trainer's sight. +std::vector wanderer_crossings(const StoryPose& pose, const StoryStep& step){ + std::vector crossings; + MapPosition from{pose.map, pose.tile}; + for (const RouteStep& leg : legs_walked(pose, step)){ + const std::optional crossing = trainer_crossing(story_maps(), from, leg); + if (crossing && std::any_of(crossing->seen.begin(), crossing->seen.end(), wanderer_sees)){ + crossings.push_back({leg, *crossing}); + } + from = leg.end; + } + return crossings; +} + +// Why the walk couldn't handle a wandering trainer who sees the player on +// `step`, or empty. Whether they look the player's way can't be planned +// for, so the walk watches for them after each leg that crosses their +// sight and fights them; it then goes on from where they stopped the +// player, which it knows when the leg has one tile in any trainer's sight +// and is off the grass (run_route()). +std::string wanderer_problem(const StoryPose& pose, const StoryStep& step){ + for (const WandererCrossing& c : wanderer_crossings(pose, step)){ + if (c.crossing.seen.size() != 1 || c.leg.wild || c.leg.warps){ + return to_string(c.leg) + " crosses a wandering trainer's sight where the walk can't tell where they'd stop the player"; + } + } + return ""; +} + +// The tiles a ROUTE or LEGS `step` from `pose` walks onto, in order. +std::vector tiles_walked(const StoryPose& pose, const StoryStep& step){ std::vector tiles; - MapPosition from = start; - for (const RouteStep& leg : route ? *route : std::vector{}){ + MapPosition from{pose.map, pose.tile}; + for (const RouteStep& leg : legs_walked(pose, step)){ if (leg.action == RouteAction::WALK){ const std::vector passed = leg_tiles(story_maps(), from, leg); tiles.insert(tiles.end(), passed.begin() + 1, passed.end()); @@ -429,6 +473,20 @@ std::string tile_problem(const MapPosition& tile, const std::set& beaten){ + if (step.kind != StoryStepKind::ROUTE && step.kind != StoryStepKind::LEGS){ + return ""; + } + for (const MapPosition& tile : tiles_walked(pose, step)){ + if (std::string problem = tile_problem(tile, beaten); !problem.empty()){ + return problem; + } + } + return wanderer_problem(pose, step); +} + // Moves `pose` past `step`, as check_checkpoint() does. UnitTestResult advance(StoryPose& pose, const StoryStep& step){ if (step.kind == StoryStepKind::ROUTE){ @@ -453,10 +511,8 @@ UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set{}){ - if (const std::string problem = tile_problem(tile, beaten); !problem.empty()){ - return where + problem; - } + if (const std::string problem = walk_problem(pose, step, beaten); !problem.empty()){ + return where + problem; } const MapData* map = find_map_named(pose.map); const MapObject* trainer = step.kind == StoryStepKind::STEP && map ? trainer_seeing(*map, pose.tile.next(step.direction), beaten) : nullptr; @@ -475,7 +531,9 @@ UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set beaten; bool has_shoes = false; @@ -493,6 +551,26 @@ UnitTestResult test_trainer_lines(){ return true; } +// 00.12's walk east over Route 3 crosses Lass Robin's sight (she wanders +// x 40, y 10..12, looking north and south, sight 3) on (40, 9) only: if +// she sees the player, the walk goes on from there (console B, +// 2026-11-02, she stopped "east 11 (to wall)" there). +UnitTestResult test_robin_crossing(){ + const StoryCheckpoint* salesman = nullptr; + for (const StoryCheckpoint& checkpoint : story_checkpoints()){ + salesman = checkpoint.id == "00.12" ? &checkpoint : salesman; + } + if (salesman == nullptr || salesman->steps.empty() || salesman->steps[0].name != "route4_nurse"){ + return "Expected 00.12 to start with route4_nurse."; + } + const std::vector crossings = wanderer_crossings(one_tile(*salesman->start), salesman->steps[0]); + const std::vector expected{MapPosition{"Route3", {40, 9}}}; + if (crossings.size() != 1 || crossings[0].crossing.seen != expected || crossings[0].crossing.approach_tiles != 3){ + return "Expected 00.12/route4_nurse to cross Lass Robin's sight once, on Route3 (40, 9), with an approach of 3; it crosses wanderers' sight on " + std::to_string(crossings.size()) + " leg(s)."; + } + return true; +} + // Every checkpoint that starts from a save has a place the save card // names, so a save left elsewhere is caught before it runs; and the // places of the starts that matter differ. @@ -893,6 +971,7 @@ void add_tests_AutoStory(UnitTestDatabase& database){ database.add("Pewter: to the aide's trigger", test_pewter_aide_walk); database.add("routes run after the Running Shoes", test_runs_after_shoes); database.add("Route 3: only the planned trainers' lines", test_trainer_lines); + database.add("Route 3: Lass Robin stops the walk on (40, 9)", test_robin_crossing); database.add("intro: names, the rival's from the list", test_intro_names); database.add("options: RIGHT presses from the shown value", test_option_presses); } From 304c2ef008b09dafd97c9907b402c6aa9bbba37b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 20:19:20 -0800 Subject: [PATCH 3/4] FRLG AutoStory: shorter wanderer checks in the table test One helper, wanderer_stops(), gives for each leg crossing a wanderer's sight the tile the walk would place the stop on; the lines test and the Route 3 Lass Robin test both use it. Keeps the file well under the 1000-line limit for the PRs stacked on it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 92 +++++++------------ 1 file changed, 32 insertions(+), 60 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp index 5ee907e621..eb948eef22 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -359,9 +359,8 @@ UnitTestResult test_runs_after_shoes(){ } // The trainer on `map` who would see the player on `tile`, other than the -// `beaten`. Wanderers are left out: no route can plan around one. -// wanderer_problem() checks the walk can handle each crossing of their -// sight instead. +// `beaten`. Wanderers are left out: no route can plan around one (see +// wanderer_stops()). const MapObject* trainer_seeing(const MapData& map, const TilePos& tile, const std::set& beaten){ for (const MapObject& object : map.objects){ if (object.trainer && !object.wander && !beaten.contains(&object) && trainer_may_see(map, object, tile)){ @@ -400,41 +399,23 @@ bool wanderer_sees(const MapPosition& tile){ return false; } -// A leg that crosses a wandering trainer's sight, and the crossing as the -// walk sees it (trainer_crossing()). -struct WandererCrossing{ - RouteStep leg; - TrainerCrossing crossing; -}; - -// The legs of a ROUTE or LEGS `step` from `pose` that cross a wandering -// trainer's sight. -std::vector wanderer_crossings(const StoryPose& pose, const StoryStep& step){ - std::vector crossings; +// Whether they look the player's way can't be planned for, so the walk +// watches for a wanderer after each leg that crosses their sight, fights +// them, and goes on from where they stopped the player, if it can tell +// (run_route()'s trainer_stop(): the leg has one tile in any trainer's +// sight and is off the grass). For each leg of `step` from `pose` that +// crosses a wanderer's sight, that tile, or nothing if it can't tell. +std::vector> wanderer_stops(const StoryPose& pose, const StoryStep& step){ + std::vector> stops; MapPosition from{pose.map, pose.tile}; for (const RouteStep& leg : legs_walked(pose, step)){ - const std::optional crossing = trainer_crossing(story_maps(), from, leg); - if (crossing && std::any_of(crossing->seen.begin(), crossing->seen.end(), wanderer_sees)){ - crossings.push_back({leg, *crossing}); + const std::optional c = trainer_crossing(story_maps(), from, leg); + if (c && std::any_of(c->seen.begin(), c->seen.end(), wanderer_sees)){ + stops.push_back(c->seen.size() == 1 && !leg.wild && !leg.warps ? std::optional(c->seen[0]) : std::nullopt); } from = leg.end; } - return crossings; -} - -// Why the walk couldn't handle a wandering trainer who sees the player on -// `step`, or empty. Whether they look the player's way can't be planned -// for, so the walk watches for them after each leg that crosses their -// sight and fights them; it then goes on from where they stopped the -// player, which it knows when the leg has one tile in any trainer's sight -// and is off the grass (run_route()). -std::string wanderer_problem(const StoryPose& pose, const StoryStep& step){ - for (const WandererCrossing& c : wanderer_crossings(pose, step)){ - if (c.crossing.seen.size() != 1 || c.leg.wild || c.leg.warps){ - return to_string(c.leg) + " crosses a wandering trainer's sight where the walk can't tell where they'd stop the player"; - } - } - return ""; + return stops; } // The tiles a ROUTE or LEGS `step` from `pose` walks onto, in order. @@ -473,20 +454,6 @@ std::string tile_problem(const MapPosition& tile, const std::set& beaten){ - if (step.kind != StoryStepKind::ROUTE && step.kind != StoryStepKind::LEGS){ - return ""; - } - for (const MapPosition& tile : tiles_walked(pose, step)){ - if (std::string problem = tile_problem(tile, beaten); !problem.empty()){ - return problem; - } - } - return wanderer_problem(pose, step); -} - // Moves `pose` past `step`, as check_checkpoint() does. UnitTestResult advance(StoryPose& pose, const StoryStep& step){ if (step.kind == StoryStepKind::ROUTE){ @@ -511,8 +478,15 @@ UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set{}){ + if (const std::string problem = tile_problem(tile, beaten); !problem.empty()){ + return where + problem; + } + } + const std::vector> stops = walks ? wanderer_stops(pose, step) : std::vector>{}; + if (std::count(stops.begin(), stops.end(), std::nullopt) > 0){ + return where + "crosses a wandering trainer's sight where the walk can't tell where they'd stop the player."; } const MapData* map = find_map_named(pose.map); const MapObject* trainer = step.kind == StoryStepKind::STEP && map ? trainer_seeing(*map, pose.tile.next(step.direction), beaten) : nullptr; @@ -556,19 +530,17 @@ UnitTestResult test_trainer_lines(){ // she sees the player, the walk goes on from there (console B, // 2026-11-02, she stopped "east 11 (to wall)" there). UnitTestResult test_robin_crossing(){ - const StoryCheckpoint* salesman = nullptr; for (const StoryCheckpoint& checkpoint : story_checkpoints()){ - salesman = checkpoint.id == "00.12" ? &checkpoint : salesman; - } - if (salesman == nullptr || salesman->steps.empty() || salesman->steps[0].name != "route4_nurse"){ - return "Expected 00.12 to start with route4_nurse."; - } - const std::vector crossings = wanderer_crossings(one_tile(*salesman->start), salesman->steps[0]); - const std::vector expected{MapPosition{"Route3", {40, 9}}}; - if (crossings.size() != 1 || crossings[0].crossing.seen != expected || crossings[0].crossing.approach_tiles != 3){ - return "Expected 00.12/route4_nurse to cross Lass Robin's sight once, on Route3 (40, 9), with an approach of 3; it crosses wanderers' sight on " + std::to_string(crossings.size()) + " leg(s)."; + if (checkpoint.id != "00.12" || checkpoint.steps.empty() || checkpoint.steps[0].name != "route4_nurse"){ + continue; + } + const std::vector> expected{MapPosition{"Route3", {40, 9}}}; + if (wanderer_stops(one_tile(*checkpoint.start), checkpoint.steps[0]) != expected){ + return "Expected 00.12/route4_nurse to cross Lass Robin's sight once, the walk placing her stop on Route3 (40, 9)."; + } + return true; } - return true; + return "No step 00.12/route4_nurse."; } // Every checkpoint that starts from a save has a place the save card From 869a29e4898087c9650912a6a84031d52860eede Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 21:32:47 -0800 Subject: [PATCH 4/4] FRLG AutoStory: 00.11 and 00.12 start with the moves the lead has after Brock So a run from these rows answers its first move prompt (Bite) as the story plans, instead of taking the lead for a new game's Squirtle. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index cc4eb7e604..0e04ff867b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -326,7 +326,7 @@ StoryCheckpoint running_shoes(){ // its own, so no leg is cut; the trainer walks up to the player, who stays // put facing the way they stepped. StoryCheckpoint route3(){ - return row("00.11", "Pewter Center, then Route 3's trainers", at(PEWTER, 17, 26, SOUTH), {}, StartMenu::FULL, { + return row("00.11", "Pewter Center, then Route 3's trainers", at(PEWTER, 17, 26, SOUTH), after_brock(), StartMenu::FULL, { running(route("pewter_nurse", at(PEWTER_CENTER, 7, 4, NORTH), "in by the door below the player")), heal("heal_pewter"), // Up from row 9 west of Janice's line, which ends at x 14. @@ -357,7 +357,7 @@ StoryCheckpoint route3(){ // (40, 9) (run_route(); console B, 2026-11-02, she stopped the player // there and spoke after the leg's short watch). StoryCheckpoint magikarp_salesman(){ - return row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), {}, StartMenu::FULL, { + return row("00.12", "Route 3 and 4 to the Magikarp salesman", at(ROUTE3, 36, 8, SOUTH), after_brock(), StartMenu::FULL, { running(route("route4_nurse", at(ROUTE4_CENTER, 7, 4, NORTH), "east over Route 3, off the grass, north into Route 4 and its Pokemon Center")), heal("heal_route4"), // The seam for a Gift RNG program: the save faces the salesman (1, 3).