diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp index b684bf8b40..d50094c97d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldSim_Tests.cpp @@ -773,31 +773,104 @@ 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 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_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 (9, 13) reached, no trainer met:" + describe(entered); + } + 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), + 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. @@ -813,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; +} } @@ -865,6 +938,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 6583d13a39..59c7c827fa 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. * */ @@ -117,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", @@ -170,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", @@ -186,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", @@ -370,6 +370,117 @@ 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; +} + +// 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; +} + } @@ -380,6 +491,8 @@ 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); + database.add("00.14b: Potions and Super Potions, a GRIND to Lv23, the nurse, then the Gym", test_misty_grind_order); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp index 69ec31968c..0a7e4a6f87 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp @@ -43,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){ @@ -515,6 +516,84 @@ 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 (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. +// +// 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 then fights +// until the sim beats the Cerulean rival often enough. +StoryCheckpoint cerulean_gym(){ + 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", "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")), + }); +} + +// 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, 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, { + 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()), 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 @@ -617,7 +696,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(); 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++){