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 a57983b637..d86fdf9ec4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -55,7 +55,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 d5df6173eb..3b86177eb9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -312,15 +312,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 b716973374..ed59ef4e97 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 eb29f6d463..001a7b7998 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}); @@ -112,9 +114,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; } @@ -309,6 +311,59 @@ 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), 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. + 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, 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), 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). + running(route("before_salesman", at(ROUTE4_CENTER, 2, 3, WEST), "beside the Magikarp salesman, facing him")), }); } @@ -318,7 +373,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 d282fd40bd..de56612556 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -8,8 +8,10 @@ * */ +#include #include #include +#include #include #include #include @@ -19,6 +21,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" @@ -212,8 +215,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++){ @@ -336,6 +339,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){ @@ -345,15 +349,201 @@ 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; } +// 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 (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)){ + return &object; + } + } + return nullptr; +} + +// 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){ + 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); + } + 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; +} + +// 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 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 stops; +} + +// 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{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()); + } + 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 + ": "; + const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS; + for (const MapPosition& tile : walks ? tiles_walked(pose, step) : std::vector{}){ + 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; + 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; and where they +// cross a wandering trainer's sight, the walk can place the player if the +// trainer stops them. +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; +} + +// 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(){ + for (const StoryCheckpoint& checkpoint : story_checkpoints()){ + 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 "No step 00.12/route4_nurse."; +} + // 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. @@ -368,7 +558,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; @@ -596,7 +790,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"}, @@ -749,6 +943,8 @@ 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("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); }