From 11ec40d573c4a4d6ca3647b503e907996e29281f Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 05:48:35 -0800 Subject: [PATCH 1/5] FRLG AutoStory: tests for Misty before the Cerulean rival (red) 00.14b and 00.14c between 00.14 and 00.15: 00.14b buys Potions at Cerulean's Mart and fights no battle; 00.14c starts from a save on (9, 7) in Cerulean Gym, talks to Misty from (9, 6), hers is its only battle, and it teaches TM03 Water Pulse in place of Tail Whip. The run plan's ID lists and the row count take the two rows. Sim: the way to Misty and back round the pool meets no trainer; the straight ways meet Luis and Diana. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_OverworldSim_Tests.cpp | 69 +++++++++++++++++++ .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 68 ++++++++++++++++-- .../AutoStory/PokemonFRLG_AutoStory_Tests.cpp | 4 +- 3 files changed, 133 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index b684bf8b40..1a79f041ce 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -798,6 +798,73 @@ UnitTestResult test_route4_to_cerulean(){ return true; } +// Story rows 00.14b and 00.14c in Cerulean Gym (pokefirered map.json): +// from the Mart's clerk into the Gym, up column 9 and west to (4, 13), +// then to walls round the pool's west and north sides and west to (9, 7) +// below Misty's platform, meeting no one; and back from beside her (9, 6) +// the same way round to (9, 13) and out to the Center. +UnitTestResult test_cerulean_gym_walks(){ + const SimCase in{.start = at("CeruleanCity_Mart", 4, 3), .facing = Direction::WEST, .goal = at("CeruleanCity_Gym", 4, 13)}; + const SimRun entered = run_case(in); + if (!arrived(entered, in.goal) || entered.state.warps != 2 || !entered.state.trainers_beaten.empty()){ + return "Expected the Gym entered and (4, 13) reached, no trainer met:" + describe(entered); + } + SimCase up{.start = in.goal, .facing = Direction::WEST, .goal = at("CeruleanCity_Gym", 9, 7)}; + up.route = std::vector{ + leg(Direction::NORTH, 3, at("CeruleanCity_Gym", 4, 10), true), + leg(Direction::EAST, 8, at("CeruleanCity_Gym", 12, 10), true), + leg(Direction::NORTH, 3, at("CeruleanCity_Gym", 12, 7), true), + leg(Direction::WEST, 3, up.goal, false), + }; + const SimRun walk = run_case(up); + if (!arrived(walk, up.goal) || !walk.state.trainers_beaten.empty()){ + return "Expected (9, 7) below Misty's platform reached round the pool, no trainer met:" + describe(walk); + } + SimCase out{.start = at("CeruleanCity_Gym", 9, 6), .facing = Direction::WEST, .goal = at("CeruleanCity_Gym", 9, 13)}; + out.route = std::vector{ + leg(Direction::SOUTH, 1, at("CeruleanCity_Gym", 9, 7), false), + leg(Direction::EAST, 3, at("CeruleanCity_Gym", 12, 7), true), + leg(Direction::SOUTH, 3, at("CeruleanCity_Gym", 12, 10), true), + leg(Direction::WEST, 8, at("CeruleanCity_Gym", 4, 10), true), + leg(Direction::SOUTH, 3, at("CeruleanCity_Gym", 4, 13), true), + leg(Direction::EAST, 5, out.goal, true), + }; + const SimRun back = run_case(out); + if (!arrived(back, out.goal) || !back.state.trainers_beaten.empty()){ + return "Expected (9, 13) reached from beside Misty round the pool, no trainer met:" + describe(back); + } + const SimCase nurse{.start = out.goal, .facing = Direction::EAST, .goal = at("CeruleanCity_PokemonCenter_1F", 7, 4)}; + const SimRun healed = run_case(nurse); + if (!arrived(healed, nurse.goal) || healed.state.warps != 2 || !healed.state.trainers_beaten.empty()){ + return "Expected the Center reached from (9, 13), no trainer met:" + describe(healed); + } + return true; +} + +// The straight ways in Cerulean Gym that 00.14b and 00.14c keep off: the +// step onto (9, 12) meets Swimmer Luis, and the step onto (8, 7), +// misty_front, Picnicker Diana. +UnitTestResult test_cerulean_gym_lines(){ + const struct{ + MapPosition from; + Direction direction; + MapPosition onto; + const char* trainer; + } LINES[] = { + {at("CeruleanCity_Gym", 9, 13), Direction::NORTH, at("CeruleanCity_Gym", 9, 12), "swimmer_m_water"}, + {at("CeruleanCity_Gym", 9, 7), Direction::WEST, at("CeruleanCity_Gym", 8, 7), "picnicker"}, + }; + for (const auto& line : LINES){ + SimCase c{.start = line.from, .facing = line.direction, .goal = line.onto}; + c.route = std::vector{leg(line.direction, 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 {} fought on the step onto {}:", line.trainer, to_string(line.onto)) + 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. @@ -865,6 +932,8 @@ void add_tests_OverworldSim(UnitTestDatabase& database){ database.add("Mt. Moon: black after a leg not marked wild", test_mt_moon_black_after_unmarked_leg); database.add("Mt. Moon: a battle seen only before the next leg", test_mt_moon_battle_before_next_leg); database.add("Route 4 east, down its ledge, to Cerulean's Pokemon Center", test_route4_to_cerulean); + database.add("Cerulean Gym: to Misty and back round the pool, no trainer met", test_cerulean_gym_walks); + database.add("Cerulean Gym: the straight ways meet Luis and Diana", test_cerulean_gym_lines); database.add("Cerulean: the Center to the rival's row and Nugget Bridge", test_cerulean_to_bridge); 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_AutoStoryRunPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp index 9fabcb2487..0d778bc621 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -4,7 +4,7 @@ * * Which rows of the story table a run takes (plan_story_run()) with the * RNG programs' handoffs on and off, the rows the handoffs split or - * join, and what 00.15 runs before the rival. + * join, Misty in Cerulean Gym, and what 00.15 runs before the rival. * */ @@ -20,6 +20,7 @@ #include "PokemonFRLG_AutoStoryGrind.h" #include "PokemonFRLG_AutoStoryMart.h" #include "PokemonFRLG_AutoStoryTable.h" +#include "PokemonFRLG_AutoStoryTeach.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -116,11 +117,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.15b 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.14b 00.14c 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.15b 00.16"}, + {.name = "handoffs off: Mt. Moon to Nugget Bridge", .first = "00.13", .last = "00.16", .expected_ids = "00.13 00.14 00.14b 00.14c 00.15 00.15b 00.16"}, { .name = "SID Helper: 00.01 stops on the keyboard", .first = "00.01", @@ -169,8 +170,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.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 off: 00.12b is passed over", .first = "00.11", .expected_ids = "00.11 00.12 00.12c 00.13 00.14 00.14b 00.14c 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.14b 00.14c 00.15 00.15b 00.16"}, { .name = "Gift RNG: started at 00.12b, from the save beside the salesman", .first = "00.12b", @@ -185,7 +186,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.15b 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.14b 00.14c 00.15 00.15b 00.16"}, { .name = "Starter RNG first, with both on", .first = "00.03", @@ -348,6 +349,60 @@ UnitTestResult test_cerulean_rival_row(){ return true; } +// The battles in `row`, each name after a space. +std::string battle_names(const StoryCheckpoint& row){ + std::string battles; + for (const StoryStep& step : row.steps){ + battles += step.kind == StoryStepKind::BATTLE ? " " + step.name : ""; + } + return battles; +} + +// 00.14b, before Misty: Potions at Cerulean's Mart, then into the Gym to +// a save on (9, 7), below her platform; no battle. 00.14c, Misty's battle +// as its own checkpoint: it starts from that save, talks to her from +// beside her, (9, 6), and hers is its only battle; then TM03 Water Pulse +// in place of Tail Whip. +UnitTestResult test_misty_rows(){ + const std::vector& checkpoints = story_checkpoints(); + const StoryCheckpoint& gym = checkpoints[row_index("00.14b")]; + const StoryCheckpoint& misty = checkpoints[row_index("00.14c")]; + if (checkpoints[row_index("00.14b") - 1].id != "00.14" || checkpoints[row_index("00.14c") + 1].id != "00.15"){ + return "Expected 00.14b and 00.14c between 00.14 and 00.15."; + } + const std::vector& steps = gym.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"; + }); + if (mart == steps.size() || !battle_names(gym).empty()){ + return "00.14b: expected a Cerulean Mart step buying Potions and no battle; its battles:" + battle_names(gym); + } + const std::optional start = misty.start ? story_pose(*misty.start) : std::nullopt; + if (!start || misty.live_start || start->map != "CeruleanCity_Gym" || !(start->tile == TilePos{9, 7}) + || save_card_place(start->map).empty() + ){ + return "00.14c: expected it to start from a save on (9, 7) in Cerulean Gym."; + } + if (battle_names(misty) != " misty" || misty.steps.size() < 3 || misty.steps[1].kind != StoryStepKind::TALK + || misty.steps[2].name != "misty" + ){ + return "00.14c: expected a walk beside Misty, the talk and her battle, the only one; its battles:" + battle_names(misty); + } + const std::optional beside = misty.steps[0].target ? story_pose(*misty.steps[0].target) : std::nullopt; + if (!beside || !(beside->tile == TilePos{9, 6}) || beside->facing != Direction::WEST){ + return "00.14c: expected the talk from (9, 6), facing Misty (8, 6)."; + } + const size_t teach = find_step(misty.steps, 0, [](const StoryStep& step){ + const std::optional plan = parse_teach_variant(step.variant); + return plan && plan->item == "tm03" && plan->move == "water-pulse" && plan->member == 0 && plan->forget_move == "tail-whip"; + }); + if (teach == misty.steps.size()){ + return "00.14c: expected TM03 Water Pulse taught to the lead in place of Tail Whip."; + } + return true; +} + } @@ -358,6 +413,7 @@ void add_tests_AutoStoryRunPlan(UnitTestDatabase& database){ 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); + database.add("00.14b and 00.14c: Cerulean Gym, then Misty alone from a save before her", test_misty_rows); } 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 73aaa3eded..af9c26cf8e 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() != 21 || checkpoints[0].start){ - return "Expected 21 checkpoints, the first from a new game."; + if (checkpoints.size() != 23 || checkpoints[0].start){ + return "Expected 23 checkpoints, the first from a new game."; } StoryPose pose; for (size_t c = 0; c < checkpoints.size(); c++){ From 604311b2293f5aeb7e85af09809f5b52d46c7683 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 05:48:36 -0800 Subject: [PATCH 2/5] FRLG AutoStory: 00.14b and 00.14c, Misty before the Cerulean rival 00.14b: from Cerulean's Center, up to 5 Potions at the Mart, then into the Gym and round the pool to (9, 7), below Misty's platform, out of Picnicker Diana's line (4, 7, east, sight 4) and Swimmer Luis's (10, 12, west, sight 1), where it saves. No gym trainer is needed for Misty, so none is fought and there is no nurse trip. 00.14c: up onto (9, 6), Misty and her battle (Staryu Lv18, Starmie Lv21), back round the pool to the Center and the nurse, then TM03 Water Pulse in place of Tail Whip, last, since the console's TEACH has no TM Case reader yet. It ends where 00.15 starts. 00.15 moves from index 18 to 20. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 51 ++++++++++++++++++- .../AutoStory/PokemonFRLG_AutoStoryTable.h | 10 ++-- 2 files changed, 56 insertions(+), 5 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..122ec8a26a 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{ @@ -42,6 +43,7 @@ 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 CERULEAN_GYM = "CeruleanCity_Gym"; const char* const ROUTE24 = "Route24"; Pokemon::Pose at(const char* map, int x, int y, std::optional facing = std::nullopt){ @@ -514,6 +516,53 @@ StoryCheckpoint route4_to_cerulean(){ }); } +// Cerulean Gym (pokefirered CeruleanCity_Gym map.json and scripts.inc): +// a pool with walkways. Misty (8, 6) stands on a platform (7..9, 6) above +// row 7. Picnicker Diana (4, 7) faces east, sight 4, over (5..8, 7), so +// misty_front (8, 7) is in her line; Swimmer Luis (10, 12) in the water +// faces west, sight 1, over (9, 12), on column 9 north of the door. No +// gym trainer has to be fought for Misty, so neither is: from the door up +// column 9 to row 13, west to (4, 13), and to walls round the pool's west +// and north sides, (4, 10), (12, 10), (12, 7), then west to (9, 7), the +// tile below the platform's east end, out of Diana's line. 00.14c steps +// up onto (9, 6) and talks to Misty from beside her. +// +// Before her: up to 5 Potions at the Mart (CeruleanCity_Mart_Items: row +// 3, 300 each; floor and budget 1500 as in 00.15, since the money box +// can't be read yet). With no gym trainer fought there is no nurse trip: +// the row starts healed at the Center, and its save is before Misty, so +// her battle is its own checkpoint, 00.14c, verified and retried from it. +StoryCheckpoint cerulean_gym(){ + const MartPlan potions{.map = CERULEAN_MART, .lines = {{.item = "potion", .quantity = 5}}, .budget = 1500, .money_floor = 1500}; + return row("00.14b", "Cerulean: Potions, then into the Gym, saved below Misty's platform", 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", "up to 5 Potions before Misty", potions), + running(route("gym_west_walkway", at(CERULEAN_GYM, 4, 13), "into the Gym, up column 9 to row 13, west to its wall, short of Luis's (9, 12)")), + running(legs("below_misty_platform", {{NORTH, 3}, {EAST, 8}, {NORTH, 3}, {WEST, 3}}, "walls (4, 10), (12, 10), (12, 7), then to (9, 7), out of Diana's line")), + }); +} + +// Misty (TRAINER_LEADER_MISTY: Staryu Lv18, Starmie Lv21), talked to from +// (9, 6), beside her on the platform. Her win text runs on to the CASCADE +// BADGE, TM03 (Water Pulse) and its explanation, all read with the +// battle's. Bite is chosen against her party (Starmie is Water/Psychic). +// Back to the nurse round the pool's west side, out of Luis's line; then +// Water Pulse in place of Tail Whip, last, at the Center: the console's +// TEACH has no TM Case reader yet and stops there, after the walk and the +// heal. Ends where 00.15 starts. +StoryCheckpoint misty(){ + const TeachPlan water_pulse{.item = "tm03", .move = "water-pulse", .member = 0, .forget_move = "tail-whip"}; + return row("00.14c", "Cerulean Gym: Misty, then Water Pulse", at(CERULEAN_GYM, 9, 7, WEST), after_brock(), StartMenu::FULL, { + running(route("beside_misty", at(CERULEAN_GYM, 9, 6, WEST), "up onto (9, 6), facing Misty (8, 6)")), + talk("challenge_misty", "Misty's speech"), + battle("misty", at(CERULEAN_GYM, 9, 6, WEST), false, "Staryu and Starmie, with Bite; CASCADE BADGE and TM03"), + running(legs("gym_way_out", {{SOUTH, 1}, {EAST, 3}, {SOUTH, 3}, {WEST, 8}, {SOUTH, 3}, {EAST, 5}}, "walls (12, 7), (12, 10), (4, 10), (4, 13), (9, 13), round Luis's (9, 12)")), + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "down column 9 out of the Gym, to the Center")), + heal("heal_cerulean"), + teach_step("teach_water_pulse", "TM03 Water Pulse in place of Tail Whip", water_pulse), + }); +} + // 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 @@ -602,7 +651,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_grind(), cerulean_rival(), nugget_bridge(), + route4_to_cerulean(), cerulean_gym(), misty(), 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 f1bc862172..153a4f1e8b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h @@ -4,8 +4,8 @@ * * FireRed from a new game to the Boulder Badge, the Running Shoes, Route * 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 + * Moon to Route 4 east of it, and on to Cerulean City, Misty, 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. * @@ -26,8 +26,10 @@ 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, 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. +// after 00.12, 00.12c, the Magikarp put in BOX 1, after it, 00.14b and +// 00.14c, Cerulean Gym to a save below Misty's platform and Misty alone +// from it, and 00.15b, the rival in Cerulean alone, from 00.15's save +// below his trigger. const std::vector& story_checkpoints(); From 49cb58036c5ad54236edc4b91e7f26355795d504 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 06:13:06 -0800 Subject: [PATCH 3/5] FRLG AutoStory: test 00.14b's GRIND to Lv23 and Super Potions before Misty (red) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRunPlan_Tests.cpp | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) 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 0d778bc621..8e6a7ee176 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRunPlan_Tests.cpp @@ -403,6 +403,63 @@ UnitTestResult test_misty_rows(){ return true; } +// 00.14b in order (Misty lost to at Lv20, console B, run m51): the Mart +// first, Potions then Super Potions, the Potions enough for the GRIND's +// wild battles at the table's money floor and at least one Super Potion +// after them; a GRIND of the lead to Lv23 on Route 4, from a route to its +// patch, with Cerulean's nurse as its target; the nurse; then the walk +// into the Gym, ending on (9, 7). +UnitTestResult test_misty_grind_order(){ + const std::vector& steps = story_checkpoints()[row_index("00.14b")].steps; + std::optional plan; + const size_t mart = find_step(steps, 0, [&](const StoryStep& step){ + plan = parse_mart_variant(step.variant); + return plan && plan->map == "CeruleanCity_Mart"; + }); + if (mart == steps.size() || plan->lines.size() < 2 || plan->lines[0].item != "potion" || plan->lines[1].item != "super-potion" + || !plan->money_floor + ){ + return "00.14b: expected a Cerulean Mart step buying Potions, then Super Potions, with a money floor."; + } + const std::optional> buys = plan_mart_buys(mart_stock(plan->map), *plan, *plan->money_floor); + if (!buys || buys->size() < 2 || (*buys)[0].quantity < 2 || (*buys)[1].item != "super-potion"){ + return "00.14b: expected the money floor to buy 2 or more Potions and a Super Potion."; + } + const size_t grind = find_step(steps, mart, [](const StoryStep& step){ + const std::optional grind_plan = parse_grind_variant(step.variant); + return grind_plan && grind_plan->member == 0 && grind_plan->level >= 23; + }); + if (grind == steps.size() || !steps[grind - 1].target || !steps[grind].target){ + return "00.14b: expected a GRIND of the lead to Lv23 after the Mart, right after a route to its patch, the nurse its target."; + } + const std::optional patch = story_pose(*steps[grind - 1].target); + const std::optional nurse = story_pose(*steps[grind].target); + if (!patch || patch->map != "Route4" || !nurse || nurse->map != "CeruleanCity_PokemonCenter_1F" + || !plans(*steps[grind - 1].target, *steps[grind].target) || !plans(*steps[grind].target, *steps[grind - 1].target) + ){ + return "00.14b: expected the GRIND on Route 4, its trips to Cerulean's nurse and back planning."; + } + const size_t heal = find_step(steps, grind + 1, [](const StoryStep& step){ + return step.name == "heal_cerulean"; + }); + const size_t gym = find_step(steps, grind + 1, [](const StoryStep& step){ + const std::optional pose = step.target ? story_pose(*step.target) : std::nullopt; + return pose && pose->map == "CeruleanCity_Gym"; + }); + if (heal == steps.size() || gym == steps.size() || heal > gym){ + return "00.14b: expected the nurse after the GRIND, then the walk into the Gym."; + } + StoryPose end; + for (const StoryStep& step : steps){ + const std::optional pose = step.target ? story_pose(*step.target) : std::nullopt; + end = pose ? *pose : end; + } + if (end.map != "CeruleanCity_Gym"){ + return "00.14b: expected the row to end in Cerulean Gym."; + } + return true; +} + } @@ -414,6 +471,7 @@ void add_tests_AutoStoryRunPlan(UnitTestDatabase& database){ 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); database.add("00.14b and 00.14c: Cerulean Gym, then Misty alone from a save before her", test_misty_rows); + database.add("00.14b: Potions and Super Potions, a GRIND to Lv23, the nurse, then the Gym", test_misty_grind_order); } From de3f85bd95b10a6f1f03cff1fe428bb82300782b Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 06:13:06 -0800 Subject: [PATCH 4/5] FRLG AutoStory: 00.14b grinds to Lv23 and buys Super Potions before Misty Misty beat a Lv20 Wartortle on console B (run m51). 00.14b now buys 5 Potions and up to 3 Super Potions, grinds the lead to Lv23 on Route 4, heals and walks into the Gym. The walk in goes to (9, 13) first, so it no longer passes through Luis's line at (9, 12). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_OverworldSim_Tests.cpp | 114 +++++++++--------- .../AutoStory/PokemonFRLG_AutoStoryTable.cpp | 58 +++++++-- 2 files changed, 105 insertions(+), 67 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index 1a79f041ce..d50094c97d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -773,43 +773,49 @@ UnitTestResult test_mt_moon_way_out(){ 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 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.14b and 00.14c in Cerulean Gym (pokefirered map.json): -// from the Mart's clerk into the Gym, up column 9 and west to (4, 13), +// from the Center into the Gym, up column 9 to (9, 13), west to (4, 13), // then to walls round the pool's west and north sides and west to (9, 7) // below Misty's platform, meeting no one; and back from beside her (9, 6) // the same way round to (9, 13) and out to the Center. UnitTestResult test_cerulean_gym_walks(){ - const SimCase in{.start = at("CeruleanCity_Mart", 4, 3), .facing = Direction::WEST, .goal = at("CeruleanCity_Gym", 4, 13)}; + const SimCase in{.start = at("CeruleanCity_PokemonCenter_1F", 7, 4), .facing = Direction::NORTH, .goal = at("CeruleanCity_Gym", 9, 13)}; const SimRun entered = run_case(in); if (!arrived(entered, in.goal) || entered.state.warps != 2 || !entered.state.trainers_beaten.empty()){ - return "Expected the Gym entered and (4, 13) reached, no trainer met:" + describe(entered); + return "Expected the Gym entered and (9, 13) reached, no trainer met:" + describe(entered); } - SimCase up{.start = in.goal, .facing = Direction::WEST, .goal = at("CeruleanCity_Gym", 9, 7)}; + SimCase west{.start = in.goal, .facing = Direction::NORTH, .goal = at("CeruleanCity_Gym", 4, 13)}; + west.route = std::vector{leg(Direction::WEST, 5, west.goal, true)}; + const SimRun walkway = run_case(west); + if (!arrived(walkway, west.goal) || !walkway.state.trainers_beaten.empty()){ + return "Expected the wall (4, 13) reached west from (9, 13), no trainer met:" + describe(walkway); + } + SimCase up{.start = west.goal, .facing = Direction::WEST, .goal = at("CeruleanCity_Gym", 9, 7)}; up.route = std::vector{ leg(Direction::NORTH, 3, at("CeruleanCity_Gym", 4, 10), true), leg(Direction::EAST, 8, at("CeruleanCity_Gym", 12, 10), true), @@ -880,31 +886,31 @@ UnitTestResult test_cerulean_to_bridge(){ 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; -} +// 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; +} } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 122ec8a26a..8f13fb9d28 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -522,22 +522,54 @@ StoryCheckpoint route4_to_cerulean(){ // misty_front (8, 7) is in her line; Swimmer Luis (10, 12) in the water // faces west, sight 1, over (9, 12), on column 9 north of the door. No // gym trainer has to be fought for Misty, so neither is: from the door up -// column 9 to row 13, west to (4, 13), and to walls round the pool's west -// and north sides, (4, 10), (12, 10), (12, 7), then west to (9, 7), the -// tile below the platform's east end, out of Diana's line. 00.14c steps -// up onto (9, 6) and talks to Misty from beside her. +// column 9 to row 13 (a route there, so the planner's walk to a wall +// doesn't run on past it through (9, 12)), west to (4, 13), and to walls +// round the pool's west and north sides, (4, 10), (12, 10), (12, 7), then +// west to (9, 7), the tile below the platform's east end, out of Diana's +// line. 00.14c steps up onto (9, 6) and talks to Misty from beside her. // -// Before her: up to 5 Potions at the Mart (CeruleanCity_Mart_Items: row -// 3, 300 each; floor and budget 1500 as in 00.15, since the money box -// can't be read yet). With no gym trainer fought there is no nurse trip: -// the row starts healed at the Center, and its save is before Misty, so -// her battle is its own checkpoint, 00.14c, verified and retried from it. +// Misty beat a Lv20 Wartortle (console B, run m51), so first a GRIND to +// Lv23 on 00.15's patch. Gen 3 numbers (pokefirered: her mons have IV 0; +// Wartortle taken at IV 15, no EVs): Starmie Lv21 has 56 HP, Atk 36, SpA +// 47, SpD 40. Bite is Dark, special in Gen 3, no STAB, x2 on Psychic: at +// Lv23 (SpA 39) it does 23..28, three hits (two on top rolls); at Lv20, +// 20..24. Her Swift (physical, 60) does 9..11 to a Lv23 Wartortle (about +// 67 HP, Def 45) and Water Pulse (STAB, resisted) 8..10: 6 or more hits, +// though m51 saw about 20 a turn. Her Recover (28) and Super Potion undo +// a Bite, so the margin is in Bite's damage. +// +// The Mart comes before the GRIND: its wild battles are fought only with +// 2 or more Potions, and 00.14's save has none (m51 met Misty with only +// the 5 this row bought). 5 Potions (row 3, 300 each), then up to 3 Super +// Potions (row 2, 700), last so they take the Items pocket's last row; +// the battle doesn't use them yet. The money box can't be read, so the +// floor stands for it, and a floor above the money fails the step (the +// quantity box wraps). Prizes (pokefirered: 4 x last level x the class's +// factor) from a new game's 3000, less Viridian's 2000 and the Magikarp's +// 500: Liam 220, Brock 1400, Route 3's bug catchers 360, the Mt. Moon +// grunt 416 and Miguel 288 (the first rival's 80 only if won), at least +// 3184 here. Floor and budget 3000: 5 Potions and 2 Super Potions, 2900. +// Misty's 2100 (4 x 21 x 25) then leaves 00.15 its floor of 1500. Then the +// nurse, and the save before Misty, so her battle is its own checkpoint, +// 00.14c, verified and retried from it. 00.15's GRIND to Lv22 then ends +// at once. StoryCheckpoint cerulean_gym(){ - const MartPlan potions{.map = CERULEAN_MART, .lines = {{.item = "potion", .quantity = 5}}, .budget = 1500, .money_floor = 1500}; - return row("00.14b", "Cerulean: Potions, then into the Gym, saved below Misty's platform", at(CERULEAN_CENTER, 7, 4, NORTH), after_brock(), StartMenu::FULL, { + const MartPlan supplies{ + .map = CERULEAN_MART, + .lines = {{.item = "potion", .quantity = 5}, {.item = "super-potion", .quantity = 3}}, + .budget = 3000, + .money_floor = 3000, + }; + const GrindPlan to_lv23{.member = 0, .level = 23, .max_encounters = 100, .pace = WEST, .heal_every = 0}; + return row("00.14b", "Cerulean: Potions, a GRIND to Lv23, then into the Gym, saved below Misty's platform", 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", "up to 5 Potions before Misty", potions), - running(route("gym_west_walkway", at(CERULEAN_GYM, 4, 13), "into the Gym, up column 9 to row 13, west to its wall, short of Luis's (9, 12)")), + mart_step("buy_potions", "5 Potions for the GRIND's wild battles, then Super Potions as the money allows", supplies), + 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_lv23", "wild battles until the lead is Lv23, where Bite takes Starmie in three", to_lv23, at(CERULEAN_CENTER, 7, 4, NORTH)), + running(route("cerulean_nurse", at(CERULEAN_CENTER, 7, 4, NORTH), "back to the Center: Misty's battle starts at full HP and PP")), + heal("heal_cerulean"), + running(route("gym_below_luis", at(CERULEAN_GYM, 9, 13), "into the Gym, up column 9 to row 13, short of Luis's (9, 12)")), + running(legs("gym_west_walkway", {{WEST, 5}}, "west to the wall (4, 13)")), running(legs("below_misty_platform", {{NORTH, 3}, {EAST, 8}, {NORTH, 3}, {WEST, 3}}, "walls (4, 10), (12, 10), (12, 7), then to (9, 7), out of Diana's line")), }); } From 359eea6dffaaecb4b87671f1d8444aeea0326394 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 07:36:47 -0800 Subject: [PATCH 5/5] FRLG AutoStory: 00.14c's TEACH has the TM CASE's readers now Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp | 5 ++--- 1 file changed, 2 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 e193335ebd..c76c52110d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -579,9 +579,8 @@ StoryCheckpoint cerulean_gym(){ // BADGE, TM03 (Water Pulse) and its explanation, all read with the // battle's. Bite is chosen against her party (Starmie is Water/Psychic). // Back to the nurse round the pool's west side, out of Luis's line; then -// Water Pulse in place of Tail Whip, last, at the Center: the console's -// TEACH has no TM Case reader yet and stops there, after the walk and the -// heal. Ends where 00.15 starts. +// Water Pulse in place of Tail Whip, last, at the Center, from the bag's +// TM CASE. Ends where 00.15 starts. StoryCheckpoint misty(){ const TeachPlan water_pulse{.item = "tm03", .move = "water-pulse", .member = 0, .forget_move = "tail-whip"}; return row("00.14c", "Cerulean Gym: Misty, then Water Pulse", at(CERULEAN_GYM, 9, 7, WEST), after_brock(), StartMenu::FULL, {