From d0bb65b8f82c338d950298b32bff1a17160e51c8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 01:08:18 -0800 Subject: [PATCH 01/10] FRLG maps: Cerulean's Mart and Gym, Route 25 and Bill's Sea Cottage Generated with devtools/maps/frlg_maps.py, with the Mart clerk's and Misty's front tiles as points. Cerulean City's Gym and Mart doors and Route 24's east edge now lead somewhere. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Overworld/Maps/CeruleanCity.json | 10 +- .../Overworld/Maps/CeruleanCity_Gym.json | 116 ++++++++++ .../Overworld/Maps/CeruleanCity_Mart.json | 86 ++++++++ .../PokemonFRLG/Overworld/Maps/Route24.json | 5 + .../PokemonFRLG/Overworld/Maps/Route25.json | 200 ++++++++++++++++++ .../Overworld/Maps/Route25_SeaCottage.json | 66 ++++++ .../Overworld/PokemonFRLG_OverworldMaps.cpp | 5 + .../Overworld/PokemonFRLG_OverworldMaps.h | 7 +- .../PokemonFRLG_OverworldMaps_Tests.cpp | 37 ++++ devtools/maps/frlg_maps.py | 4 + 10 files changed, 531 insertions(+), 5 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Gym.json create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Mart.json create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25.json create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25_SeaCottage.json diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity.json index 10e47a673a..51f0256ec9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity.json @@ -91,6 +91,8 @@ ], "points": { "warp_3": [22, 19], + "warp_4": [31, 21], + "warp_6": [29, 28], "warp_7": [1, 12] }, "warps": [ @@ -119,7 +121,9 @@ { "x": 31, "y": 21, - "enter": "walk_into" + "enter": "walk_into", + "to_map": "CeruleanCity_Gym", + "to_point": "warp_1" }, { "x": 13, @@ -129,7 +133,9 @@ { "x": 29, "y": 28, - "enter": "walk_into" + "enter": "walk_into", + "to_map": "CeruleanCity_Mart", + "to_point": "warp_1" }, { "x": 1, diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Gym.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Gym.json new file mode 100644 index 0000000000..519944eecb --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Gym.json @@ -0,0 +1,116 @@ +{ + "name": "CeruleanCity_Gym", + "source": { + "decomp_file": "data/maps/CeruleanCity_Gym/map.json", + "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", + "date": "2026-10-31" + }, + "allow_running": false, + "tiles": [ + "#################", + "#####w#w.w#w#####", + "#################", + "#...............#", + "#......www......#", + "#.wwwwwwwwwwwww.#", + "#.wwwww...wwwww.#", + "#.ww.........ww.#", + "#.wwwww...ww.ww.#", + "#.wwwwwwwwww.ww.#", + "#.ww.........ww.#", + "#.ww.wwww.wwwww.#", + "#.ww.wwww.wwwww.#", + "#.ww......wwwww.#", + "#.wwwwwww.wwwww.#", + "#.wwwwwww.wwwww.#", + "#.wwww.....wwww.#", + "#.wwwwS...Swwww.#", + "#...............#", + "#################" + ], + "elevation": [ + "00000000000000000", + "00000000000000000", + "00000000000000000", + "03333333333333330", + "03333331113333330", + "03111111111111130", + "03111113331111130", + "03113333333331130", + "03111113331131130", + "03111111111131130", + "03113333333331130", + "03113111131111130", + "03113111130111130", + "03113333331111130", + "03111111131111130", + "03111111131111130", + "03111133333111130", + "03111103330111130", + "03333333333333330", + "00000000000000000" + ], + "points": { + "misty_front": [8, 7], + "warp_1": [8, 18] + }, + "warps": [ + { + "x": 7, + "y": 18, + "enter": "walk_into", + "to_map": "CeruleanCity", + "to_point": "warp_4" + }, + { + "x": 8, + "y": 18, + "enter": "walk_south", + "to_map": "CeruleanCity", + "to_point": "warp_4" + }, + { + "x": 9, + "y": 18, + "enter": "walk_into", + "to_map": "CeruleanCity", + "to_point": "warp_4" + } + ], + "objects": [ + { + "name": "swimmer_m_water", + "kind": "npc", + "x": 10, + "y": 12, + "facing": "west", + "trainer": { + "sight": 1 + } + }, + { + "name": "picnicker", + "kind": "npc", + "x": 4, + "y": 7, + "facing": "east", + "trainer": { + "sight": 4 + } + }, + { + "name": "misty", + "kind": "npc", + "x": 8, + "y": 6 + }, + { + "name": "gym_guy", + "kind": "npc", + "x": 7, + "y": 16 + } + ], + "flag_sets": {}, + "connections": [] +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Mart.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Mart.json new file mode 100644 index 0000000000..db4c9defee --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/CeruleanCity_Mart.json @@ -0,0 +1,86 @@ +{ + "name": "CeruleanCity_Mart", + "source": { + "decomp_file": "data/maps/CeruleanCity_Mart/map.json", + "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", + "date": "2026-10-31" + }, + "allow_running": false, + "tiles": [ + "###########", + "###########", + "##.#......#", + "...#.......", + "####...##.#", + ".......##.#", + ".##....##.#", + "...........", + "###########" + ], + "elevation": [ + "00000000000", + "00000000000", + "00303333330", + "33003333333", + "00003330030", + "33333330030", + "30033330030", + "33333333333", + "00000000000" + ], + "points": { + "clerk_front": [4, 3], + "warp_1": [4, 7] + }, + "warps": [ + { + "x": 3, + "y": 7, + "enter": "walk_into", + "to_map": "CeruleanCity", + "to_point": "warp_6" + }, + { + "x": 4, + "y": 7, + "enter": "walk_south", + "to_map": "CeruleanCity", + "to_point": "warp_6" + }, + { + "x": 5, + "y": 7, + "enter": "walk_into", + "to_map": "CeruleanCity", + "to_point": "warp_6" + } + ], + "objects": [ + { + "name": "clerk", + "kind": "npc", + "x": 2, + "y": 3 + }, + { + "name": "woman_1", + "kind": "npc", + "x": 9, + "y": 2, + "wander": { + "x0": 5, + "y0": 2, + "x1": 10, + "y1": 2 + } + }, + { + "name": "youngster", + "kind": "npc", + "x": 1, + "y": 7 + } + ], + "flag_sets": {}, + "connections": [] +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route24.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route24.json index 2c3bd7ee03..c8402d903e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route24.json +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route24.json @@ -173,6 +173,11 @@ "side": "south", "to_map": "CeruleanCity", "offset": -12 + }, + { + "side": "east", + "to_map": "Route25", + "offset": 0 } ] } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25.json new file mode 100644 index 0000000000..29dcb02843 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25.json @@ -0,0 +1,200 @@ +{ + "name": "Route25", + "source": { + "decomp_file": "data/maps/Route25/map.json", + "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", + "date": "2026-10-31" + }, + "tiles": [ + "########################################################################", + "########################################################################", + "############...#...#.....#.....#..........##############################", + ".~~~~~~~~......#...#.....#.....#..........####################wwww######", + ".~~~~~~~~................###.#............######S##D##########wwww######", + "................#.#.#....#...#.....#.........................#wwww######", + "vvvvvvvvvv##..##....#.##.#.........#.........................#wwwww#####", + "................#...#....#......##...........................#wwwwwww###", + ".............#..##..#...........#............................#wwwwwwwwww", + ".............#......#.#.........#..........wwwwww.....wwwwww.#wwwwwwwwww", + "###########################################wwwwww.....wwwwww.#wwwwwwwwww", + "...........................................wwwwww.....wwwwww.#wwwwwwwwww", + "#########################################....................#wwwwwwwwww", + "##############################################################wwwwwwwwww", + "#########################################wwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", + "#########################################wwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", + "#########################################wwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", + "#########################################wwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", + "#########################################wwwwwwwwwwwwwwwwwwwwwwwwwwwwwww", + "#########################################wwwwwwwwwwwwwwwwwwwwwwwwwwwwwww" + ], + "elevation": [ + "000000000000000000000000000000000000000000000000000000000000000000000000", + "000000000000000000000000000000000000000000000000000000000000000000000000", + "000000000000333033303333303333303333333333000000000000000000000000000000", + "333333333333333033303333303333303333333333000000000000000000001111000000", + "333333333333333333333333300030333333333333000000000000000000001001000000", + "333333333333333303030333303330333330333333333333333333333333301001000000", + "000000000000330033330300303333333330333333333333333333333333301001100000", + "333333333333333303330333303333330033333333333333333333333333301000111000", + "333333333333303300330333333333330333333333333333333333333333301000001111", + "333333333333303333330303333333330333333333311111133333111111301000000000", + "000000000000000000000000000000000000000000011111133333111111301000000000", + "333333333333333333333333333333333333333333311111133333111111301000000000", + "000000000000000000000000000000000000000003333333333333333333301000000000", + "000000000000000000000000000000000000000000000000000000000000001000000000", + "000000000000000000000000000000000000000001111111111111111111111000000000", + "000000000000000000000000000000000000000001100000000000000000000000000000", + "000000000000000000000000000000000000000001000000000000000000000000000000", + "000000000000000000000000000000000000000001000000000000000000000000000000", + "000000000000000000000000000000000000000001000000000000000000000000000000", + "000000000000000000000000000000000000000001000000000000000000000000000000" + ], + "points": { + "warp_0": [51, 4] + }, + "warps": [ + { + "x": 51, + "y": 4, + "enter": "walk_into", + "to_map": "Route25_SeaCottage", + "to_point": "warp_1" + } + ], + "objects": [ + { + "name": "hiker", + "kind": "npc", + "x": 11, + "y": 4, + "facing": "east", + "trainer": { + "sight": 4 + } + }, + { + "name": "youngster", + "kind": "npc", + "x": 18, + "y": 2, + "facing": "south", + "trainer": { + "sight": 2 + } + }, + { + "name": "hiker", + "kind": "npc", + "x": 17, + "y": 7, + "facing": "east", + "trainer": { + "sight": 2 + } + }, + { + "name": "youngster", + "kind": "npc", + "x": 22, + "y": 4, + "trainer": { + "sight": 2, + "looks": [ + "north", + "east", + "south", + "west" + ] + } + }, + { + "name": "picnicker", + "kind": "npc", + "x": 22, + "y": 8, + "facing": "east", + "trainer": { + "sight": 2 + } + }, + { + "name": "hiker", + "kind": "npc", + "x": 27, + "y": 9, + "facing": "north", + "trainer": { + "sight": 3 + } + }, + { + "name": "camper", + "kind": "npc", + "x": 28, + "y": 4, + "facing": "south", + "trainer": { + "sight": 3 + } + }, + { + "name": "youngster", + "kind": "npc", + "x": 36, + "y": 4, + "trainer": { + "sight": 2, + "looks": [ + "north", + "east", + "south", + "west" + ] + } + }, + { + "name": "lass", + "kind": "npc", + "x": 42, + "y": 5, + "facing": "south", + "trainer": { + "sight": 3 + } + }, + { + "name": "item_ball", + "kind": "item", + "x": 26, + "y": 2, + "hide_flag": "FLAG_HIDE_ROUTE25_TM43" + }, + { + "name": "cut_tree", + "kind": "npc", + "x": 30, + "y": 3, + "hide_flag": "FLAG_TEMP_12" + }, + { + "name": "beauty", + "kind": "npc", + "x": 49, + "y": 10 + }, + { + "name": "man", + "kind": "npc", + "x": 49, + "y": 11 + } + ], + "flag_sets": {}, + "connections": [ + { + "side": "west", + "to_map": "Route24", + "offset": 0 + } + ] +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25_SeaCottage.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25_SeaCottage.json new file mode 100644 index 0000000000..0ec5259806 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/Maps/Route25_SeaCottage.json @@ -0,0 +1,66 @@ +{ + "name": "Route25_SeaCottage", + "source": { + "decomp_file": "data/maps/Route25_SeaCottage/map.json", + "revision": "ce6406d8c2eaf46d543c8a2606de0c3cb1951982", + "date": "2026-10-31" + }, + "allow_running": false, + "tiles": [ + "###############", + "###############", + "#.##########..#", + "###D######D#.##", + "..............#", + "....##........#", + "..............#", + "#.............#", + "#..............", + "###..........##", + "###############" + ], + "points": { + "warp_1": [7, 9] + }, + "warps": [ + { + "x": 6, + "y": 9, + "enter": "walk_into", + "to_map": "Route25", + "to_point": "warp_0" + }, + { + "x": 7, + "y": 9, + "enter": "walk_south", + "to_map": "Route25", + "to_point": "warp_0" + }, + { + "x": 8, + "y": 9, + "enter": "walk_into", + "to_map": "Route25", + "to_point": "warp_0" + } + ], + "objects": [ + { + "name": "bill", + "kind": "npc", + "x": 7, + "y": 5, + "hide_flag": "FLAG_HIDE_BILL_HUMAN_SEA_COTTAGE" + }, + { + "name": "clefairy", + "kind": "npc", + "x": 10, + "y": 6, + "hide_flag": "FLAG_HIDE_BILL_CLEFAIRY" + } + ], + "flag_sets": {}, + "connections": [] +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp index 96954b03cc..c859069ceb 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.cpp @@ -49,6 +49,11 @@ const char* const MAP_NAMES[] = { "CeruleanCity_PokemonCenter_1F", "CeruleanCity", "Route24", + // Cerulean City's Mart and Gym, and Route 25 to Bill's Sea Cottage. + "CeruleanCity_Mart", + "CeruleanCity_Gym", + "Route25", + "Route25_SeaCottage", "CeruleanCave_1F", "CeruleanCave_2F", "CeruleanCave_B1F", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h index cff0668eb5..76a83fa212 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps.h @@ -4,9 +4,10 @@ * * The FRLG maps the overworld routes know about so far: every map from a * new game in Pallet Town through Viridian City, Route 2 and Viridian - * Forest to Brock's gym in Pewter City; and Cerulean City, its Pokemon - * Center, Route 24 to its north, and the three floors of Cerulean Cave, - * where Mewtwo waits on B1F. + * Forest to Brock's gym in Pewter City, Route 3, Mt. Moon and Route 4; + * and Cerulean City, its Pokemon Center, Mart and Gym, Route 24 (Nugget + * Bridge) and Route 25 to Bill's Sea Cottage, and the three floors of + * Cerulean Cave, where Mewtwo waits on B1F. * * Each map is a JSON file, Maps/.json next to this file, generated by * devtools/maps/frlg_maps.py and built into the program by CMake diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp index 8a89186186..98aba1f87f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldMaps_Tests.cpp @@ -285,6 +285,42 @@ UnitTestResult test_mt_moon_maps(){ return true; } +// Whether a route plans from Cerulean's Pokemon Center, at its nurse, to +// `goal` on `maps`. +bool plans_from_cerulean_nurse(const std::vector& maps, const RouteGoal& goal){ + const MapPosition nurse{"CeruleanCity_PokemonCenter_1F", map_point(maps, "CeruleanCity_PokemonCenter_1F", "nurse_front")}; + return plan_overworld_route(maps, nurse, goal, default_route_costs(), Direction::NORTH).has_value(); +} + +// Cerulean City's Mart and Gym and Route 25's Sea Cottage load with the +// points the story walks to, and join the city: from its Pokemon Center +// a route plans to the Mart's clerk, to Misty, and north over Nugget +// Bridge (Route 24) and east along Route 25 into Bill's Sea Cottage. +UnitTestResult test_cerulean_maps(){ + const std::vector maps = overworld_maps(); + for (const char* name : {"CeruleanCity_Mart", "CeruleanCity_Gym", "Route25", "Route25_SeaCottage"}){ + if (built_in(maps, name) == nullptr){ + return std::string("Missing map ") + name + "."; + } + } + const MapPosition clerk{"CeruleanCity_Mart", map_point(maps, "CeruleanCity_Mart", "clerk_front")}; + const MapPosition misty{"CeruleanCity_Gym", map_point(maps, "CeruleanCity_Gym", "misty_front")}; + if (clerk.point != TilePos{4, 3} || misty.point != TilePos{8, 7}){ + return "Unexpected clerk_front or misty_front."; + } + if (!plans_from_cerulean_nurse(maps, RouteGoal{clerk, Direction::WEST})){ + return "No route from Cerulean's Pokemon Center to the Mart's clerk."; + } + if (!plans_from_cerulean_nurse(maps, RouteGoal{misty, Direction::NORTH})){ + return "No route from Cerulean's Pokemon Center to Misty."; + } + const MapPosition bill_front{"Route25_SeaCottage", {7, 7}}; + if (!plans_from_cerulean_nurse(maps, RouteGoal{bill_front, Direction::NORTH})){ + return "No route from Cerulean's Pokemon Center over Nugget Bridge and Route 25 into Bill's Sea Cottage."; + } + return true; +} + // A planned step walks where its map doesn't let the player run, and only // there: out of Pewter's Pokemon Center to the Gym. UnitTestResult test_walk_only_indoors(){ @@ -485,6 +521,7 @@ void add_tests_OverworldMaps(UnitTestDatabase& database){ database.add("not JSON", test_not_json); database.add("Route 3, Route 4 and its Pokemon Center", test_route3_route4_maps); database.add("Mt. Moon joins Route 4's cave mouths", test_mt_moon_maps); + database.add("Cerulean's Mart and Gym, Route 25 and Bill's Sea Cottage", test_cerulean_maps); database.add("walks where running isn't allowed", test_walk_only_indoors); database.add("Route 4's Pokemon Center escalator", test_route4_center_escalator); database.add("trainer sight", test_trainer_sight); diff --git a/devtools/maps/frlg_maps.py b/devtools/maps/frlg_maps.py index a5fa31f145..a6bca3a140 100644 --- a/devtools/maps/frlg_maps.py +++ b/devtools/maps/frlg_maps.py @@ -82,6 +82,10 @@ 'Route4_PokemonCenter_1F': {'nurse_front': (7, 4), 'salesman_front': (2, 3)}, # The tile in front of the PC. 'CeruleanCity_PokemonCenter_1F': {'pc': (11, 2), 'nurse_front': (7, 4)}, + # The clerk (2, 3) is across the counter to the west. + 'CeruleanCity_Mart': {'clerk_front': (4, 3)}, + # Below Misty (8, 6). + 'CeruleanCity_Gym': {'misty_front': (8, 7)}, # Where the player stands after entering from Cerulean City. 'CeruleanCave_1F': {'entrance': (33, 21)}, # Mewtwo's tile. Its battle starts by talking to it from the south. From db245ba6cdb2c09ebbdfac0d5ab19a469c690d4f Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 01:08:19 -0800 Subject: [PATCH 02/10] FRLG AutoStory: 00.14 to Cerulean, 00.15 the rival, 00.16 Nugget Bridge 00.14 runs east over Route 4 to Cerulean's Pokemon Center and heals. 00.15 steps onto the rival's middle trigger (23, 6), fights him and heals again. 00.16 steps beside each of Nugget Bridge's five trainers in turn, then onto the Rocket's trigger (11, 15) for the Nugget and his battle. Cerulean City gets its save card place. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Menus/PokemonFRLG_SaveCardReader.cpp | 1 + .../Menus/PokemonFRLG_SaveCardReader.h | 8 +- .../AutoStory/PokemonFRLG_AutoStoryEvents.cpp | 1 + .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 9 +- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 73 +++++++++++ .../AutoStory/PokemonFRLG_AutoStoryTable.h | 11 +- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 122 ++++++++++++++++-- 7 files changed, 203 insertions(+), 22 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp index 0793744ff9..9ec73b734a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.cpp @@ -70,6 +70,7 @@ class PlaceNameMatcher : public OCR::SmallDictionaryMatcher{ {"route-2", "Route 2"}, {"route-3", "Route 3"}, {"route-4", "Route 4"}, + {"cerulean-city", "Cerulean City"}, }){ 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 e7f81aa521..94514f94cf 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_SaveCardReader.h @@ -4,9 +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 Route 4, in - * English only so far: in another language nothing is read. Route 3 and - * Route 4 have no test capture yet. + * BADGES and TIME). It knows the places from a new game to Route 4, and + * Cerulean City, in English only so far: in another language nothing is + * read. Route 3, Route 4 and Cerulean City have no test capture yet. * */ @@ -31,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" to "route-4"; empty if it +// "viridian-forest", "pewter-city", "route-1" to "route-4", "cerulean-city"; 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_AutoStoryEvents.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp index 180216b90e..dd43a3f41e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp @@ -285,6 +285,7 @@ std::string save_card_place(const std::string& map){ {"PewterCity", "pewter-city"}, {"Route3", "route-3"}, {"Route4", "route-4"}, + {"CeruleanCity", "cerulean-city"}, }; for (const auto& [prefix, place] : PLACES){ if (map == prefix || map.rfind(prefix + "_", 0) == 0){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp index 6036ac956f..c0408ad46e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -111,10 +111,11 @@ 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 00.11 00.12 00.12c 00.13"; + 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 00.12c 00.13 00.14 00.15 00.16"; 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"}, + {.name = "handoffs off: Mt. Moon to Nugget Bridge", .first = "00.13", .last = "00.16", .expected_ids = "00.13 00.14 00.15 00.16"}, { .name = "SID Helper: 00.01 stops on the keyboard", .first = "00.01", @@ -163,8 +164,8 @@ void add_tests_GiftRunPlan(UnitTestDatabase& database){ .expected_ids = "00.11 00.12", .expected_stop = StoryHandoff::GIFT_RNG, }, - {.name = "Gift RNG off: 00.12b is passed over", .first = "00.11", .expected_ids = "00.11 00.12 00.12c 00.13"}, - {.name = "Gift RNG off: 00.12b runs when started at", .first = "00.12b", .expected_ids = "00.12b 00.12c 00.13"}, + {.name = "Gift RNG off: 00.12b is passed over", .first = "00.11", .expected_ids = "00.11 00.12 00.12c 00.13 00.14 00.15 00.16"}, + {.name = "Gift RNG off: 00.12b runs when started at", .first = "00.12b", .expected_ids = "00.12b 00.12c 00.13 00.14 00.15 00.16"}, { .name = "Gift RNG: started at 00.12b, from the save beside the salesman", .first = "00.12b", @@ -179,7 +180,7 @@ void add_tests_GiftRunPlan(UnitTestDatabase& database){ .expected_ids = "", .expected_stop = StoryHandoff::GIFT_RNG, }, - {.name = "Starter RNG alone: 00.12b runs when started at", .first = "00.12b", .handoffs = {.starter_rng = true}, .expected_ids = "00.12b 00.12c 00.13"}, + {.name = "Starter RNG alone: 00.12b runs when started at", .first = "00.12b", .handoffs = {.starter_rng = true}, .expected_ids = "00.12b 00.12c 00.13 00.14 00.15 00.16"}, { .name = "Starter RNG first, with both on", .first = "00.03", diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 91d4112d97..19af25923b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -37,6 +37,9 @@ const char* const ROUTE4 = "Route4"; const char* const ROUTE4_CENTER = "Route4_PokemonCenter_1F"; const char* const MT_MOON_1F = "MtMoon_1F"; const char* const MT_MOON_B2F = "MtMoon_B2F"; +const char* const CERULEAN = "CeruleanCity"; +const char* const CERULEAN_CENTER = "CeruleanCity_PokemonCenter_1F"; +const char* const ROUTE24 = "Route24"; Pokemon::Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ return to_pose(StoryPose{map, {x, y}, facing}); @@ -468,6 +471,75 @@ StoryCheckpoint mt_moon(){ }); } +// Route 4 east of Mt. Moon (Route4 map.json) joins Cerulean City at its +// east end (x 107; Route 4's row 3 is the city's row 13). Lass Crissy +// (75, 3) faces east, sight 4, over row 3 from x 76 to 79; the way east +// keeps out of her line and off the grass south of the road (75..87, +// 11..17). +StoryCheckpoint route4_to_cerulean(){ + return row("00.14", "Route 4 east to Cerulean's Pokemon Center", at(ROUTE4, 32, 6, SOUTH), after_brock(), StartMenu::FULL, { + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "east over Route 4's ledges into Cerulean City and its Pokemon Center")), + heal("heal_cerulean"), + }); +} + +// The rival north of Cerulean (CeruleanCity scripts.inc): the only way +// north to Nugget Bridge is columns 22..24, and a step onto row 6 there +// (CeruleanCity_EventScript_RivalTrigger*, VAR_MAP_SCENE_CERULEAN_CITY_RIVAL +// 0) brings him down to the player. From the middle, (23, 6), he stops +// at (23, 5); after the battle he walks round the player and off south, +// and the player, turning to watch him, ends facing south. His FAME +// CHECKER's text is read to the overworld with the battle's. For a player +// who took Squirtle he has Pidgeotto Lv17, Abra Lv16, Rattata Lv15 and +// Bulbasaur Lv18 (sParty_RivalCeruleanBulbasaur), whose Vine Whip and +// Sleep Powder are the danger. +StoryCheckpoint cerulean_rival(){ + return row("00.15", "Cerulean: the rival north of the city", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + running(route("below_rival_trigger", at(CERULEAN, 23, 7, NORTH), "out of the Center, north up to (23, 7), short of the trigger row 6")), + step_into("rival_trigger", NORTH, "onto (23, 6), the middle trigger: \"Yo! !\""), + battle("rival_cerulean", at(CERULEAN, 23, 6, SOUTH), false, "Pidgeotto, Abra, Rattata and Bulbasaur; then the FAME CHECKER"), + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "back to the Center before Nugget Bridge")), + heal("heal_cerulean"), + }); +} + +// Nugget Bridge (Route24 map.json) is x 10..12 from row 32 north to row +// 16, the city's columns 22..24 (offset 12). Its five trainers stand on +// it facing across, sight 2, so each line crosses the middle column 11 +// on the trainer's row: Bug Catcher Cale (12, 31), Lass Ali (10, 28), +// Youngster Timmy (12, 25), Lass Reli (10, 22) and Camper Ethan (12, 19). +// Each is fought on the step onto their row, beside them: they don't +// walk, and the player keeps facing north. Two tiles separate the rows, +// walked as one short leg. Then the Rocket (12, 15): the step onto +// (11, 15) (Route24_EventScript_RocketTriggerRight, VAR_MAP_SCENE_ROUTE24 +// 0) gives the NUGGET, "join Team Rocket", and his battle; he only turns, +// and the player turns east to him. Camper Shane (5, 21) looks north +// from the west bank, off the way. +StoryCheckpoint nugget_bridge(){ + return row("00.16", "Nugget Bridge: five trainers and the Rocket", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + running(route("below_cale_line", at(ROUTE24, 11, 32, NORTH), "out of the Center, north past the rival's row onto the bridge, below Cale's row")), + step_into("cale_line", NORTH, "onto (11, 31), beside Cale"), + battle("bug_catcher_cale", at(ROUTE24, 11, 31, NORTH), false, "his Bug Pokemon"), + running(legs("below_ali_line", {{NORTH, 2}}, "up to (11, 29)")), + step_into("ali_line", NORTH, "onto (11, 28), beside Ali"), + battle("lass_ali", at(ROUTE24, 11, 28, NORTH), false, "her Pokemon"), + running(legs("below_timmy_line", {{NORTH, 2}}, "up to (11, 26)")), + step_into("timmy_line", NORTH, "onto (11, 25), beside Timmy"), + battle("youngster_timmy", at(ROUTE24, 11, 25, NORTH), false, "his Pokemon"), + running(legs("below_reli_line", {{NORTH, 2}}, "up to (11, 23)")), + step_into("reli_line", NORTH, "onto (11, 22), beside Reli"), + battle("lass_reli", at(ROUTE24, 11, 22, NORTH), false, "her Pokemon"), + running(legs("below_ethan_line", {{NORTH, 2}}, "up to (11, 20)")), + step_into("ethan_line", NORTH, "onto (11, 19), beside Ethan"), + battle("camper_ethan", at(ROUTE24, 11, 19, NORTH), false, "his Pokemon"), + // A route, not legs: the nurse may have been needed. + ready("ready_for_rocket", at(CERULEAN_CENTER, 7, 4, NORTH), "items for a status or low HP; the nurse only if they can't"), + running(route("below_rocket_trigger", at(ROUTE24, 11, 16, NORTH), "up the bridge to below the Rocket's trigger (11, 15)")), + step_into("rocket_trigger", NORTH, "onto (11, 15), beside the Rocket"), + battle("rocket_grunt", at(ROUTE24, 11, 15, EAST), false, "the NUGGET, then Ekans and Zubat, Lv15"), + }); +} + } @@ -475,6 +547,7 @@ 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(), route3(), magikarp_salesman(), after_gift_rng(), magikarp_to_box(), mt_moon(), + route4_to_cerulean(), cerulean_rival(), nugget_bridge(), }; 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 d7421214f1..5106373639 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h @@ -3,10 +3,11 @@ * From: https://github.com/PokemonAutomation/ * * FireRed from a new game to the Boulder Badge, the Running Shoes, Route - * 3 to the Magikarp salesman in Route 4's Pokemon Center, and through Mt. - * Moon to Route 4 east of it, as story checkpoints 00.01 to 00.13, 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. + * 3 to the Magikarp salesman in Route 4's Pokemon Center, through Mt. + * Moon to Route 4 east of it, and on to Cerulean City, its rival and + * Nugget Bridge, as story checkpoints 00.01 to 00.16, 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. * */ @@ -22,7 +23,7 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ -// Checkpoints 00.01 to 00.13, in order, with 00.01b, the way back in after +// Checkpoints 00.01 to 00.16, in order, with 00.01b, the way back in after // SID Helper, after 00.01, 00.04 split in two at Squirtle's ball for // Starter RNG: 00.04 and 00.04b, 00.12b, the way back in after Gift RNG, // after 00.12, and 00.12c, the Magikarp put in BOX 1, after it. 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 cd22061a59..1314070f87 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -215,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() != 17 || checkpoints[0].start){ - return "Expected 17 checkpoints, the first from a new game."; + if (checkpoints.size() != 20 || checkpoints[0].start){ + return "Expected 20 checkpoints, the first from a new game."; } StoryPose pose; for (size_t c = 0; c < checkpoints.size(); c++){ @@ -553,11 +553,11 @@ UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set beaten; bool has_shoes = false; @@ -569,8 +569,8 @@ UnitTestResult test_trainer_lines(){ } has_shoes = has_shoes || checkpoint.id == "00.10"; } - if (beaten.size() != 4){ - return "Expected to fight Colton, Greg and James on Route 3 and the Grunt at (12, 20) in Mt. Moon; the table fights " + if (beaten.size() != 9){ + return "Expected to fight Colton, Greg and James on Route 3, the Grunt at (12, 20) in Mt. Moon and Nugget Bridge's five; the table fights " + std::to_string(beaten.size()) + " trainers."; } return true; @@ -594,6 +594,105 @@ UnitTestResult test_robin_crossing(){ return "No step 00.12/route4_nurse."; } +// A scene the table starts on a step onto its trigger, and the walks of +// its checkpoint before that step, which keep off the trigger's tiles: +// the maps have no triggers, so a route over one would start the scene +// part-way. +struct TriggerCase{ + std::string checkpoint; + std::string step; + std::string map; + // The trigger's tiles: row `y` from `x0` to `x1`. + int y = 0; + int x0 = 0; + int x1 = 0; + // The tile the step lands on. + int onto_x = 0; +}; + +const StoryCheckpoint* find_checkpoint(const std::string& id){ + for (const StoryCheckpoint& checkpoint : story_checkpoints()){ + if (checkpoint.id == id){ + return &checkpoint; + } + } + return nullptr; +} + +UnitTestResult check_trigger(const TriggerCase& c){ + const StoryCheckpoint* checkpoint = find_checkpoint(c.checkpoint); + if (checkpoint == nullptr){ + return "No checkpoint " + c.checkpoint + "."; + } + const MapPosition onto{c.map, {c.onto_x, c.y}}; + StoryPose pose = one_tile(*checkpoint->start); + for (const StoryStep& step : checkpoint->steps){ + const std::string where = c.checkpoint + "/" + step.name + ": "; + if (step.name == c.step && step.kind == StoryStepKind::STEP && onto == MapPosition{pose.map, pose.tile.next(step.direction)}){ + return true; + } + if (step.name == c.step){ + return where + "expected a step onto " + to_string(onto) + ", from " + describe(pose) + "."; + } + const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS; + for (const MapPosition& tile : walks ? tiles_walked(pose, step) : std::vector{}){ + if (tile.map == c.map && tile.point.y == c.y && tile.point.x >= c.x0 && tile.point.x <= c.x1){ + return where + "walks over the trigger at " + to_string(tile) + "."; + } + } + if (UnitTestResult result = advance(pose, step); result.result != UnitTestResult::PASSED){ + return c.checkpoint + " " + result.message; + } + } + return "No step " + c.checkpoint + "/" + c.step + "."; +} + +// 00.15 meets the rival on the middle of his three trigger tiles, row 6 +// at x 22..24 north of Cerulean (VAR_MAP_SCENE_CERULEAN_CITY_RIVAL 0), +// and 00.16 the Rocket on the east one of his two, (10..11, 15) on Nugget +// Bridge (VAR_MAP_SCENE_ROUTE24 0), each from the tile below. +UnitTestResult test_cerulean_triggers(){ + const TriggerCase CASES[] = { + {.checkpoint = "00.15", .step = "rival_trigger", .map = "CeruleanCity", .y = 6, .x0 = 22, .x1 = 24, .onto_x = 23}, + {.checkpoint = "00.16", .step = "rocket_trigger", .map = "Route24", .y = 15, .x0 = 10, .x1 = 11, .onto_x = 11}, + }; + for (const TriggerCase& c : CASES){ + if (UnitTestResult result = check_trigger(c); result.result != UnitTestResult::PASSED){ + return result; + } + } + return true; +} + +// 00.16 steps beside Nugget Bridge's five trainers from the south, one +// step at a time, each battle right after its step: Cale, Ali, Timmy, +// Reli and Ethan, by where they stand. +UnitTestResult test_nugget_bridge_order(){ + const StoryCheckpoint* checkpoint = find_checkpoint("00.16"); + if (checkpoint == nullptr){ + return "No checkpoint 00.16."; + } + const std::vector& steps = checkpoint->steps; + std::string met; + StoryPose pose = one_tile(*checkpoint->start); + for (size_t c = 0; c < steps.size(); c++){ + const MapData* map = find_map_named(pose.map); + const MapObject* trainer = steps[c].kind == StoryStepKind::STEP && map ? trainer_seeing(*map, pose.tile.next(steps[c].direction), {}) : nullptr; + if (trainer != nullptr && (c + 1 == steps.size() || steps[c + 1].kind != StoryStepKind::BATTLE)){ + return "00.16/" + steps[c].name + ": no battle right after the step."; + } + met += trainer ? " " + to_string(TilePos{trainer->x, trainer->y}) : ""; + if (UnitTestResult result = advance(pose, steps[c]); result.result != UnitTestResult::PASSED){ + return "00.16 " + result.message; + } + } + const std::string expected = " (12, 31) (10, 28) (12, 25) (10, 22) (12, 19)"; + if (met != expected){ + return "00.16: expected the five bridge trainers south to north," + expected + "; met" + met + "."; + } + 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. @@ -611,6 +710,9 @@ UnitTestResult test_save_places(){ 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."; } + if (save_card_place("CeruleanCity_PokemonCenter_1F") != "cerulean-city"){ + return "Wrong save card place in Cerulean City."; + } // 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."; @@ -847,6 +949,8 @@ void add_tests_AutoStory(UnitTestDatabase& database){ 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("Cerulean: the rival's and the Rocket's triggers", test_cerulean_triggers); + database.add("Nugget Bridge: its five trainers, south to north", test_nugget_bridge_order); 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); database.add("00.12c: the Magikarp's deposit is skipped without Gift RNG", test_magikarp_deposit_skippable); From d5589b78116172ffd471d7049c16714c5123fa97 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 01:08:20 -0800 Subject: [PATCH 03/10] FRLG OverworldSim: Route 4 to Cerulean, and Nugget Bridge's trainers Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_OverworldSim_Tests.cpp | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index 39dea30c7b..6f5b0b2f16 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -548,6 +548,59 @@ UnitTestResult test_mt_moon_way_out(){ return true; } +// Story row 00.14: from Route 4 east of Mt. Moon over its ledges into +// Cerulean City and its Pokemon Center, one door, out of Lass Crissy's +// line (75, 3). +UnitTestResult test_route4_to_cerulean(){ + const SimCase c{.start = at("Route4", 32, 6), .facing = Direction::SOUTH, .goal = at("CeruleanCity_PokemonCenter_1F", 7, 4)}; + const SimRun now = run_case(c); + if (!arrived(now, c.goal) || now.state.warps != 1 || !now.state.trainers_beaten.empty()){ + return "Expected Cerulean's Pokemon Center reached by its door, no trainer met:" + describe(now); + } + return true; +} + +// Story rows 00.15 and 00.16: out of Cerulean's Pokemon Center by its mat +// to below the rival's trigger row, and on north across the city's edge +// onto Nugget Bridge, below Cale's row, meeting no one. +UnitTestResult test_cerulean_to_bridge(){ + const MapPosition nurse = at("CeruleanCity_PokemonCenter_1F", 7, 4); + for (const MapPosition& goal : {at("CeruleanCity", 23, 7), at("Route24", 11, 32)}){ + const SimCase c{.start = nurse, .facing = Direction::NORTH, .goal = goal}; + const SimRun now = run_case(c); + if (!arrived(now, c.goal) || now.state.warps != 1 || !now.state.trainers_beaten.empty()){ + return "Expected " + to_string(goal) + " reached by the Center's mat, no trainer met:" + describe(now); + } + } + return true; +} + +// Story row 00.16 up Nugget Bridge: each short leg ends on a trainer's +// row, in their sight beside them, and they fight there without walking: +// Cale, Ali, Timmy, Reli and Ethan, and the legs go on to below the +// Rocket (11, 16). +UnitTestResult test_nugget_bridge_legs(){ + SimCase c{.start = at("Route24", 11, 32), .facing = Direction::NORTH, .goal = at("Route24", 11, 16)}; + c.route = std::vector{ + leg(Direction::NORTH, 1, at("Route24", 11, 31), false), + leg(Direction::NORTH, 2, at("Route24", 11, 29), false), + leg(Direction::NORTH, 1, at("Route24", 11, 28), false), + leg(Direction::NORTH, 2, at("Route24", 11, 26), false), + leg(Direction::NORTH, 1, at("Route24", 11, 25), false), + leg(Direction::NORTH, 2, at("Route24", 11, 23), false), + leg(Direction::NORTH, 1, at("Route24", 11, 22), false), + leg(Direction::NORTH, 2, at("Route24", 11, 20), false), + leg(Direction::NORTH, 1, at("Route24", 11, 19), false), + leg(Direction::NORTH, 3, c.goal, false), + }; + const SimRun now = run_case(c); + const std::vector expected{"bug_catcher", "lass", "youngster", "lass", "camper"}; + if (!arrived(now, c.goal) || now.state.trainers_beaten != expected){ + return "Expected the five bridge trainers fought in turn and the bridge walked:" + describe(now); + } + return true; +} + } @@ -568,6 +621,9 @@ void add_tests_OverworldSim(UnitTestDatabase& database){ database.add("Mt. Moon: a short open leg before the Grunt's line", test_mt_moon_short_leg); database.add("Mt. Moon: a late wild battle in the cave", test_mt_moon_late_battle); database.add("Mt. Moon: after the HELIX, out to Route 4", test_mt_moon_way_out); + database.add("Route 4 east to Cerulean's Pokemon Center", test_route4_to_cerulean); + database.add("Cerulean: the Center to the rival's row and Nugget Bridge", test_cerulean_to_bridge); + database.add("Nugget Bridge: a trainer at each short leg's end", test_nugget_bridge_legs); } From b515f7539fc1247913a81a33a01a9f59060edaaf Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 01:26:39 -0800 Subject: [PATCH 04/10] FRLG AutoStory 00.14: hop Route 4's ledge to reach Cerulean by land Route 4's upper road meets Cerulean City only across water, and planned routes never hop ledges, so 00.14 had no route. It now walks to (93, 8), takes one step south down the ledge onto (93, 10), and routes on from there. The sim checks the hop and both walks; the Nugget Bridge sim case now steps onto each trainer's row from the tile below, as the table does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_OverworldSim_Tests.cpp | 91 +++++++++++-------- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 24 ++++- 2 files changed, 71 insertions(+), 44 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index 670cced333..d57ceb521e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -645,18 +645,31 @@ UnitTestResult test_mt_moon_way_out(){ return true; } -// Story row 00.14: from Route 4 east of Mt. Moon over its ledges into -// Cerulean City and its Pokemon Center, one door, out of Lass Crissy's -// line (75, 3). -UnitTestResult test_route4_to_cerulean(){ - const SimCase c{.start = at("Route4", 32, 6), .facing = Direction::SOUTH, .goal = at("CeruleanCity_PokemonCenter_1F", 7, 4)}; - const SimRun now = run_case(c); - if (!arrived(now, c.goal) || now.state.warps != 1 || !now.state.trainers_beaten.empty()){ - return "Expected Cerulean's Pokemon Center reached by its door, no trainer met:" + describe(now); - } - return true; -} - +// Story row 00.14: from Route 4 east of Mt. Moon along the upper road to +// above the ledge row 9, out of Lass Crissy's line (75, 3); one step +// south hops the ledge onto the lower road two tiles down; from there +// into Cerulean City and its Pokemon Center by one door. No route plans +// past the ledge: the upper road reaches the city only by water. +UnitTestResult test_route4_to_cerulean(){ + const SimCase upper{.start = at("Route4", 32, 6), .facing = Direction::SOUTH, .goal = at("Route4", 93, 8)}; + const SimRun walk = run_case(upper); + if (!arrived(walk, upper.goal) || !walk.state.trainers_beaten.empty()){ + return "Expected the upper road walked to above the ledge, no trainer met:" + describe(walk); + } + OverworldSim sim(maps(), at("Route4", 93, 8), Direction::SOUTH); + sim.tap(Direction::SOUTH); + sim.watch({SimWatcher::MESSAGE}, std::chrono::milliseconds(2000)); + if (!(sim.state().position == at("Route4", 93, 10))){ + return "Expected the step south to hop the ledge onto (93, 10), not " + to_string(sim.state().position) + "."; + } + const SimCase lower{.start = at("Route4", 93, 10), .facing = Direction::SOUTH, .goal = at("CeruleanCity_PokemonCenter_1F", 7, 4)}; + const SimRun now = run_case(lower); + if (!arrived(now, lower.goal) || now.state.warps != 1 || !now.state.trainers_beaten.empty()){ + return "Expected Cerulean's Pokemon Center reached by its door, no trainer met:" + describe(now); + } + return true; +} + // Story rows 00.15 and 00.16: out of Cerulean's Pokemon Center by its mat // to below the rival's trigger row, and on north across the city's edge // onto Nugget Bridge, below Cale's row, meeting no one. @@ -672,31 +685,31 @@ UnitTestResult test_cerulean_to_bridge(){ return true; } -// Story row 00.16 up Nugget Bridge: each short leg ends on a trainer's -// row, in their sight beside them, and they fight there without walking: -// Cale, Ali, Timmy, Reli and Ethan, and the legs go on to below the -// Rocket (11, 16). -UnitTestResult test_nugget_bridge_legs(){ - SimCase c{.start = at("Route24", 11, 32), .facing = Direction::NORTH, .goal = at("Route24", 11, 16)}; - c.route = std::vector{ - leg(Direction::NORTH, 1, at("Route24", 11, 31), false), - leg(Direction::NORTH, 2, at("Route24", 11, 29), false), - leg(Direction::NORTH, 1, at("Route24", 11, 28), false), - leg(Direction::NORTH, 2, at("Route24", 11, 26), false), - leg(Direction::NORTH, 1, at("Route24", 11, 25), false), - leg(Direction::NORTH, 2, at("Route24", 11, 23), false), - leg(Direction::NORTH, 1, at("Route24", 11, 22), false), - leg(Direction::NORTH, 2, at("Route24", 11, 20), false), - leg(Direction::NORTH, 1, at("Route24", 11, 19), false), - leg(Direction::NORTH, 3, c.goal, false), - }; - const SimRun now = run_case(c); - const std::vector expected{"bug_catcher", "lass", "youngster", "lass", "camper"}; - if (!arrived(now, c.goal) || now.state.trainers_beaten != expected){ - return "Expected the five bridge trainers fought in turn and the bridge walked:" + describe(now); - } - return true; -} +// Story row 00.16 up Nugget Bridge: the short legs between the five +// trainers' rows meet no one; and the step from the tile below each row +// onto it is in that trainer's sight, beside them, so they fight there +// without walking: Cale, Ali, Timmy, Reli and Ethan. +UnitTestResult test_nugget_bridge_legs(){ + SimCase between{.start = at("Route24", 11, 31), .facing = Direction::NORTH, .goal = at("Route24", 11, 29)}; + between.route = std::vector{leg(Direction::NORTH, 2, between.goal, false)}; + const SimRun walk = run_case(between); + if (!arrived(walk, between.goal) || !walk.state.trainers_beaten.empty()){ + return "Expected the leg between two rows walked, no trainer met:" + describe(walk); + } + const struct{ + int row; + const char* trainer; + } LINES[] = {{31, "bug_catcher"}, {28, "lass"}, {25, "youngster"}, {22, "lass"}, {19, "camper"}}; + for (const auto& line : LINES){ + SimCase c{.start = at("Route24", 11, line.row + 1), .facing = Direction::NORTH, .goal = at("Route24", 11, line.row)}; + c.route = std::vector{leg(Direction::NORTH, 1, c.goal, false)}; + const SimRun now = run_case(c); + if (!arrived(now, c.goal) || now.state.trainers_beaten != std::vector{line.trainer}){ + return std::format("Expected the {} on row {} fought beside the player:", line.trainer, line.row) + describe(now); + } + } + return true; +} } @@ -720,9 +733,9 @@ void add_tests_OverworldSim(UnitTestDatabase& database){ database.add("Mt. Moon: a late wild battle in the cave", test_mt_moon_late_battle); database.add("Mt. Moon: after the HELIX, out to Route 4", test_mt_moon_way_out); database.add("Mt. Moon: a battle on the leg onto 1F's ladder", test_mt_moon_battle_on_ladder_leg); - database.add("Route 4 east to Cerulean's Pokemon Center", test_route4_to_cerulean); + database.add("Route 4 east, down its ledge, to Cerulean's Pokemon Center", test_route4_to_cerulean); database.add("Cerulean: the Center to the rival's row and Nugget Bridge", test_cerulean_to_bridge); - database.add("Nugget Bridge: a trainer at each short leg's end", test_nugget_bridge_legs); + database.add("Nugget Bridge: each trainer met on the step onto their row", test_nugget_bridge_legs); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 19af25923b..0cbf75b1a8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -97,6 +97,16 @@ StoryStep step_into(std::string name, Direction direction, std::string why){ return step; } +// A step the way a ledge faces, from the tile before it: the player hops +// it and lands on `landing`, two tiles on. Planned routes never hop a +// ledge (they hold into walls, and a held ledge hops), so where the only +// way on is down a ledge the table takes it as this one step. +StoryStep hop(std::string name, Direction direction, Pokemon::Pose landing, std::string why){ + StoryStep step = step_into(std::move(name), direction, std::move(why)); + step.pose_after = std::move(landing); + return step; +} + StoryStep answer(std::string name, bool yes, std::string why){ StoryStep step = make(StoryStepKind::ANSWER, std::move(name), std::move(why)); step.yes = yes; @@ -472,13 +482,17 @@ StoryCheckpoint mt_moon(){ } // Route 4 east of Mt. Moon (Route4 map.json) joins Cerulean City at its -// east end (x 107; Route 4's row 3 is the city's row 13). Lass Crissy -// (75, 3) faces east, sight 4, over row 3 from x 76 to 79; the way east -// keeps out of her line and off the grass south of the road (75..87, -// 11..17). +// east end (x 107; Route 4's row 3 is the city's row 13). Its upper road +// (rows 2..8) reaches the city only by water: the way on by land is down +// the ledge row 9 at x 90..96 to the lower road (rows 10..12), which +// joins the city's rows 20..22. Lass Crissy (75, 3) faces east, sight 4, +// over row 3 from x 76 to 79; the way east keeps out of her line and off +// the grass south of the road (75..87, 11..17). StoryCheckpoint route4_to_cerulean(){ return row("00.14", "Route 4 east to Cerulean's Pokemon Center", at(ROUTE4, 32, 6, SOUTH), after_brock(), StartMenu::FULL, { - running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "east over Route 4's ledges into Cerulean City and its Pokemon Center")), + running(route("above_ledge", at(ROUTE4, 93, 8, SOUTH), "east along Route 4's upper road to above the ledge row 9")), + hop("ledge_hop", SOUTH, at(ROUTE4, 93, 10, SOUTH), "down the ledge (93, 9) to the lower road"), + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "east into Cerulean City and its Pokemon Center")), heal("heal_cerulean"), }); } From 2e35eda3635fde56cc09c1e3b708b2b096c422ed Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 01:44:09 -0800 Subject: [PATCH 05/10] FRLG AutoStory 00.15: Potions at Cerulean's Mart and a GRIND to Lv22 before the rival 00.15 now buys 5 Potions at the Mart, then grinds the lead to Lv22 on Route 4's grass east of Mt. Moon, healing at Cerulean's Center, before stepping onto the rival's trigger. With Bite chosen against his party (#120), Lv22 is where a lone Wartortle wins. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 67 ++++++++++++++++++- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 21 +++++- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 9 ++- 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp index c0408ad46e..3758cc2dea 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -3,8 +3,8 @@ * From: https://github.com/PokemonAutomation/ * * Which rows of the story table a run takes (plan_story_run()) with the - * RNG programs' handoffs on and off, and the rows the handoffs split or - * join. + * RNG programs' handoffs on and off, the rows the handoffs split or + * join, and what 00.15 runs before the rival. * */ @@ -14,6 +14,10 @@ #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTest.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h" +#include "PokemonFRLG_AutoStoryEvents.h" +#include "PokemonFRLG_AutoStoryGrind.h" +#include "PokemonFRLG_AutoStoryMart.h" #include "PokemonFRLG_AutoStoryTable.h" namespace PokemonAutomation{ @@ -249,6 +253,64 @@ UnitTestResult test_after_sid_helper_row(){ return true; } +// The index of the first step of `steps` from `from` that `matches`, or +// steps.size(). +size_t find_step(const std::vector& steps, size_t from, const std::function& matches){ + for (size_t c = from; c < steps.size(); c++){ + if (matches(steps[c])){ + return c; + } + } + return steps.size(); +} + +// Whether a route plans from `from` to `to` (ending facing as `to` does). +bool plans(const Pokemon::Pose& from, const Pokemon::Pose& to){ + const std::optional start = story_pose(from); + const std::optional goal = story_pose(to); + if (!start || !goal){ + return false; + } + return plan_overworld_route( + story_maps(), MapPosition{start->map, start->tile}, RouteGoal{MapPosition{goal->map, goal->tile}, goal->facing}, + default_route_costs(), start->facing.value_or(Direction::NORTH) + ).has_value(); +} + +// 00.15, before the rival: Potions from Cerulean's Mart (a loaded save's +// bag may hold too few for the wild battles a GRIND fights), then a GRIND +// on Route 4's grass east of Mt. Moon to Lv22, where Bite beats his party +// (#211), healing at Cerulean's Center; its trip there plans both ways. +UnitTestResult test_cerulean_mart_and_grind(){ + const std::vector& steps = story_checkpoints()[row_index("00.15")].steps; + const size_t mart = find_step(steps, 0, [](const StoryStep& step){ + const std::optional plan = parse_mart_variant(step.variant); + return plan && plan->map == "CeruleanCity_Mart" && !plan->lines.empty() && plan->lines[0].item == "potion"; + }); + const size_t grind = find_step(steps, mart, [](const StoryStep& step){ + const std::optional plan = parse_grind_variant(step.variant); + return plan && plan->member == 0 && plan->level >= 22; + }); + const size_t rival = find_step(steps, 0, [](const StoryStep& step){ return step.name == "rival_trigger"; }); + if (mart == steps.size() || grind == steps.size() || rival == steps.size() || grind > rival){ + return "00.15: expected a Cerulean Mart step buying Potions, then a GRIND of the lead to Lv22, before the rival's trigger."; + } + const std::optional clerk = mart > 0 && steps[mart - 1].target ? story_pose(*steps[mart - 1].target) : std::nullopt; + if (!clerk || clerk->map != "CeruleanCity_Mart"){ + return "00.15: expected the Mart step right after a route to the clerk."; + } + if (grind == 0 || !steps[grind - 1].target || !steps[grind].target){ + return "00.15: expected the GRIND right after a route to its patch, with the nurse as its target."; + } + const Pokemon::Pose& patch = *steps[grind - 1].target; + const Pokemon::Pose& nurse = *steps[grind].target; + const std::optional nurse_pose = story_pose(nurse); + if (!nurse_pose || nurse_pose->map != "CeruleanCity_PokemonCenter_1F" || !plans(patch, nurse) || !plans(nurse, patch)){ + return "00.15: expected the GRIND's trips to Cerulean's nurse and back to plan."; + } + return true; +} + } @@ -257,6 +319,7 @@ void add_tests_AutoStoryRunPlan(UnitTestDatabase& database){ add_tests_GiftRunPlan(database); database.add("00.01b and 00.12b: back in after SID Helper and Gift RNG", test_after_sid_helper_row); database.add("00.04 and 00.04b: one checkpoint with Starter RNG off", test_starter_rows_joined); + database.add("00.15: Potions at Cerulean's Mart and a GRIND before the rival", test_cerulean_mart_and_grind); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 0cbf75b1a8..ae23d4ffff 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -6,6 +6,8 @@ #include "PokemonFRLG_AutoStoryCapture.h" #include "PokemonFRLG_AutoStoryChoice.h" +#include "PokemonFRLG_AutoStoryGrind.h" +#include "PokemonFRLG_AutoStoryMart.h" #include "PokemonFRLG_AutoStoryPcStorage.h" #include "PokemonFRLG_AutoStoryTable.h" @@ -39,6 +41,7 @@ const char* const MT_MOON_1F = "MtMoon_1F"; const char* const MT_MOON_B2F = "MtMoon_B2F"; const char* const CERULEAN = "CeruleanCity"; const char* const CERULEAN_CENTER = "CeruleanCity_PokemonCenter_1F"; +const char* const CERULEAN_MART = "CeruleanCity_Mart"; const char* const ROUTE24 = "Route24"; Pokemon::Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ @@ -507,9 +510,23 @@ StoryCheckpoint route4_to_cerulean(){ // who took Squirtle he has Pidgeotto Lv17, Abra Lv16, Rattata Lv15 and // Bulbasaur Lv18 (sParty_RivalCeruleanBulbasaur), whose Vine Whip and // Sleep Powder are the danger. +// +// Before him: 5 Potions at the Mart (CeruleanCity_Mart_Items: POTION is +// row 3, 300 each; Mt. Moon's trainers leave well over the floor), then a +// GRIND to Lv22 on Route 4's grass east of Mt. Moon (75..86, 11..17; +// Spearow, Rattata, Ekans and Mankey around Lv10, about 80 EXP each), +// healing at the Center. With Bite chosen against his party, Lv22 is +// where a lone Wartortle beats it (#211's estimate: about 70 HP of his +// damage against its 61, which the Potions cover; at Lv18 about 100). StoryCheckpoint cerulean_rival(){ - return row("00.15", "Cerulean: the rival north of the city", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { - running(route("below_rival_trigger", at(CERULEAN, 23, 7, NORTH), "out of the Center, north up to (23, 7), short of the trigger row 6")), + const MartPlan potions{.map = CERULEAN_MART, .lines = {{.item = "potion", .quantity = 5}}, .budget = 1500, .money_floor = 1500}; + const GrindPlan to_lv22{.member = 0, .level = 22, .max_encounters = 100, .pace = WEST, .heal_every = 0}; + return row("00.15", "Cerulean: Potions, a GRIND to Lv22, then the rival north of the city", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + running(route("mart_clerk", at(CERULEAN_MART, 4, 3, WEST), "out of the Center to the Mart's clerk")), + mart_step("buy_potions", "5 Potions: the GRIND fights wild battles only with 2 or more", potions), + running(route("grind_patch", at(ROUTE4, 86, 16, WEST), "west out of the city along Route 4's lower road onto its grass's east edge")), + grind_step("grind_to_lv22", "wild battles until the lead is Lv22, the nurse when hurt or low on PP", to_lv22, at(CERULEAN_CENTER, 7, 4, NORTH)), + running(route("below_rival_trigger", at(CERULEAN, 23, 7, NORTH), "back east into the city, north up to (23, 7), short of the trigger row 6")), step_into("rival_trigger", NORTH, "onto (23, 6), the middle trigger: \"Yo! !\""), battle("rival_cerulean", at(CERULEAN, 23, 6, SOUTH), false, "Pidgeotto, Abra, Rattata and Bulbasaur; then the FAME CHECKER"), running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "back to the Center before Nugget Bridge")), 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 1314070f87..c4f1646795 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -23,6 +23,7 @@ #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRecovery.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h" #include "PokemonFRLG_AutoStoryActions.h" +#include "PokemonFRLG_AutoStoryGrind.h" #include "PokemonFRLG_AutoStoryPcStorage.h" #include "PokemonFRLG_AutoStoryTable.h" @@ -524,8 +525,12 @@ UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set{}){ - if (const std::string problem = tile_problem(tile, beaten); !problem.empty()){ + const StoryPose end = to_patch ? one_tile(*step.target) : StoryPose{}; + const bool patch = to_patch && tile.map == end.map && tile.point == end.tile; + if (const std::string problem = tile_problem(tile, beaten); !problem.empty() && !(patch && problem.starts_with("in the grass"))){ return where + problem; } } @@ -552,7 +557,7 @@ UnitTestResult check_lines(const StoryCheckpoint& checkpoint, std::set Date: Tue, 3 Nov 2026 04:46:18 -0800 Subject: [PATCH 06/10] FRLG 00.14: into Cerulean by legs to walls, not an open leg on On console B (2026-11-03) the open leg east from Route 4 into Cerulean City ended a tile short, on (6, 20). No hold to a wall in the city's west end finds the player there, so the walk went on a column off: the hold south stopped at the fence on (6, 23), and the door leg missed the Pokemon Center. The map and the Route 4 connection match the decomp and the leg screenshots; the walk was the layer that failed. The entry now ends on (9, 20), and legs to walls follow: up column 9 (or 8) to row 12, west to (8, 12), east to the policeman, south, west to (25, 17), down the ledge (25, 18), then a short route to the door. A new test walks the row on the sim with the entry leg whole or a tile short; the old row fails it with "no fade", as on the console. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStorySimulation_Tests.cpp | 81 +++++++++++++++++++ .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 16 +++- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp index 53b2da73ad..23353dc736 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include "Common/Cpp/Exceptions.h" @@ -24,6 +25,7 @@ #include "Pokemon/AutoStory/Pokemon_StorySegments.h" #include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.h" +#include "VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim.h" #include "PokemonFRLG_AutoStoryTable.h" namespace PokemonAutomation{ @@ -378,6 +380,84 @@ UnitTestResult test_gives_up_after_attempts(){ return true; } +// The route the program walks for `step`, a ROUTE or LEGS, from `at`: +// planned, or the legs laid out. +std::optional> walked_route(const StoryPose& at, const StoryStep& step){ + const MapPosition start{at.map, at.tile}; + if (step.kind == StoryStepKind::LEGS){ + return fixed_route(story_maps(), start, step.legs); + } + const std::optional target = story_pose(*step.target); + const RouteGoal goal{{target->map, target->tile}, target->facing}; + return plan_overworld_route(story_maps(), start, goal, default_route_costs(), at.facing); +} + +// Walks `step` of 00.14 on `sim` from `at`, where the table says the +// player stands, and moves `at` to where it says the step ends. Returns +// the failure, or empty. +std::string walk_on_sim(OverworldSim& sim, StoryPose& at, const StoryStep& step){ + const std::string where = "00.14/" + step.name + ": "; + // A walk that ends on an open leg may leave the player short of it. + bool open_end = false; + if (step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS){ + const std::optional> route = walked_route(at, step); + if (!route || route->empty()){ + return where + "no route from " + to_string(MapPosition{at.map, at.tile}) + "."; + } + const RoutePlanning planning{.maps = &story_maps(), .start = MapPosition{at.map, at.tile}, .label = step.name}; + const std::string failure = OverworldSimWalker(sim).run(*route, planning); + if (!failure.empty()){ + return where + failure + " On " + to_string(sim.state().position) + "."; + } + const RouteStep& last = route->back(); + open_end = last.action == RouteAction::WALK && !last.to_wall && !last.warps; + at = StoryPose{last.end.map, last.end.point, last.facing_after}; + }else if (step.kind == StoryStepKind::STEP){ + if (sim.state().facing != step.direction){ + sim.tap(step.direction); + } + sim.tap(step.direction); + sim.watch({SimWatcher::MESSAGE}, std::chrono::milliseconds(2000)); + at = *story_pose(*step.pose_after); + } + if (!open_end && !(sim.state().position == MapPosition{at.map, at.tile})){ + return where + "expected " + to_string(MapPosition{at.map, at.tile}) + ", but on " + to_string(sim.state().position) + "."; + } + return ""; +} + +// Console B, 2026-11-03 (row 00.14): the open leg east from Route 4 +// into Cerulean City ended a tile short, on (6, 20), with nothing on +// screen. No hold to a wall finds the player there, and the walk went on +// a column off, the hold south stopped by the fence on (6, 23), to a +// wall below a row of houses, not the Pokemon Center's door. From the +// foot of Route 4's ledge, the row's walks on the sim reach the nurse +// with that leg whole or a tile short. +UnitTestResult test_cerulean_entry_short(){ + const std::vector& rows = story_checkpoints(); + const auto row = std::find_if(rows.begin(), rows.end(), [](const StoryCheckpoint& data){ return data.id == "00.14"; }); + if (row == rows.end()){ + return "No checkpoint 00.14."; + } + const auto hop = std::find_if(row->steps.begin(), row->steps.end(), [](const StoryStep& step){ return step.name == "ledge_hop"; }); + if (hop == row->steps.end() || !hop->pose_after){ + return "00.14: no ledge_hop."; + } + for (int short_by : {0, 1}){ + StoryPose at = *story_pose(*hop->pose_after); + SimScript script; + script.short_holds[1] = short_by; + OverworldSim sim(story_maps(), MapPosition{at.map, at.tile}, *at.facing, script); + for (auto step = hop + 1; step != row->steps.end() && step->kind != StoryStepKind::HEAL; ++step){ + const std::string failure = walk_on_sim(sim, at, *step); + if (!failure.empty()){ + return std::format("Entry {} tile(s) short: ", short_by) + failure; + } + } + } + return true; +} + } @@ -389,6 +469,7 @@ void add_tests_AutoStorySimulation(UnitTestDatabase& database){ add("start and end checkpoints", test_start_and_end); add("a failed walk reloads its checkpoint's save", test_retry_from_save); add("a battle lost every time stops after 3 attempts", test_gives_up_after_attempts); + add("00.14 into Cerulean, the entry leg a tile short", test_cerulean_entry_short); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index ae23d4ffff..490e8fd1b1 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -491,11 +491,25 @@ StoryCheckpoint mt_moon(){ // joins the city's rows 20..22. Lass Crissy (75, 3) faces east, sight 4, // over row 3 from x 76 to 79; the way east keeps out of her line and off // the grass south of the road (75..87, 11..17). +// +// The open leg east across the city's edge ended a tile short on the +// console (2026-11-03: on (6, 20), not (7, 20)), and no hold to a wall +// in the city's west end finds the player from there: the next hold +// south met the fence on (6, 23) and the walk went on a column off. So +// the entry ends on (9, 20), and only legs to walls follow: north up +// column 9 (or 8, a tile short) to row 12, west to (8, 12), east to +// (30, 12) beside the policeman (31, 12), south to (30, 17), west to +// (25, 17), and down the ledge row 18 to (25, 19), a few tiles from the +// door (22, 19). The way keeps out of the boy's (16, 18..24) and the +// balding man's (8..10, 22..24) wander boxes. StoryCheckpoint route4_to_cerulean(){ return row("00.14", "Route 4 east to Cerulean's Pokemon Center", at(ROUTE4, 32, 6, SOUTH), after_brock(), StartMenu::FULL, { running(route("above_ledge", at(ROUTE4, 93, 8, SOUTH), "east along Route 4's upper road to above the ledge row 9")), hop("ledge_hop", SOUTH, at(ROUTE4, 93, 10, SOUTH), "down the ledge (93, 9) to the lower road"), - running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "east into Cerulean City and its Pokemon Center")), + running(route("cerulean_entry", at(CERULEAN, 9, 20, EAST), "east into Cerulean City, to (9, 20) below column 9")), + running(legs("above_city_ledge", {{NORTH, 8}, {WEST, 1}, {EAST, 22}, {SOUTH, 5}, {WEST, 5}}, "to walls: (9, 12), (8, 12), (30, 12), (30, 17), (25, 17)")), + hop("city_ledge_hop", SOUTH, at(CERULEAN, 25, 19, SOUTH), "down the ledge (25, 18)"), + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "west along row 20 to the Pokemon Center's door (22, 19)")), heal("heal_cerulean"), }); } From bdf777b0205a8366ea76f7e0106febb1204076e4 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 05:26:02 -0800 Subject: [PATCH 07/10] FRLG AutoStory: 00.15b, the rival in Cerulean as its own checkpoint 00.15 now ends after the GRIND with the nurse and a save on (23, 7), the tile below the rival's trigger. The new 00.15b is the step onto the trigger and his battle, so a lost battle is retried, and verify mode runs it, from right before him. 00.16 starts where his battle ends and goes to the nurse first. 00.16 moves from index 19 to 20. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 54 ++++++++++++++++--- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 52 ++++++++++-------- .../AutoStory/PokemonFRLG_AutoStoryTable.h | 3 +- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 8 +-- 4 files changed, 83 insertions(+), 34 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp index 3758cc2dea..9fabcb2487 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -9,6 +9,7 @@ */ #include +#include #include #include #include @@ -115,11 +116,11 @@ 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 00.11 00.12 00.12c 00.13 00.14 00.15 00.16"; + 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 00.12c 00.13 00.14 00.15 00.15b 00.16"; 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"}, - {.name = "handoffs off: Mt. Moon to Nugget Bridge", .first = "00.13", .last = "00.16", .expected_ids = "00.13 00.14 00.15 00.16"}, + {.name = "handoffs off: Mt. Moon to Nugget Bridge", .first = "00.13", .last = "00.16", .expected_ids = "00.13 00.14 00.15 00.15b 00.16"}, { .name = "SID Helper: 00.01 stops on the keyboard", .first = "00.01", @@ -168,8 +169,8 @@ void add_tests_GiftRunPlan(UnitTestDatabase& database){ .expected_ids = "00.11 00.12", .expected_stop = StoryHandoff::GIFT_RNG, }, - {.name = "Gift RNG off: 00.12b is passed over", .first = "00.11", .expected_ids = "00.11 00.12 00.12c 00.13 00.14 00.15 00.16"}, - {.name = "Gift RNG off: 00.12b runs when started at", .first = "00.12b", .expected_ids = "00.12b 00.12c 00.13 00.14 00.15 00.16"}, + {.name = "Gift RNG off: 00.12b is passed over", .first = "00.11", .expected_ids = "00.11 00.12 00.12c 00.13 00.14 00.15 00.15b 00.16"}, + {.name = "Gift RNG off: 00.12b runs when started at", .first = "00.12b", .expected_ids = "00.12b 00.12c 00.13 00.14 00.15 00.15b 00.16"}, { .name = "Gift RNG: started at 00.12b, from the save beside the salesman", .first = "00.12b", @@ -184,7 +185,7 @@ void add_tests_GiftRunPlan(UnitTestDatabase& database){ .expected_ids = "", .expected_stop = StoryHandoff::GIFT_RNG, }, - {.name = "Starter RNG alone: 00.12b runs when started at", .first = "00.12b", .handoffs = {.starter_rng = true}, .expected_ids = "00.12b 00.12c 00.13 00.14 00.15 00.16"}, + {.name = "Starter RNG alone: 00.12b runs when started at", .first = "00.12b", .handoffs = {.starter_rng = true}, .expected_ids = "00.12b 00.12c 00.13 00.14 00.15 00.15b 00.16"}, { .name = "Starter RNG first, with both on", .first = "00.03", @@ -291,9 +292,8 @@ UnitTestResult test_cerulean_mart_and_grind(){ const std::optional plan = parse_grind_variant(step.variant); return plan && plan->member == 0 && plan->level >= 22; }); - const size_t rival = find_step(steps, 0, [](const StoryStep& step){ return step.name == "rival_trigger"; }); - if (mart == steps.size() || grind == steps.size() || rival == steps.size() || grind > rival){ - return "00.15: expected a Cerulean Mart step buying Potions, then a GRIND of the lead to Lv22, before the rival's trigger."; + if (mart == steps.size() || grind == steps.size()){ + return "00.15: expected a Cerulean Mart step buying Potions, then a GRIND of the lead to Lv22."; } const std::optional clerk = mart > 0 && steps[mart - 1].target ? story_pose(*steps[mart - 1].target) : std::nullopt; if (!clerk || clerk->map != "CeruleanCity_Mart"){ @@ -311,6 +311,43 @@ UnitTestResult test_cerulean_mart_and_grind(){ return true; } +// 00.15b, the rival's battle as its own checkpoint, as every battle lost +// twice or more gets: it starts from a save, 00.15's on (23, 7) below his +// trigger with the nurse just before, so a verify run or a retry reloads +// right before him; and his is its only battle. +UnitTestResult test_cerulean_rival_row(){ + const std::vector& checkpoints = story_checkpoints(); + const size_t index = row_index("00.15b"); + const StoryCheckpoint& rival = checkpoints[index]; + const std::optional start = rival.start ? story_pose(*rival.start) : std::nullopt; + if (!start || rival.live_start || start->map != "CeruleanCity" || !(start->tile == TilePos{23, 7}) + || save_card_place(start->map).empty() + ){ + return "00.15b: expected it to start from a save on (23, 7) in Cerulean City."; + } + std::string battles; + for (const StoryStep& step : rival.steps){ + battles += step.kind == StoryStepKind::BATTLE ? " " + step.name : ""; + } + if (battles != " rival_cerulean" || rival.steps.size() != 2 || rival.steps[0].name != "rival_trigger"){ + return "00.15b: expected only the step onto his trigger and his battle; its battles:" + battles; + } + const StoryCheckpoint& before = checkpoints[index - 1]; + const size_t n = before.steps.size(); + const std::optional end = n > 0 && before.steps[n - 1].target ? story_pose(*before.steps[n - 1].target) : std::nullopt; + if (before.id != "00.15" || n < 2 || before.steps[n - 2].kind != StoryStepKind::HEAL + || !end || end->map != start->map || !(end->tile == start->tile) + ){ + return "00.15: expected it to end with the nurse, then the walk to (23, 7) where 00.15b starts."; + } + for (const StoryStep& step : before.steps){ + if (step.kind == StoryStepKind::BATTLE){ + return "00.15: expected no trainer battle before its save; found " + step.name + "."; + } + } + return true; +} + } @@ -320,6 +357,7 @@ void add_tests_AutoStoryRunPlan(UnitTestDatabase& database){ database.add("00.01b and 00.12b: back in after SID Helper and Gift RNG", test_after_sid_helper_row); database.add("00.04 and 00.04b: one checkpoint with Starter RNG off", test_starter_rows_joined); database.add("00.15: Potions at Cerulean's Mart and a GRIND before the rival", test_cerulean_mart_and_grind); + database.add("00.15b: the rival's battle alone, from a save before him", test_cerulean_rival_row); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 490e8fd1b1..b194210e6a 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -514,6 +514,30 @@ StoryCheckpoint route4_to_cerulean(){ }); } +// Before the rival north of Cerulean (cerulean_rival()): 5 Potions at the +// Mart (CeruleanCity_Mart_Items: POTION is row 3, 300 each; Mt. Moon's +// trainers leave well over the floor), then a GRIND to Lv22 on Route 4's +// grass east of Mt. Moon (75..86, 11..17; Spearow, Rattata, Ekans and +// Mankey around Lv10, about 80 EXP each), healing at the Center. With +// Bite chosen against his party, Lv22 is where a lone Wartortle beats it +// (#211's estimate: about 70 HP of his damage against its 61, which the +// Potions cover; at Lv18 about 100). Then the nurse, and the save on +// (23, 7), the tile below his trigger: his battle is its own checkpoint, +// 00.15b, verified and retried from this save. +StoryCheckpoint cerulean_grind(){ + const MartPlan potions{.map = CERULEAN_MART, .lines = {{.item = "potion", .quantity = 5}}, .budget = 1500, .money_floor = 1500}; + const GrindPlan to_lv22{.member = 0, .level = 22, .max_encounters = 100, .pace = WEST, .heal_every = 0}; + return row("00.15", "Cerulean: Potions, a GRIND to Lv22, then saved below the rival's trigger", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + running(route("mart_clerk", at(CERULEAN_MART, 4, 3, WEST), "out of the Center to the Mart's clerk")), + mart_step("buy_potions", "5 Potions: the GRIND fights wild battles only with 2 or more", potions), + running(route("grind_patch", at(ROUTE4, 86, 16, WEST), "west out of the city along Route 4's lower road onto its grass's east edge")), + grind_step("grind_to_lv22", "wild battles until the lead is Lv22, the nurse when hurt or low on PP", to_lv22, at(CERULEAN_CENTER, 7, 4, NORTH)), + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "back to the Center: the rival's battle starts at full HP and PP")), + heal("heal_cerulean"), + running(route("below_rival_trigger", at(CERULEAN, 23, 7, NORTH), "out of the Center, north up to (23, 7), short of the trigger row 6")), + }); +} + // The rival north of Cerulean (CeruleanCity scripts.inc): the only way // north to Nugget Bridge is columns 22..24, and a step onto row 6 there // (CeruleanCity_EventScript_RivalTrigger*, VAR_MAP_SCENE_CERULEAN_CITY_RIVAL @@ -523,28 +547,12 @@ StoryCheckpoint route4_to_cerulean(){ // CHECKER's text is read to the overworld with the battle's. For a player // who took Squirtle he has Pidgeotto Lv17, Abra Lv16, Rattata Lv15 and // Bulbasaur Lv18 (sParty_RivalCeruleanBulbasaur), whose Vine Whip and -// Sleep Powder are the danger. -// -// Before him: 5 Potions at the Mart (CeruleanCity_Mart_Items: POTION is -// row 3, 300 each; Mt. Moon's trainers leave well over the floor), then a -// GRIND to Lv22 on Route 4's grass east of Mt. Moon (75..86, 11..17; -// Spearow, Rattata, Ekans and Mankey around Lv10, about 80 EXP each), -// healing at the Center. With Bite chosen against his party, Lv22 is -// where a lone Wartortle beats it (#211's estimate: about 70 HP of his -// damage against its 61, which the Potions cover; at Lv18 about 100). +// Sleep Powder are the danger. Only the step and the battle: the row +// starts from 00.15's save below the trigger, and saves after him. StoryCheckpoint cerulean_rival(){ - const MartPlan potions{.map = CERULEAN_MART, .lines = {{.item = "potion", .quantity = 5}}, .budget = 1500, .money_floor = 1500}; - const GrindPlan to_lv22{.member = 0, .level = 22, .max_encounters = 100, .pace = WEST, .heal_every = 0}; - return row("00.15", "Cerulean: Potions, a GRIND to Lv22, then the rival north of the city", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { - running(route("mart_clerk", at(CERULEAN_MART, 4, 3, WEST), "out of the Center to the Mart's clerk")), - mart_step("buy_potions", "5 Potions: the GRIND fights wild battles only with 2 or more", potions), - running(route("grind_patch", at(ROUTE4, 86, 16, WEST), "west out of the city along Route 4's lower road onto its grass's east edge")), - grind_step("grind_to_lv22", "wild battles until the lead is Lv22, the nurse when hurt or low on PP", to_lv22, at(CERULEAN_CENTER, 7, 4, NORTH)), - running(route("below_rival_trigger", at(CERULEAN, 23, 7, NORTH), "back east into the city, north up to (23, 7), short of the trigger row 6")), + return row("00.15b", "Cerulean: the rival north of the city", at(CERULEAN, 23, 7, NORTH), after_brock(), StartMenu::FULL, { step_into("rival_trigger", NORTH, "onto (23, 6), the middle trigger: \"Yo! !\""), battle("rival_cerulean", at(CERULEAN, 23, 6, SOUTH), false, "Pidgeotto, Abra, Rattata and Bulbasaur; then the FAME CHECKER"), - running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "back to the Center before Nugget Bridge")), - heal("heal_cerulean"), }); } @@ -561,7 +569,9 @@ StoryCheckpoint cerulean_rival(){ // and the player turns east to him. Camper Shane (5, 21) looks north // from the west bank, off the way. StoryCheckpoint nugget_bridge(){ - return row("00.16", "Nugget Bridge: five trainers and the Rocket", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + return row("00.16", "Nugget Bridge: the nurse, five trainers and the Rocket", at(CERULEAN, 23, 6, SOUTH), after_brock(), StartMenu::FULL, { + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "back to the Center after the rival, before Nugget Bridge")), + heal("heal_cerulean"), running(route("below_cale_line", at(ROUTE24, 11, 32, NORTH), "out of the Center, north past the rival's row onto the bridge, below Cale's row")), step_into("cale_line", NORTH, "onto (11, 31), beside Cale"), battle("bug_catcher_cale", at(ROUTE24, 11, 31, NORTH), false, "his Bug Pokemon"), @@ -592,7 +602,7 @@ 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(), route3(), magikarp_salesman(), after_gift_rng(), magikarp_to_box(), mt_moon(), - route4_to_cerulean(), cerulean_rival(), nugget_bridge(), + route4_to_cerulean(), cerulean_grind(), cerulean_rival(), nugget_bridge(), }; 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 5106373639..f1bc862172 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h @@ -26,7 +26,8 @@ namespace PokemonFRLG{ // Checkpoints 00.01 to 00.16, in order, with 00.01b, the way back in after // SID Helper, after 00.01, 00.04 split in two at Squirtle's ball for // Starter RNG: 00.04 and 00.04b, 00.12b, the way back in after Gift RNG, -// after 00.12, and 00.12c, the Magikarp put in BOX 1, after it. +// after 00.12, 00.12c, the Magikarp put in BOX 1, after it, and 00.15b, +// the rival in Cerulean alone, from 00.15's save below his trigger. 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 c4f1646795..73aaa3eded 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory_Tests.cpp @@ -216,8 +216,8 @@ UnitTestResult check_checkpoint(const StoryCheckpoint& checkpoint, StoryPose& po UnitTestResult test_routes_and_chain(){ const std::vector& checkpoints = story_checkpoints(); - if (checkpoints.size() != 20 || checkpoints[0].start){ - return "Expected 20 checkpoints, the first from a new game."; + if (checkpoints.size() != 21 || checkpoints[0].start){ + return "Expected 21 checkpoints, the first from a new game."; } StoryPose pose; for (size_t c = 0; c < checkpoints.size(); c++){ @@ -652,13 +652,13 @@ UnitTestResult check_trigger(const TriggerCase& c){ return "No step " + c.checkpoint + "/" + c.step + "."; } -// 00.15 meets the rival on the middle of his three trigger tiles, row 6 +// 00.15b meets the rival on the middle of his three trigger tiles, row 6 // at x 22..24 north of Cerulean (VAR_MAP_SCENE_CERULEAN_CITY_RIVAL 0), // and 00.16 the Rocket on the east one of his two, (10..11, 15) on Nugget // Bridge (VAR_MAP_SCENE_ROUTE24 0), each from the tile below. UnitTestResult test_cerulean_triggers(){ const TriggerCase CASES[] = { - {.checkpoint = "00.15", .step = "rival_trigger", .map = "CeruleanCity", .y = 6, .x0 = 22, .x1 = 24, .onto_x = 23}, + {.checkpoint = "00.15b", .step = "rival_trigger", .map = "CeruleanCity", .y = 6, .x0 = 22, .x1 = 24, .onto_x = 23}, {.checkpoint = "00.16", .step = "rocket_trigger", .map = "Route24", .y = 15, .x0 = 10, .x1 = 11, .onto_x = 11}, }; for (const TriggerCase& c : CASES){ From cf7e173433c107ee50655585b39345cf9b0546d3 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 07:34:54 -0800 Subject: [PATCH 08/10] FRLG AutoStory: test that 00.15b first teaches Water Pulse if it isn't known (red) Console B's save (2026-11-03) is past 00.14c's TEACH, which failed there, and saved below the rival's trigger. 00.15b starts from that save, so it gets the same TEACH first; on a playthrough where 00.14c taught it, the step reads the moves and does nothing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp index 9fabcb2487..49f79c135b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -20,6 +20,7 @@ #include "PokemonFRLG_AutoStoryGrind.h" #include "PokemonFRLG_AutoStoryMart.h" #include "PokemonFRLG_AutoStoryTable.h" +#include "PokemonFRLG_AutoStoryTeach.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -314,7 +315,9 @@ UnitTestResult test_cerulean_mart_and_grind(){ // 00.15b, the rival's battle as its own checkpoint, as every battle lost // twice or more gets: it starts from a save, 00.15's on (23, 7) below his // trigger with the nurse just before, so a verify run or a retry reloads -// right before him; and his is its only battle. +// right before him; and his is its only battle. First Water Pulse over +// Tail Whip if the lead doesn't know it: console B's save (2026-11-03) is +// past the TEACH that failed there, which does nothing once it is known. UnitTestResult test_cerulean_rival_row(){ const std::vector& checkpoints = story_checkpoints(); const size_t index = row_index("00.15b"); @@ -329,8 +332,14 @@ UnitTestResult test_cerulean_rival_row(){ for (const StoryStep& step : rival.steps){ battles += step.kind == StoryStepKind::BATTLE ? " " + step.name : ""; } - if (battles != " rival_cerulean" || rival.steps.size() != 2 || rival.steps[0].name != "rival_trigger"){ - return "00.15b: expected only the step onto his trigger and his battle; its battles:" + battles; + if (battles != " rival_cerulean" || rival.steps.size() != 3 || rival.steps[1].name != "rival_trigger"){ + return "00.15b: expected a TEACH, the step onto his trigger and his battle; its battles:" + battles; + } + const std::optional teach = parse_teach_variant(rival.steps[0].variant); + if (rival.steps[0].kind != StoryStepKind::GAME || !teach || teach->item != "tm03" || teach->move != "water-pulse" + || teach->member != 0 || teach->forget_move != "tail-whip" + ){ + return "00.15b: expected it to start with TM03 Water Pulse over the lead's Tail Whip."; } const StoryCheckpoint& before = checkpoints[index - 1]; const size_t n = before.steps.size(); From 278fb46ac3c143860b02965bc494f0b3ce08a8b1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 07:35:43 -0800 Subject: [PATCH 09/10] FRLG AutoStory: 00.15b teaches Water Pulse over Tail Whip first, if not known Console B's save is below the rival's trigger, past the TEACH that failed for want of a TM CASE reader. The row now starts with the same TEACH; with Water Pulse already known it reads the moves and does nothing. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index b194210e6a..5c2a536873 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -10,6 +10,7 @@ #include "PokemonFRLG_AutoStoryMart.h" #include "PokemonFRLG_AutoStoryPcStorage.h" #include "PokemonFRLG_AutoStoryTable.h" +#include "PokemonFRLG_AutoStoryTeach.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -547,10 +548,15 @@ StoryCheckpoint cerulean_grind(){ // CHECKER's text is read to the overworld with the battle's. For a player // who took Squirtle he has Pidgeotto Lv17, Abra Lv16, Rattata Lv15 and // Bulbasaur Lv18 (sParty_RivalCeruleanBulbasaur), whose Vine Whip and -// Sleep Powder are the danger. Only the step and the battle: the row -// starts from 00.15's save below the trigger, and saves after him. +// Sleep Powder are the danger. The row starts from 00.15's save below the +// trigger, and saves after him. First TM03 Water Pulse over Tail Whip (from +// the bag, where the player stands), if the lead doesn't know it yet: +// console B's save (2026-11-03) is past Misty's TEACH, which failed there. +// Where she taught it, the step reads the moves and does nothing. StoryCheckpoint cerulean_rival(){ - return row("00.15b", "Cerulean: the rival north of the city", at(CERULEAN, 23, 7, NORTH), after_brock(), StartMenu::FULL, { + const TeachPlan water_pulse{.item = "tm03", .move = "water-pulse", .member = 0, .forget_move = "tail-whip"}; + return row("00.15b", "Cerulean: Water Pulse if not known, then the rival north of the city", at(CERULEAN, 23, 7, NORTH), after_brock(), StartMenu::FULL, { + teach_step("teach_water_pulse", "TM03 Water Pulse in place of Tail Whip, unless the lead knows it", water_pulse), step_into("rival_trigger", NORTH, "onto (23, 6), the middle trigger: \"Yo! !\""), battle("rival_cerulean", at(CERULEAN, 23, 6, SOUTH), false, "Pidgeotto, Abra, Rattata and Bulbasaur; then the FAME CHECKER"), }); From cd6d4e629f1124f49cce000c1323f73b569da950 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 07:52:36 -0800 Subject: [PATCH 10/10] FRLG AutoStory 00.15: Water Pulse first, then GRIND until the rival is 70% The row teaches TM03 Water Pulse over Tail Whip first, if the lead doesn't know it (skipped without TM03), so the GRIND's sim plays it. After the Mart, the GRIND stops when the sim beats the rival 00.15b fights (the table's trainer for rival_cerulean) 70% or more, the chance his battle refuses below, instead of at Lv22; Lv28 and 150 encounters are its caps. Console B's Wartortle is about 42% at Lv22 and 76% at Lv25. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 29 ++++++++++----- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 35 ++++++++++++------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp index 49f79c135b..6583d13a39 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -279,22 +279,35 @@ bool plans(const Pokemon::Pose& from, const Pokemon::Pose& to){ ).has_value(); } -// 00.15, before the rival: Potions from Cerulean's Mart (a loaded save's -// bag may hold too few for the wild battles a GRIND fights), then a GRIND -// on Route 4's grass east of Mt. Moon to Lv22, where Bite beats his party -// (#211), healing at Cerulean's Center; its trip there plans both ways. +// 00.15, before the rival, from the Center: first TM03 Water Pulse over +// Tail Whip if the lead doesn't know it, so the sim plays it in the +// GRIND; Potions from Cerulean's Mart (a loaded save's bag may hold too +// few for the wild battles a GRIND fights); then a GRIND on Route 4's +// grass east of Mt. Moon until the sim beats the rival 00.15b fights 70% +// or more, healing at Cerulean's Center; its trip there plans both ways. UnitTestResult test_cerulean_mart_and_grind(){ - const std::vector& steps = story_checkpoints()[row_index("00.15")].steps; + const StoryCheckpoint& row = story_checkpoints()[row_index("00.15")]; + const std::vector& steps = row.steps; + const std::optional start = row.start ? story_pose(*row.start) : std::nullopt; + if (!start || start->map != "CeruleanCity_PokemonCenter_1F"){ + return "00.15: expected it to start in Cerulean's Center."; + } + const std::optional teach = steps.empty() ? std::nullopt : parse_teach_variant(steps[0].variant); + if (!teach || teach->item != "tm03" || teach->move != "water-pulse" || teach->member != 0 || teach->forget_move != "tail-whip"){ + return "00.15: expected it to start with TM03 Water Pulse over the lead's Tail Whip."; + } const size_t mart = find_step(steps, 0, [](const StoryStep& step){ const std::optional plan = parse_mart_variant(step.variant); return plan && plan->map == "CeruleanCity_Mart" && !plan->lines.empty() && plan->lines[0].item == "potion"; }); - const size_t grind = find_step(steps, mart, [](const StoryStep& step){ + const std::optional rival = story_trainer("CeruleanCity", "rival_cerulean"); + const size_t grind = find_step(steps, mart, [&](const StoryStep& step){ const std::optional plan = parse_grind_variant(step.variant); - return plan && plan->member == 0 && plan->level >= 22; + return plan && plan->member == 0 && rival && plan->trainer == *rival && plan->min_chance == MIN_WIN_CHANCE + && plan->level > 25; }); if (mart == steps.size() || grind == steps.size()){ - return "00.15: expected a Cerulean Mart step buying Potions, then a GRIND of the lead to Lv22."; + return "00.15: expected a Cerulean Mart step buying Potions, then a GRIND of the lead until the rival is 70%, capped above Lv25."; } const std::optional clerk = mart > 0 && steps[mart - 1].target ? story_pose(*steps[mart - 1].target) : std::nullopt; if (!clerk || clerk->map != "CeruleanCity_Mart"){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 5c2a536873..69ec31968c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -515,24 +515,33 @@ StoryCheckpoint route4_to_cerulean(){ }); } -// Before the rival north of Cerulean (cerulean_rival()): 5 Potions at the -// Mart (CeruleanCity_Mart_Items: POTION is row 3, 300 each; Mt. Moon's -// trainers leave well over the floor), then a GRIND to Lv22 on Route 4's -// grass east of Mt. Moon (75..86, 11..17; Spearow, Rattata, Ekans and -// Mankey around Lv10, about 80 EXP each), healing at the Center. With -// Bite chosen against his party, Lv22 is where a lone Wartortle beats it -// (#211's estimate: about 70 HP of his damage against its 61, which the -// Potions cover; at Lv18 about 100). Then the nurse, and the save on -// (23, 7), the tile below his trigger: his battle is its own checkpoint, -// 00.15b, verified and retried from this save. +// Before the rival north of Cerulean (cerulean_rival()), from the +// Center: first TM03 Water Pulse over Tail Whip, if the lead doesn't know +// it yet, so the GRIND's sim plays it (a run without TM03 from Misty +// skips the step). Then 5 Potions at the Mart +// (CeruleanCity_Mart_Items: POTION is row 3, 300 each; Mt. Moon's +// trainers leave well over the floor), then a GRIND on Route 4's grass +// east of Mt. Moon (75..86, 11..17; Spearow, Rattata, Ekans and Mankey +// around Lv10, about 80 EXP each), healing at the Center, until the +// battle sim beats the rival 00.15b fights 70% or more: the win chance +// his battle refuses below. Console B's Wartortle (Lv22, 65 HP, Water +// Pulse) is about 42% there and 76% at Lv25; Lv28 (about 88%, some 120 +// battles from Lv22) is the cap. Then the nurse, and the save on (23, 7), +// the tile below his trigger: his battle is its own checkpoint, 00.15b, +// verified and retried from this save. StoryCheckpoint cerulean_grind(){ + const TeachPlan water_pulse{.item = "tm03", .move = "water-pulse", .member = 0, .forget_move = "tail-whip"}; const MartPlan potions{.map = CERULEAN_MART, .lines = {{.item = "potion", .quantity = 5}}, .budget = 1500, .money_floor = 1500}; - const GrindPlan to_lv22{.member = 0, .level = 22, .max_encounters = 100, .pace = WEST, .heal_every = 0}; - return row("00.15", "Cerulean: Potions, a GRIND to Lv22, then saved below the rival's trigger", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + const GrindPlan for_rival{ + .member = 0, .level = 28, .max_encounters = 150, .pace = WEST, .heal_every = 0, + .trainer = std::string(story_trainer(CERULEAN, "rival_cerulean").value_or("")), .min_chance = MIN_WIN_CHANCE, + }; + return row("00.15", "Cerulean: Water Pulse, Potions, a GRIND until the rival is winnable, then saved below his trigger", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + teach_step("teach_water_pulse", "TM03 Water Pulse in place of Tail Whip, unless the lead knows it or there is no TM03", water_pulse), running(route("mart_clerk", at(CERULEAN_MART, 4, 3, WEST), "out of the Center to the Mart's clerk")), mart_step("buy_potions", "5 Potions: the GRIND fights wild battles only with 2 or more", potions), running(route("grind_patch", at(ROUTE4, 86, 16, WEST), "west out of the city along Route 4's lower road onto its grass's east edge")), - grind_step("grind_to_lv22", "wild battles until the lead is Lv22, the nurse when hurt or low on PP", to_lv22, at(CERULEAN_CENTER, 7, 4, NORTH)), + grind_step("grind_for_rival", "wild battles until the sim beats the rival 70% or more, Lv28 at most; the nurse when hurt or low on PP", for_rival, at(CERULEAN_CENTER, 7, 4, NORTH)), running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "back to the Center: the rival's battle starts at full HP and PP")), heal("heal_cerulean"), running(route("below_rival_trigger", at(CERULEAN, 23, 7, NORTH), "out of the Center, north up to (23, 7), short of the trigger row 6")),