From 3d9bf943def9a12d0f808bdf028af6c73a6e43a6 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 09:53:37 -0800 Subject: [PATCH 01/13] Pokemon navigation: story steps, one table vocabulary for FRLG and BDSP A story checkpoint as a list of StorySteps (route, legs, step onto a trigger, talk, YES/NO, scene, battle, train, heal, shop, save, ready for a boss, and a game-only step), plain data, built into engine events by build_story_checkpoint(). Each event asks the game's StoryStepExecutor to do its step: one virtual per kind, failing as unsupported unless the game overrides it. Poses are the game's (TilePose); skip_if names conditions the executor answers. Tests run tables against a fake executor: event order, skip_if, pose propagation, failure naming, unsupported kinds, a segment with a reload, and lint. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../ComputerPrograms_UnitTestRunner.cpp | 1 + .../Navigation/Pokemon_Navigation_Tests.h | 1 + .../Pokemon/Navigation/Pokemon_StorySteps.cpp | 260 ++++++++++++ .../Pokemon/Navigation/Pokemon_StorySteps.h | 180 +++++++++ .../Navigation/Pokemon_StorySteps_Tests.cpp | 369 ++++++++++++++++++ SerialPrograms/cmake/SourceFiles.cmake | 3 + 6 files changed, 814 insertions(+) create mode 100644 SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp create mode 100644 SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h create mode 100644 SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index 2e17398c46..f6b73d8901 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -65,6 +65,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ Pokemon::add_tests_MapData(ret); Pokemon::add_tests_StoryEvents(ret); Pokemon::add_tests_StorySegments(ret); + Pokemon::add_tests_StorySteps(ret); NintendoSwitch::add_tests_CheckOnlineDetector(ret); NintendoSwitch::add_tests_FailedToConnectDetector(ret); NintendoSwitch::add_tests_UpdatePopupDetector(ret); diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h index 73eac9841a..8506321ce3 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h @@ -20,6 +20,7 @@ void add_tests_LegPlanner(UnitTestDatabase& database); void add_tests_MapData(UnitTestDatabase& database); void add_tests_StoryEvents(UnitTestDatabase& database); void add_tests_StorySegments(UnitTestDatabase& database); +void add_tests_StorySteps(UnitTestDatabase& database); // (name, JSON text) of each map in Pokemon/Navigation/TestMaps/, built into // the program by pa_embed_text_files() (cmake/EmbedTextFiles.cmake). diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp new file mode 100644 index 0000000000..406e45e36f --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp @@ -0,0 +1,260 @@ +/* Story Steps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Pokemon_StorySteps.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +const char* story_step_kind_name(StoryStepKind kind){ + switch (kind){ + case StoryStepKind::NEW_GAME: + return "NEW_GAME"; + case StoryStepKind::OPTIONS: + return "OPTIONS"; + case StoryStepKind::ROUTE: + return "ROUTE"; + case StoryStepKind::LEGS: + return "LEGS"; + case StoryStepKind::STEP: + return "STEP"; + case StoryStepKind::TALK: + return "TALK"; + case StoryStepKind::ANSWER: + return "ANSWER"; + case StoryStepKind::MASH: + return "MASH"; + case StoryStepKind::BATTLE: + return "BATTLE"; + case StoryStepKind::TRAIN: + return "TRAIN"; + case StoryStepKind::HEAL: + return "HEAL"; + case StoryStepKind::SHOP: + return "SHOP"; + case StoryStepKind::SAVE: + return "SAVE"; + case StoryStepKind::READY: + return "READY"; + case StoryStepKind::GAME: + return "GAME"; + } + return "?"; +} + + +Outcome unsupported_step(const StoryStep& step){ + return outcome_failed("unsupported_step", std::format("{} {}", story_step_kind_name(step.kind), step.name)); +} + +Outcome StoryStepExecutor::run(RunState& state, const StoryStep& step){ + return dispatch_story_step(*this, state, step); +} + +Outcome StoryStepExecutor::new_game(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::options(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::route(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::legs(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::step(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::talk(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::answer(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::mash(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::battle(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::train(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::heal(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::shop(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::save(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::ready(RunState&, const StoryStep& step){ + return unsupported_step(step); +} +Outcome StoryStepExecutor::game(RunState&, const StoryStep& step){ + return unsupported_step(step); +} + +Outcome dispatch_story_step(StoryStepExecutor& executor, RunState& state, const StoryStep& step){ + switch (step.kind){ + case StoryStepKind::NEW_GAME: + return executor.new_game(state, step); + case StoryStepKind::OPTIONS: + return executor.options(state, step); + case StoryStepKind::ROUTE: + return executor.route(state, step); + case StoryStepKind::LEGS: + return executor.legs(state, step); + case StoryStepKind::STEP: + return executor.step(state, step); + case StoryStepKind::TALK: + return executor.talk(state, step); + case StoryStepKind::ANSWER: + return executor.answer(state, step); + case StoryStepKind::MASH: + return executor.mash(state, step); + case StoryStepKind::BATTLE: + return executor.battle(state, step); + case StoryStepKind::TRAIN: + return executor.train(state, step); + case StoryStepKind::HEAL: + return executor.heal(state, step); + case StoryStepKind::SHOP: + return executor.shop(state, step); + case StoryStepKind::SAVE: + return executor.save(state, step); + case StoryStepKind::READY: + return executor.ready(state, step); + case StoryStepKind::GAME: + return executor.game(state, step); + } + return unsupported_step(step); +} + + +Event story_event(const StoryStep& step, StoryStepExecutor& executor){ + Event event; + event.name = step.name; + event.why = step.why; + for (const std::string& name : step.skip_if){ + event.skip_if.push_back(condition(name, [&executor, name](RunState& state){ + return executor.holds(name, state); + })); + } + event.act = [&executor, &step](RunState& state){ + return executor.run(state, step); + }; + event.pose_learned = step.pose_after; + return event; +} + +namespace{ + +// The first event of a checkpoint that starts from a save: where the save +// leaves the player, for a run that reloaded it. +Event from_save_event(const Pose& start){ + Event event; + event.name = "from_save"; + event.why = "where the checkpoint's save leaves the player"; + event.act = [](RunState&){ return outcome_ok(); }; + event.pose_learned = start; + return event; +} + +} + +Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor){ + Checkpoint checkpoint; + checkpoint.id = data.id; + if (data.start){ + checkpoint.events.push_back(from_save_event(*data.start)); + } + for (const StoryStep& step : data.steps){ + checkpoint.events.push_back(story_event(step, executor)); + } + return checkpoint; +} + + +namespace{ + +// What a step of its kind must have, or "" if it has it. +std::string missing_field(const StoryStep& step){ + switch (step.kind){ + case StoryStepKind::ROUTE: + case StoryStepKind::READY: + return step.target ? "" : "no target"; + case StoryStepKind::LEGS: + if (step.legs.empty()){ + return "no legs"; + } + for (const StoryLeg& leg : step.legs){ + if (leg.tiles <= 0){ + return "a leg of no tiles"; + } + } + return ""; + case StoryStepKind::TRAIN: + return step.battles > 0 ? "" : "no battles"; + case StoryStepKind::SHOP: + return step.items.empty() ? "no items" : ""; + case StoryStepKind::GAME: + return step.variant.empty() ? "no variant" : ""; + default: + return ""; + } +} + +} + +std::vector lint_story_checkpoint(const StoryCheckpoint& data){ + std::vector problems; + if (data.id.empty()){ + problems.emplace_back("a checkpoint has no id"); + } + std::set names; + for (size_t c = 0; c < data.steps.size(); c++){ + const StoryStep& step = data.steps[c]; + const std::string where = std::format("{} step {}", data.id, c + 1); + if (step.name.empty()){ + problems.emplace_back(where + ": no name"); + }else if (!names.insert(step.name).second){ + problems.emplace_back(std::format("{}: a second step named {}", where, step.name)); + } + const std::string missing = missing_field(step); + if (!missing.empty()){ + problems.emplace_back(std::format("{} {}: {} {}", where, step.name, story_step_kind_name(step.kind), missing)); + } + } + return problems; +} + + +Pose tile_pose(std::string map, std::vector tiles, std::optional facing){ + Pose pose; + pose.map = std::move(map); + pose.certainty = tiles.empty() ? PoseCertainty::UNKNOWN + : tiles.size() == 1 ? PoseCertainty::TILE : PoseCertainty::SET; + pose.where = std::make_shared(std::move(tiles), facing); + return pose; +} + +std::optional facing_of(const Pose& pose){ + const TilePose* tile_pose = dynamic_cast(pose.where.get()); + return tile_pose ? tile_pose->facing : std::nullopt; +} + + + +} +} diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h new file mode 100644 index 0000000000..6e74929c8e --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h @@ -0,0 +1,180 @@ +/* Story Steps + * + * From: https://github.com/PokemonAutomation/ + * + * One vocabulary of story steps for the tile games (FRLG, BDSP): a route, + * a fixed walk, a step onto a trigger, a talk, a YES/NO, a scene, a + * battle, the nurse, a shop, a save, and so on. A checkpoint is a list of + * such steps, plain data, so a game's story is a table its unit tests can + * plan without a console. + * + * build_story_checkpoint() turns a table row into engine events + * (Pokemon_StoryEvents.h). Each event's action asks the game's + * StoryStepExecutor to do its step: one virtual per kind, so a game + * implements the kinds it uses, and a step the game has no way of doing + * fails by name instead of doing something else. + * + * Poses are the game's own (Pose, with a TilePose for these games): a + * checkpoint's start, a scene's end and a route's target are poses, and + * an executor that knows where a step left the player says so in its + * Outcome, which the engine writes into the run. + * + */ + +#ifndef PokemonAutomation_Pokemon_StorySteps_H +#define PokemonAutomation_Pokemon_StorySteps_H + +#include +#include +#include +#include +#include "Pokemon_Grid.h" +#include "Pokemon_StoryEvents.h" +#include "Pokemon_TilePose.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +enum class StoryStepKind{ + NEW_GAME, // title screen (no save) to the first room + OPTIONS, // the game's options as the story needs them + ROUTE, // plan and walk to `target` + LEGS, // walk `legs` as given + STEP, // one step `direction`: onto a trigger, into a door + TALK, // A at what the player faces + ANSWER, // text to a YES/NO, then `yes` + MASH, // text until the overworld is back: a scene + BATTLE, // text into a battle, fight it, text to the overworld + TRAIN, // fight `battles` wild battles where the player stands + HEAL, // the nurse, from in front of her or of the Center's door + SHOP, // `items` from a shop + SAVE, // save the game where the player stands + READY, // before a big battle: items, and the nurse at `target` if they can't + GAME, // a step only this game has, named by `variant` +}; +const char* story_step_kind_name(StoryStepKind kind); + + +// A straight leg of a fixed walk. +struct StoryLeg{ + Direction direction = Direction::NORTH; + int tiles = 0; +}; + +// An item and how many to get. +struct StoryItem{ + std::string item; + size_t quantity = 0; +}; + +struct StoryStep{ + StoryStepKind kind = StoryStepKind::TALK; + std::string name; + std::string why; + + // ROUTE: where to go; with several tiles, any of them. READY: the nurse. + std::optional target; + // STEP + Direction direction = Direction::NORTH; + // LEGS + std::vector legs; + // ROUTE, LEGS: fight wild battles on the way instead of running. + bool fight_wild = false; + // ANSWER + bool yes = true; + // BATTLE: losing goes on (a rival's first battle). + bool may_lose = false; + // TRAIN + size_t battles = 0; + // SHOP + std::vector items; + // How this game does the kind, when it has more than one way: a + // route's plan costs, a door lined up by sight, the Mart lady instead + // of the clerk. Empty is the game's usual way. GAME: the step's name + // in the game's executor. + std::string variant; + + // Skipped when all of these hold (and there is at least one): names + // the game's executor answers (StoryStepExecutor::holds()). + std::vector skip_if; + // Where the step leaves the player, when its action does not say: the + // end of a scene that moves the player. + std::optional pose_after; +}; + +struct StoryCheckpoint{ + std::string id; + std::string title; + // Where its starting save leaves the player; none for a new game. + std::optional start; + std::vector steps; +}; + + +// How a game does each kind of step. Every kind fails as unsupported +// unless the game overrides it. +class StoryStepExecutor{ +public: + virtual ~StoryStepExecutor() = default; + + // Runs `step`: dispatch_story_step() by default. A game overrides it + // to wrap every step, e.g. to turn a failed console step into a failed + // Outcome. + virtual Outcome run(RunState& state, const StoryStep& step); + + // Whether the condition named in a step's skip_if holds. + virtual bool holds(const std::string& condition, RunState& state) = 0; + + virtual Outcome new_game(RunState& state, const StoryStep& step); + virtual Outcome options(RunState& state, const StoryStep& step); + virtual Outcome route(RunState& state, const StoryStep& step); + virtual Outcome legs(RunState& state, const StoryStep& step); + virtual Outcome step(RunState& state, const StoryStep& step); + virtual Outcome talk(RunState& state, const StoryStep& step); + virtual Outcome answer(RunState& state, const StoryStep& step); + virtual Outcome mash(RunState& state, const StoryStep& step); + virtual Outcome battle(RunState& state, const StoryStep& step); + virtual Outcome train(RunState& state, const StoryStep& step); + virtual Outcome heal(RunState& state, const StoryStep& step); + virtual Outcome shop(RunState& state, const StoryStep& step); + virtual Outcome save(RunState& state, const StoryStep& step); + virtual Outcome ready(RunState& state, const StoryStep& step); + virtual Outcome game(RunState& state, const StoryStep& step); +}; + +// Calls the executor's method for the step's kind. +Outcome dispatch_story_step(StoryStepExecutor& executor, RunState& state, const StoryStep& step); + +// "unsupported_step: TRAIN train_in_grass": what an executor without the kind returns. +Outcome unsupported_step(const StoryStep& step); + + +// The step as an event that runs it with `executor`. Both must outlive +// the event. +Event story_event(const StoryStep& step, StoryStepExecutor& executor); + +// The checkpoint `data` as events: "from_save" first when it has a start +// (the start pose, for a run that reloaded its save), then one event per +// step. `data` and `executor` must outlive the checkpoint. Saving is the +// game's: it sets the checkpoint's save, or adds a SAVE step. +Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor); + +// Problems an author should fix before running: an empty id, unnamed or +// repeated steps, a route or READY without a target, empty legs, a GAME +// step without a variant, TRAIN without battles, SHOP without items. +std::vector lint_story_checkpoint(const StoryCheckpoint& data); + + +// A pose on one of `tiles` of `map`, facing `facing` if set: TILE for one +// tile, SET for several, UNKNOWN for none. +Pose tile_pose(std::string map, std::vector tiles, std::optional facing = std::nullopt); + +// The facing of a tile pose; nothing for another pose or none. +std::optional facing_of(const Pose& pose); + + + +} +} +#endif diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp new file mode 100644 index 0000000000..948efe3911 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp @@ -0,0 +1,369 @@ +/* Story Steps Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Story tables built into checkpoints and run against a fake executor + * that writes down each step it is asked to do: the order of the events, + * skipped steps, the pose each step leaves, and how a failure is named. + * Nothing here belongs to one game. + * + */ + +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon_StoryEvents.h" +#include "Pokemon_StorySegments.h" +#include "Pokemon_StorySteps.h" +#include "Pokemon_TilePose.h" +#include "Pokemon_Navigation_Tests.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +namespace{ + +// A world of two maps. Routes arrive on their target; a step moves a tile +// from the run's pose. Each step it does is written down as "KIND name", +// with the pose it started from when that is known. +class FakeExecutor : public StoryStepExecutor{ +public: + std::vector trace; + std::set conditions; + // Steps by name that fail, and how. + std::set failing; + // The pose each step started from, by name. + std::vector> poses_seen; + + virtual Outcome run(RunState& state, const StoryStep& step) override{ + poses_seen.emplace_back(step.name, to_string(state.pose)); + trace.push_back(std::string(story_step_kind_name(step.kind)) + " " + step.name); + if (failing.contains(step.name)){ + return outcome_failed("blocked", step.name + " was blocked"); + } + return StoryStepExecutor::run(state, step); + } + virtual bool holds(const std::string& condition, RunState&) override{ + return conditions.contains(condition); + } + virtual Outcome route(RunState&, const StoryStep& step) override{ + return outcome_ok(*step.target); + } + virtual Outcome step(RunState& state, const StoryStep& step) override{ + const std::vector tiles = tiles_of(state.pose); + if (tiles.size() != 1){ + return outcome_failed("no_pose", to_string(state.pose)); + } + return outcome_ok(tile_pose(state.pose.map, {tiles[0].next(step.direction)}, step.direction)); + } + virtual Outcome talk(RunState&, const StoryStep&) override{ + return outcome_ok(); + } + virtual Outcome mash(RunState&, const StoryStep&) override{ + return outcome_ok(); + } + virtual Outcome battle(RunState&, const StoryStep& step) override{ + // The first battle is lost; a step that may lose goes on. + return step.may_lose ? outcome_ok() : outcome_failed("blackout"); + } + virtual Outcome shop(RunState&, const StoryStep& step) override{ + for (const StoryItem& item : step.items){ + trace.push_back(" bought " + std::to_string(item.quantity) + " " + item.item); + } + return outcome_ok(); + } + virtual Outcome game(RunState&, const StoryStep& step) override{ + trace.push_back(" game " + step.variant); + return outcome_ok(); + } +}; + +StoryStep make(StoryStepKind kind, std::string name){ + StoryStep step; + step.kind = kind; + step.name = std::move(name); + return step; +} + +StoryStep route_to(std::string name, Pose target){ + StoryStep step = make(StoryStepKind::ROUTE, std::move(name)); + step.target = std::move(target); + return step; +} + +StoryStep step_into(std::string name, Direction direction){ + StoryStep step = make(StoryStepKind::STEP, std::move(name)); + step.direction = direction; + return step; +} + +StoryStep scene(std::string name, std::optional pose_after){ + StoryStep step = make(StoryStepKind::MASH, std::move(name)); + step.pose_after = std::move(pose_after); + return step; +} + +// A lab checkpoint: to the table, a trigger, the scene into the lab, a +// talk, a shop, a game step. +StoryCheckpoint lab_checkpoint(){ + StoryStep shop = make(StoryStepKind::SHOP, "potions"); + shop.items = {{.item = "potion", .quantity = 5}, {.item = "antidote", .quantity = 3}}; + StoryStep game = make(StoryStepKind::GAME, "dance"); + game.variant = "dance"; + return StoryCheckpoint{ + .id = "00.03", + .title = "to the lab", + .start = tile_pose("Town", {{6, 8}}, Direction::SOUTH), + .steps = { + route_to("below_trigger", tile_pose("Town", {{12, 2}}, Direction::NORTH)), + step_into("trigger", Direction::NORTH), + scene("to_the_lab", tile_pose("Lab", {{6, 4}}, Direction::NORTH)), + make(StoryStepKind::TALK, "talk_oak"), + shop, + game, + }, + }; +} + +struct Run{ + CheckpointReport report; + RunState state; + std::vector lines; +}; + +Run run(const StoryCheckpoint& data, StoryStepExecutor& executor){ + Run ret; + Checkpoint checkpoint = build_story_checkpoint(data, executor); + EventLogger log = [&ret](const std::string& line){ ret.lines.push_back(line); }; + ret.report = run_checkpoint(ret.state, checkpoint, log); + return ret; +} + +std::string joined(const std::vector& lines){ + std::string text; + for (const std::string& line : lines){ + text += "\n " + line; + } + return text; +} + + +UnitTestResult test_events_in_table_order(){ + const StoryCheckpoint data = lab_checkpoint(); + FakeExecutor executor; + const Run result = run(data, executor); + const std::vector expected{ + "ROUTE below_trigger", + "STEP trigger", + "MASH to_the_lab", + "TALK talk_oak", + "SHOP potions", + " bought 5 potion", + " bought 3 antidote", + "GAME dance", + " game dance", + }; + if (!result.report.completed || executor.trace != expected){ + return "Expected the steps in table order:" + joined(expected) + "\nbut ran:" + joined(executor.trace); + } + // "from_save" is the engine's own event: the executor is not asked. + const std::string first = event_log_line("00.03", 1, "from_save", "start"); + if (result.lines.empty() || std::find(result.lines.begin(), result.lines.end(), first) == result.lines.end()){ + return "Expected the from_save event first:" + joined(result.lines); + } + return true; +} + +UnitTestResult test_skip_if(){ + StoryCheckpoint data = lab_checkpoint(); + data.steps[0].skip_if = {"has_parcel"}; + data.steps[3].skip_if = {"has_parcel", "talked"}; + FakeExecutor executor; + executor.conditions = {"has_parcel"}; + // The route is skipped; the talk needs both conditions, so it runs. + const Run result = run(data, executor); + if (!result.report.completed || result.report.events_skipped != 1){ + return "Expected one step skipped, got " + std::to_string(result.report.events_skipped) + ":" + joined(result.lines); + } + if (std::find(executor.trace.begin(), executor.trace.end(), "ROUTE below_trigger") != executor.trace.end()){ + return "Expected the route skipped:" + joined(executor.trace); + } + if (std::find(executor.trace.begin(), executor.trace.end(), "TALK talk_oak") == executor.trace.end()){ + return "Expected the talk run, one of its conditions not holding:" + joined(executor.trace); + } + return true; +} + +UnitTestResult test_pose_propagation(){ + const StoryCheckpoint data = lab_checkpoint(); + FakeExecutor executor; + const Run result = run(data, executor); + // Each step starts where the one before left the player: the save, the + // route's target, a tile north of it, the scene's end, and the scene's + // end again after a talk that says nothing of where it leaves us. + const std::vector> expected{ + {"below_trigger", "Town (6, 8) facing south (tile)"}, + {"trigger", "Town (12, 2) facing north (tile)"}, + {"to_the_lab", "Town (12, 1) facing north (tile)"}, + {"talk_oak", "Lab (6, 4) facing north (tile)"}, + {"potions", "Lab (6, 4) facing north (tile)"}, + {"dance", "Lab (6, 4) facing north (tile)"}, + }; + if (!result.report.completed || executor.poses_seen != expected){ + std::string text; + for (const auto& item : executor.poses_seen){ + text += "\n " + item.first + ": " + item.second; + } + return "Unexpected poses at the steps' starts:" + text; + } + if (to_string(result.state.pose) != "Lab (6, 4) facing north (tile)"){ + return "Expected the run to end at the scene's end, got " + to_string(result.state.pose); + } + return true; +} + +UnitTestResult test_failure_names_the_step(){ + const StoryCheckpoint data = lab_checkpoint(); + FakeExecutor executor; + executor.failing = {"trigger"}; + const Run result = run(data, executor); + // Event 3: from_save is event 1. + if (result.report.completed || result.report.failed_event != 3 || result.report.failed_name != "trigger" + || result.report.failure.failure != "blocked" + ){ + return "Expected a failure at event 3 trigger, blocked:" + joined(result.lines); + } + if (executor.trace.size() != 2){ + return "Expected nothing run after the failed step:" + joined(executor.trace); + } + return true; +} + +UnitTestResult test_unsupported_and_lost_battles(){ + // The fake has no TRAIN, and loses battles a step may not lose. + StoryStep train = make(StoryStepKind::TRAIN, "train"); + train.battles = 3; + FakeExecutor executor; + Run result = run(StoryCheckpoint{.id = "01", .title = "", .start = std::nullopt, .steps = {train}}, executor); + if (result.report.completed || result.report.failure.failure != "unsupported_step" || result.report.failure.detail != "TRAIN train"){ + return "Expected TRAIN unsupported:" + joined(result.lines); + } + StoryStep rival = make(StoryStepKind::BATTLE, "rival"); + rival.may_lose = true; + StoryStep brock = make(StoryStepKind::BATTLE, "brock"); + result = run(StoryCheckpoint{.id = "02", .title = "", .start = std::nullopt, .steps = {rival, brock}}, executor); + if (result.report.completed || result.report.failed_name != "brock" || result.report.failure.failure != "blackout"){ + return "Expected the rival's battle passed and Brock's lost:" + joined(result.lines); + } + return true; +} + +UnitTestResult test_segment_of_tables(){ + // Two rows built into a segment, run with saves and a reload from the + // game's hooks: the second row's first attempt fails, its retry starts + // over from its start pose. + const std::vector tables{ + lab_checkpoint(), + StoryCheckpoint{ + .id = "00.04", + .title = "the parcel", + .start = tile_pose("Lab", {{6, 4}}, Direction::NORTH), + .steps = {make(StoryStepKind::TALK, "give_parcel")}, + }, + }; + FakeExecutor executor; + Segment segment; + segment.id = "00"; + for (const StoryCheckpoint& data : tables){ + segment.checkpoints.push_back(CheckpointSpec{ + .id = data.id, .title = data.title, .saves = true, + .build = [&data, &executor]{ return build_story_checkpoint(data, executor); }, + }); + } + std::vector lines; + SegmentHooks hooks; + hooks.save = [&executor](RunState&){ + executor.trace.push_back("save"); + return outcome_ok(); + }; + // Whatever blocked the talk has gone by the reload. + hooks.reload = [&executor](RunState&){ + executor.trace.push_back("reload"); + executor.failing.clear(); + return outcome_ok(); + }; + hooks.log = [&lines](const std::string& line){ lines.push_back(line); }; + executor.failing = {"give_parcel"}; + RunState state; + const SegmentReport report = run_segment(state, segment, SegmentRange{}, SegmentPolicy{}, hooks); + const std::vector tail{"save", "TALK give_parcel", "reload", "TALK give_parcel", "save"}; + const bool tail_matches = executor.trace.size() >= tail.size() + && std::equal(tail.begin(), tail.end(), executor.trace.end() - (std::ptrdiff_t)tail.size()); + if (!report.completed || report.reloads != 1 || !tail_matches){ + return "Expected 00.04 reloaded once and passed:" + joined(executor.trace) + joined(lines); + } + return true; +} + +UnitTestResult test_lint(){ + StoryStep no_target = make(StoryStepKind::ROUTE, "walk"); + StoryStep again = make(StoryStepKind::TALK, "walk"); + StoryStep bad_legs = make(StoryStepKind::LEGS, "legs"); + bad_legs.legs = {{.direction = Direction::NORTH, .tiles = 0}}; + StoryStep no_variant = make(StoryStepKind::GAME, ""); + const StoryCheckpoint data{.id = "", .title = "", .start = std::nullopt, .steps = {no_target, again, bad_legs, no_variant}}; + const std::vector expected{ + "a checkpoint has no id", + " step 1 walk: ROUTE no target", + " step 2: a second step named walk", + " step 3 legs: LEGS a leg of no tiles", + " step 4: no name", + " step 4 : GAME no variant", + }; + const std::vector problems = lint_story_checkpoint(data); + if (problems != expected){ + return "Unexpected lint:" + joined(problems); + } + if (!lint_story_checkpoint(lab_checkpoint()).empty()){ + return "Expected the lab checkpoint to lint clean:" + joined(lint_story_checkpoint(lab_checkpoint())); + } + return true; +} + + +class Test_StorySteps : public UnitTest{ +public: + Test_StorySteps(std::string name, std::function body) + : UnitTest("Pokemon::StorySteps - " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + return m_body(); + } + +private: + std::function m_body; +}; + +} + + +void add_tests_StorySteps(UnitTestDatabase& database){ + database.add("steps run in table order", test_events_in_table_order); + database.add("skip_if asks the executor", test_skip_if); + database.add("each step starts where the last left the player", test_pose_propagation); + database.add("a failure names its step", test_failure_names_the_step); + database.add("unsupported kinds and lost battles", test_unsupported_and_lost_battles); + database.add("tables as a segment, with a reload", test_segment_of_tables); + database.add("lint", test_lint); +} + + + +} +} diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 05657bdfcc..6374b239a2 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1456,6 +1456,9 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Navigation/Pokemon_StorySegments.cpp Source/Pokemon/Navigation/Pokemon_StorySegments.h Source/Pokemon/Navigation/Pokemon_StorySegments_Tests.cpp + Source/Pokemon/Navigation/Pokemon_StorySteps.cpp + Source/Pokemon/Navigation/Pokemon_StorySteps.h + Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp Source/Pokemon/Navigation/Pokemon_TilePose.cpp Source/Pokemon/Navigation/Pokemon_TilePose.h Source/Pokemon/Options/Pokemon_BoxSortingTable.cpp From 474c2a013fe83abc1fc8146f721bcfbac7ae15b1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 10:01:14 -0800 Subject: [PATCH 02/13] Pokemon navigation: one retry policy for every story segment SegmentPolicy is now where retries are set: attempts per checkpoint (default 3), stop at the first failure (no reload; the game stays where it failed), and in-place recoveries per event (SegmentHooks::recover, then the event again) before a checkpoint is reloaded, default 0. run_segment reports each checkpoint's attempts and recoveries and logs "[Checkpoint] 00.07: passed on attempt 2 (1 reset)" and "[Segment] 00: 2 of 3 checkpoints one-shot", so the one-shot rate can be tracked. StoryRunOptions is the program-option group both AutoStory programs embed: "Attempts per checkpoint" and "Stop at the first failure". A story step's skip_if condition is a run flag unless the game overrides holds(). Tests: the policy against the fake executor (attempts then fail, stop at first failure, recover in place, one-shot report). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Navigation/Pokemon_StorySegments.cpp | 69 +++++++- .../Navigation/Pokemon_StorySegments.h | 37 ++++- .../Pokemon/Navigation/Pokemon_StorySteps.cpp | 4 + .../Pokemon/Navigation/Pokemon_StorySteps.h | 8 +- .../Navigation/Pokemon_StorySteps_Tests.cpp | 147 +++++++++++++++++- .../Options/Pokemon_StoryRunOptions.cpp | 40 +++++ .../Pokemon/Options/Pokemon_StoryRunOptions.h | 41 +++++ SerialPrograms/cmake/SourceFiles.cmake | 2 + 8 files changed, 336 insertions(+), 12 deletions(-) create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.cpp index e80ff4915f..ad072e3411 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.cpp @@ -83,21 +83,67 @@ Outcome reload(RunState& state, SegmentHooks& hooks){ return outcome_ok(); } +// The policy's in-place recovery, after each event's own: the game's +// recover hook, if any, then the event again. Counts into `used`. +void add_policy_recoveries(Checkpoint& checkpoint, const SegmentPolicy& policy, SegmentHooks& hooks, size_t& used){ + if (policy.recoveries_per_event == 0){ + return; + } + for (Event& event : checkpoint.events){ + Recovery recovery; + recovery.kind = RecoveryKind::REANCHOR; + recovery.attempts = policy.recoveries_per_event; + recovery.why = "the segment's policy: recover in place before reloading"; + recovery.prepare = [&hooks, &used, name = event.name](RunState& state){ + used++; + return hooks.recover ? hooks.recover(state, name) : outcome_ok(); + }; + event.recoveries.push_back(std::move(recovery)); + } +} + +std::string resets_text(size_t attempts){ + if (attempts == 1){ + return "one-shot"; + } + return std::format("{} reset{}", attempts - 1, attempts == 2 ? "" : "s"); +} + // One row to success, reloading and rebuilding after each failed attempt. -// Returns the last attempt's report, or the reload's failure. +// Returns the last attempt's report, or the reload's failure; `run` says +// how many attempts and recoveries it took. CheckpointReport run_row( RunState& state, const Segment& segment, CheckpointSpec& spec, - const SegmentPolicy& policy, SegmentHooks& hooks, SegmentReport& report + const SegmentPolicy& policy, SegmentHooks& hooks, SegmentReport& report, CheckpointRun& run ){ for (size_t attempt = 1; ; attempt++){ + run.attempts = attempt; Checkpoint checkpoint = spec.build(); if (spec.saves && !checkpoint.save){ checkpoint.save = [&hooks](RunState& state){ return hooks.save ? hooks.save(state) : outcome_failed("no_save_hook"); }; } + add_policy_recoveries(checkpoint, policy, hooks, run.recoveries); CheckpointReport result = run_checkpoint(state, checkpoint, hooks.log); - if (result.completed || attempt >= policy.attempts_per_checkpoint){ + if (result.completed){ + run.passed = true; + hooks.log(std::format( + "[Checkpoint] {}: passed on attempt {} ({}){}", + spec.id, attempt, resets_text(attempt), + run.recoveries == 0 ? "" : std::format(", {} in place", run.recoveries) + )); + return result; + } + if (policy.stop_at_first_failure){ + hooks.log(std::format( + "[Segment] {}: {} failed; stopping at the first failure, the game stays where it failed", + segment.id, spec.id + )); + report.stopped_at_first_failure = true; + return result; + } + if (attempt >= policy.attempts_per_checkpoint){ return result; } hooks.log(std::format( @@ -128,6 +174,19 @@ SegmentReport stop_segment(SegmentReport& report, SegmentHooks& hooks, const std } +bool CheckpointRun::one_shot() const{ + return passed && attempts == 1 && recoveries == 0; +} + +size_t SegmentReport::one_shot_passes() const{ + size_t count = 0; + for (const CheckpointRun& run : runs){ + count += run.one_shot() ? 1 : 0; + } + return count; +} + + SegmentReport run_segment( RunState& state, Segment& segment, const SegmentRange& range, const SegmentPolicy& policy, SegmentHooks& hooks @@ -141,7 +200,8 @@ SegmentReport run_segment( for (size_t c = selected.first; c <= selected.last; c++){ CheckpointSpec& spec = segment.checkpoints[c]; report.checkpoints_run++; - CheckpointReport result = run_row(state, segment, spec, policy, hooks, report); + report.runs.push_back(CheckpointRun{.id = spec.id}); + CheckpointReport result = run_row(state, segment, spec, policy, hooks, report, report.runs.back()); if (!result.completed){ report.failed_checkpoint = spec.id; report.failed_event = result.failed_name; @@ -149,6 +209,7 @@ SegmentReport run_segment( } } report.completed = true; + hooks.log(std::format("[Segment] {}: {} of {} checkpoints one-shot", segment.id, report.one_shot_passes(), report.checkpoints_run)); hooks.log(std::format("[Segment] {}: done, {} checkpoints", segment.id, report.checkpoints_run)); return report; } diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.h index 69e181ef57..5e6bf66e55 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySegments.h @@ -46,15 +46,28 @@ struct Segment{ // What the game lends the runner: how to save, how to reload the last save // (the run state is emptied first; the reload's Outcome says where the // player stands), and where to log. +// `recover`, if set, is the game's way of recovering in place from a failed +// event (re-anchor on a landmark, close a stray dialogue) before the event +// is tried again; see SegmentPolicy::recoveries_per_event. struct SegmentHooks{ Act save; Act reload; EventLogger log; + MoveOnlyFunction recover; }; +// How hard a segment tries, for every game: the one place retries are set. struct SegmentPolicy{ - // Runs of one checkpoint, reloading between them, before the segment stops. + // Runs of one checkpoint, reloading its save between them, before the + // segment stops. size_t attempts_per_checkpoint = 3; + // Lab: stop at the first failed attempt without reloading, leaving the + // game where it failed for a look. + bool stop_at_first_failure = false; + // Times each event may recover in place (SegmentHooks::recover, then + // the event again) once its own recoveries are used up, before its + // checkpoint fails and is reloaded. 0 keeps a failure a reload. + size_t recoveries_per_event = 0; }; // The checkpoints to run, by id, both inclusive. An unset end is the @@ -65,8 +78,22 @@ struct SegmentRange{ }; +// How one checkpoint went: one attempt with no in-place recovery is a +// one-shot pass. +struct CheckpointRun{ + std::string id; + bool passed = false; + size_t attempts = 0; + // In-place recoveries (SegmentPolicy::recoveries_per_event) used, in all attempts. + size_t recoveries = 0; + + bool one_shot() const; +}; + struct SegmentReport{ bool completed = false; + // The run stopped at its first failure (SegmentPolicy::stop_at_first_failure). + bool stopped_at_first_failure = false; // Both empty when nothing failed; the event is empty when a start // check, an end check, a save or a reload failed. std::string failed_checkpoint; @@ -75,13 +102,21 @@ struct SegmentReport{ size_t checkpoints_run = 0; // Reloads after failed attempts. size_t reloads = 0; + // Each checkpoint run, in order, the failed one last. + std::vector runs; + + size_t one_shot_passes() const; }; // Runs the checkpoints in `range`, in order. Logs through `hooks.log`, // around the checkpoint lines: // [Segment] 00: start, 3 checkpoints // [Segment] 00: 00.02 failed, reloading for attempt 2/3 +// [Checkpoint] 00.02: passed on attempt 2 (1 reset) +// [Checkpoint] 00.03: passed on attempt 1 (one-shot) +// [Segment] 00: 2 of 3 checkpoints one-shot // [Segment] 00: done, 3 checkpoints +// [Segment] 00: 00.02 failed; stopping at the first failure, the game stays where it failed // [Segment] 00: failed at 00.02 talk_mom: expect_failed: dialogue opened SegmentReport run_segment( RunState& state, Segment& segment, const SegmentRange& range, diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp index 406e45e36f..b0ecdd4514 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp @@ -59,6 +59,10 @@ Outcome StoryStepExecutor::run(RunState& state, const StoryStep& step){ return dispatch_story_step(*this, state, step); } +bool StoryStepExecutor::holds(const std::string& condition, RunState& state){ + return state.flags.contains(condition); +} + Outcome StoryStepExecutor::new_game(RunState&, const StoryStep& step){ return unsupported_step(step); } diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h index 6e74929c8e..41fe51fb77 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h @@ -96,7 +96,8 @@ struct StoryStep{ std::string variant; // Skipped when all of these hold (and there is at least one): names - // the game's executor answers (StoryStepExecutor::holds()). + // the game's executor answers (StoryStepExecutor::holds()), run flags + // unless the game reads them off the screen. std::vector skip_if; // Where the step leaves the player, when its action does not say: the // end of a scene that moves the player. @@ -123,8 +124,9 @@ class StoryStepExecutor{ // Outcome. virtual Outcome run(RunState& state, const StoryStep& step); - // Whether the condition named in a step's skip_if holds. - virtual bool holds(const std::string& condition, RunState& state) = 0; + // Whether the condition named in a step's skip_if holds: by default, + // whether the run has the flag (set by an earlier step's action). + virtual bool holds(const std::string& condition, RunState& state); virtual Outcome new_game(RunState& state, const StoryStep& step); virtual Outcome options(RunState& state, const StoryStep& step); diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp index 948efe3911..fe0cc60623 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -35,8 +36,10 @@ class FakeExecutor : public StoryStepExecutor{ public: std::vector trace; std::set conditions; - // Steps by name that fail, and how. + // Steps by name that always fail, and how. std::set failing; + // Steps by name that fail this many more times, then pass. + std::map failures_left; // The pose each step started from, by name. std::vector> poses_seen; @@ -46,10 +49,15 @@ class FakeExecutor : public StoryStepExecutor{ if (failing.contains(step.name)){ return outcome_failed("blocked", step.name + " was blocked"); } + size_t& left = failures_left[step.name]; + if (left > 0){ + left--; + return outcome_failed("blocked", step.name + " was blocked"); + } return StoryStepExecutor::run(state, step); } - virtual bool holds(const std::string& condition, RunState&) override{ - return conditions.contains(condition); + virtual bool holds(const std::string& condition, RunState& state) override{ + return conditions.contains(condition) || StoryStepExecutor::holds(condition, state); } virtual Outcome route(RunState&, const StoryStep& step) override{ return outcome_ok(*step.target); @@ -77,8 +85,10 @@ class FakeExecutor : public StoryStepExecutor{ } return outcome_ok(); } - virtual Outcome game(RunState&, const StoryStep& step) override{ + // A game step sets a run flag named for its variant. + virtual Outcome game(RunState& state, const StoryStep& step) override{ trace.push_back(" game " + step.variant); + state.flags.insert(step.variant); return outcome_ok(); } }; @@ -196,6 +206,18 @@ UnitTestResult test_skip_if(){ if (std::find(executor.trace.begin(), executor.trace.end(), "TALK talk_oak") == executor.trace.end()){ return "Expected the talk run, one of its conditions not holding:" + joined(executor.trace); } + + // By default a condition is a run flag: the game step "dance" sets + // one, and a talk after it that needs it is skipped. + StoryStep after = make(StoryStepKind::TALK, "talk_after_dance"); + after.skip_if = {"dance"}; + data = lab_checkpoint(); + data.steps.push_back(after); + FakeExecutor plain; + const Run flagged = run(data, plain); + if (!flagged.report.completed || flagged.report.events_skipped != 1 || plain.trace.back() != " game dance"){ + return "Expected the talk after the dance skipped on its flag:" + joined(plain.trace); + } return true; } @@ -307,6 +329,120 @@ UnitTestResult test_segment_of_tables(){ if (!report.completed || report.reloads != 1 || !tail_matches){ return "Expected 00.04 reloaded once and passed:" + joined(executor.trace) + joined(lines); } + // The report and the log tell a one-shot pass from a retried one. + const bool runs_ok = report.runs.size() == 2 + && report.runs[0].one_shot() && report.runs[0].attempts == 1 + && !report.runs[1].one_shot() && report.runs[1].attempts == 2 && report.runs[1].passed + && report.one_shot_passes() == 1; + const auto has = [&lines](const std::string& line){ + return std::find(lines.begin(), lines.end(), line) != lines.end(); + }; + if (!runs_ok + || !has("[Checkpoint] 00.03: passed on attempt 1 (one-shot)") + || !has("[Checkpoint] 00.04: passed on attempt 2 (1 reset)") + || !has("[Segment] 00: 1 of 2 checkpoints one-shot") + ){ + return "Expected 00.03 one-shot and 00.04 passed on attempt 2:" + joined(lines); + } + return true; +} + + +// The two tables of test_segment_of_tables() as a segment run by `executor`, +// with hooks that write saves, reloads and recoveries into its trace. +struct PolicyLab{ + std::vector tables{ + lab_checkpoint(), + StoryCheckpoint{ + .id = "00.04", + .title = "the parcel", + .start = tile_pose("Lab", {{6, 4}}, Direction::NORTH), + .steps = {make(StoryStepKind::TALK, "give_parcel")}, + }, + }; + FakeExecutor executor; + std::vector lines; + + SegmentReport run(const SegmentPolicy& policy){ + Segment segment; + segment.id = "00"; + for (const StoryCheckpoint& data : tables){ + segment.checkpoints.push_back(CheckpointSpec{ + .id = data.id, .title = data.title, .saves = true, + .build = [&data, this]{ return build_story_checkpoint(data, executor); }, + }); + } + SegmentHooks hooks; + hooks.save = [this](RunState&){ + executor.trace.push_back("save"); + return outcome_ok(); + }; + hooks.reload = [this](RunState&){ + executor.trace.push_back("reload"); + return outcome_ok(); + }; + hooks.recover = [this](RunState&, const std::string& event){ + executor.trace.push_back("recover " + event); + return outcome_ok(); + }; + hooks.log = [this](const std::string& line){ lines.push_back(line); }; + RunState state; + return run_segment(state, segment, SegmentRange{}, policy, hooks); + } + bool has(const std::string& line) const{ + return std::find(lines.begin(), lines.end(), line) != lines.end(); + } + size_t count(const std::string& item) const{ + return (size_t)std::count(executor.trace.begin(), executor.trace.end(), item); + } +}; + +UnitTestResult test_policy_attempts_then_fail(){ + // Three attempts by default, then the segment stops on the last one's failure. + PolicyLab lab; + lab.executor.failing = {"give_parcel"}; + const SegmentReport report = lab.run(SegmentPolicy{}); + if (report.completed || report.reloads != 2 || lab.count("TALK give_parcel") != 3 + || report.runs.size() != 2 || report.runs[1].passed || report.runs[1].attempts != 3 + || report.failed_checkpoint != "00.04" || report.failure.failure != "blocked" + ){ + return "Expected 00.04 tried 3 times, then failed:" + joined(lab.executor.trace) + joined(lab.lines); + } + return true; +} + +UnitTestResult test_policy_stop_at_first_failure(){ + PolicyLab lab; + lab.executor.failing = {"give_parcel"}; + const SegmentReport report = lab.run(SegmentPolicy{.stop_at_first_failure = true}); + if (report.completed || !report.stopped_at_first_failure || report.reloads != 0 || lab.count("reload") != 0 + || lab.count("TALK give_parcel") != 1 + || !lab.has("[Segment] 00: 00.04 failed; stopping at the first failure, the game stays where it failed") + ){ + return "Expected a stop at the first failure, with no reload:" + joined(lab.executor.trace) + joined(lab.lines); + } + return true; +} + +UnitTestResult test_policy_recovers_in_place(){ + // The talk fails twice; two recoveries in place get it through + // without a reload, but not one-shot. + PolicyLab lab; + lab.executor.failures_left = {{"give_parcel", 2}}; + const SegmentReport report = lab.run(SegmentPolicy{.recoveries_per_event = 2}); + if (!report.completed || report.reloads != 0 || lab.count("recover give_parcel") != 2 + || report.runs.size() != 2 || report.runs[1].recoveries != 2 || report.runs[1].one_shot() + || !lab.has("[Checkpoint] 00.04: passed on attempt 1 (one-shot), 2 in place") + ){ + return "Expected 00.04 recovered in place twice:" + joined(lab.executor.trace) + joined(lab.lines); + } + // Past the budget, the checkpoint is reloaded as before. + PolicyLab reloaded; + reloaded.executor.failures_left = {{"give_parcel", 2}}; + const SegmentReport second = reloaded.run(SegmentPolicy{.recoveries_per_event = 1}); + if (!second.completed || second.reloads != 1 || second.runs[1].attempts != 2){ + return "Expected a reload once the in-place budget was used:" + joined(reloaded.executor.trace) + joined(reloaded.lines); + } return true; } @@ -361,6 +497,9 @@ void add_tests_StorySteps(UnitTestDatabase& database){ database.add("unsupported kinds and lost battles", test_unsupported_and_lost_battles); database.add("tables as a segment, with a reload", test_segment_of_tables); database.add("lint", test_lint); + database.add("policy: attempts per checkpoint, then fail", test_policy_attempts_then_fail); + database.add("policy: stop at the first failure", test_policy_stop_at_first_failure); + database.add("policy: recover in place before reloading", test_policy_recovers_in_place); } diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp new file mode 100644 index 0000000000..ceccb40638 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp @@ -0,0 +1,40 @@ +/* Story Run Options + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Pokemon_StoryRunOptions.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +StoryRunOptions::StoryRunOptions() + : GroupOption("Checkpoint Retries", LockMode::LOCK_WHILE_RUNNING) + , ATTEMPTS_PER_CHECKPOINT( + "Attempts per checkpoint:
A failed checkpoint reloads its save and runs again, up to this many times in all.", + LockMode::LOCK_WHILE_RUNNING, + 3, 1, 20 + ) + , STOP_AT_FIRST_FAILURE( + "Stop at the first failure:
Stop without reloading, leaving the game where it failed for a look.", + LockMode::LOCK_WHILE_RUNNING, + false + ) +{ + PA_ADD_OPTION(ATTEMPTS_PER_CHECKPOINT); + PA_ADD_OPTION(STOP_AT_FIRST_FAILURE); +} + +SegmentPolicy StoryRunOptions::policy() const{ + SegmentPolicy policy; + policy.attempts_per_checkpoint = ATTEMPTS_PER_CHECKPOINT; + policy.stop_at_first_failure = STOP_AT_FIRST_FAILURE; + return policy; +} + + + +} +} diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h new file mode 100644 index 0000000000..652ddd44ab --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -0,0 +1,41 @@ +/* Story Run Options + * + * From: https://github.com/PokemonAutomation/ + * + * The options every AutoStory program shows for how hard to try: the + * same group in each game, read into the shared SegmentPolicy + * (Pokemon/Navigation/Pokemon_StorySegments.h). + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryRunOptions_H +#define PokemonAutomation_Pokemon_StoryRunOptions_H + +#include +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/GroupOption.h" +#include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +class StoryRunOptions : public GroupOption{ +public: + StoryRunOptions(); + + // The policy these options set. In-place recoveries are the game's to + // set, not the user's. + SegmentPolicy policy() const; + +public: + SimpleIntegerOption ATTEMPTS_PER_CHECKPOINT; + BooleanCheckBoxOption STOP_AT_FIRST_FAILURE; +}; + + + +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 6374b239a2..ecf66e1e16 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1474,6 +1474,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Options/Pokemon_NameSelectWidget.h Source/Pokemon/Options/Pokemon_StatsHuntFilter.cpp Source/Pokemon/Options/Pokemon_StatsHuntFilter.h + Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp + Source/Pokemon/Options/Pokemon_StoryRunOptions.h Source/Pokemon/Pokemon_AdvRng.cpp Source/Pokemon/Pokemon_AdvRng.h Source/Pokemon/Pokemon_BdspRng.cpp From c0859dd95eab1a65a31945c8575b069223ae1904 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 10:07:08 -0800 Subject: [PATCH 03/13] BDSP AutoStory: 00.04b as story steps, and the shared retry options 00.04b (after the catch, to Dawn in Jubilife) was one function; it is now a table of the shared story steps (Data/PokemonBDSP_AutoStory_Segment00Steps): find the save, to the catch grass, train 3, into Sandgem, below the Center's sign, taps to its door, heal, Potions, back to Route 202 through the signs' gap, save, north past the trainers, the stairs, Dawn's scene. Steps 2-8 skip on the "has_potions" flag and 9-10 on "potions_saved", set by the steps that know (the Bag on resume, the Mart lady's own save). ConsoleStoryExecutor (Utils) does the kinds BDSP has (route with trainer sight and named plan costs, D-pad legs, train, heal, save, mash) and runs each step through the table's runner, so failures keep their screenshot and the "checkpoint_reattempt_loop: attempt N failed: : ..." line the lab watcher reads. Segment 00 registers its own variants (Mart lady or clerk, the signs' gap, the Jubilife stairs). The other rows stay wrapped. Retries: the lab's STOP_AT_FIRST_FAILURE option is replaced by the shared StoryRunOptions group (attempts per checkpoint, default 3; stop at the first failure). The story tables (segments 00 and 01) run with that policy instead of 20 attempts; checkpoint_reattempt_loop() for the other segments still has its 20 and follows the stop option. A new test lints 00.04b and plans each of its routes on the story maps from where the step before leaves the player, the clerk's way. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + .../PokemonBDSP_AutoStory_Segment00Steps.cpp | 157 +++++++++++ .../PokemonBDSP_AutoStory_Segment00Steps.h | 48 ++++ .../AutoStory/PokemonBDSP_AutoStory.cpp | 16 +- .../AutoStory/PokemonBDSP_AutoStory.h | 5 +- .../AutoStory/PokemonBDSP_AutoStoryTools.h | 3 + ...monBDSP_AutoStory_Segment00Steps_Tests.cpp | 190 ++++++++++++++ ...kemonBDSP_AutoStory_Segment00Steps_Tests.h | 26 ++ .../PokemonBDSP_AutoStory_Segment_00.cpp | 244 ++++++------------ .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 212 +++++++++++++++ .../PokemonBDSP_AutoStory_StoryExecutor.h | 125 +++++++++ .../PokemonBDSP_AutoStory_StoryTables.cpp | 16 +- .../Utils/PokemonBDSP_AutoStory_StoryTables.h | 3 +- SerialPrograms/cmake/SourceFiles.cmake | 6 + 14 files changed, 861 insertions(+), 192 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 258b5b7789..2a81ec1000 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -21,6 +21,7 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h" @@ -296,6 +297,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_MapGrids(database); add_tests_AutoStory(database); add_tests_AutoStory_Segment00(database); + add_tests_AutoStory_Segment00Steps(database); add_tests_AutoStory_StoryTables(database); add_tests_AutoStory_Indoors(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp new file mode 100644 index 0000000000..89231f8f7a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp @@ -0,0 +1,157 @@ +/* BDSP AutoStory - Segment 00 as story steps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonBDSP_AutoStory_Route202.h" +#include "PokemonBDSP_AutoStory_SandgemTown.h" +#include "PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP_AutoStory_Segment00Steps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; +using Pokemon::Pose; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +const char* const STEP_LOCATE_SAVE = "locate_save"; +const char* const STEP_SANDGEM_POTIONS = "sandgem_potions"; +const char* const STEP_SIGN_GAP = "sign_gap"; +const char* const STEP_JUBILIFE_STAIRS = "jubilife_stairs"; + + +namespace{ + +// Steps 2 to 8 are done once the Bag has Potions; 9 and 10 once a save +// with them stands on Route 202. +const char* const HAS_POTIONS = "has_potions"; +const char* const POTIONS_SAVED = "potions_saved"; + +Pose on(const char* map, std::vector world, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, std::move(world), facing); +} + +TilePos world(const char* map, const TilePos& local){ + return story_maps().to_world(map, local); +} + +// The catch grass the training walks to: tall grass in the training area. +std::vector catch_grass(){ + std::vector grass = story_after_training_area(); + std::erase_if(grass, [](const TilePos& pos){ + return story_maps().world().at(pos) != Pokemon::TileType::TALL_GRASS; + }); + return grass; +} + +// Route 202's open rows at the top: north from any of them is Jubilife. +std::vector jubilife_exit(){ + std::vector exit; + const Pokemon::TileGrid& grid = Route202::grid(); + for (int y = 0; y <= 3; y++){ + for (int x = 0; x < grid.width(); x++){ + const TilePos pos = world(MAP_ROUTE_202, {x, y}); + if (story_maps().world().standable(pos) && story_at_jubilife_exit(pos)){ + exit.push_back(pos); + } + } + } + return exit; +} + +StoryStep make(StoryStepKind kind, std::string name, std::string why, std::vector skip_if = {}){ + StoryStep step; + step.kind = kind; + step.name = std::move(name); + step.why = std::move(why); + step.skip_if = std::move(skip_if); + return step; +} + +StoryStep with_variant(StoryStep step, std::string variant){ + step.variant = std::move(variant); + return step; +} + +StoryStep with_target(StoryStep step, Pose target){ + step.target = std::move(target); + return step; +} + +StoryStep with_pose_after(StoryStep step, Pose pose){ + step.pose_after = std::move(pose); + return step; +} + +Pokemon::StoryCheckpoint make_after_catch(){ + const TilePos save_spot = world(MAP_ROUTE_202, Route202::SAVE_SPOT); + const TilePos below_sign = world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH)); + const int to_door = SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x; + + StoryStep train = make(StoryStepKind::TRAIN, "train", "a Lv5 starter fainted to Route 202's trainers (r72)", {HAS_POTIONS}); + train.battles = 3; + StoryStep below_door = make(StoryStepKind::LEGS, "below_center_door", + "taps west below the Center's door; the finder read the signs as doorways from below the sign (r252, r255)", {HAS_POTIONS}); + below_door.legs = {{.direction = Direction::WEST, .tiles = to_door}}; + StoryStep potions = make(StoryStepKind::SHOP, "potions", + "Potions before Route 202's trainers; the lady's 10 are free", {HAS_POTIONS}); + potions.items = {{.item = "potion", .quantity = 10}}; + StoryStep stairs = make(StoryStepKind::STEP, "into_jubilife", + "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); + stairs.direction = Direction::NORTH; + + return Pokemon::StoryCheckpoint{ + .id = "00.04b", + .title = "After the catch, to Dawn in Jubilife", + .start = on(MAP_ROUTE_202, story_catch_save_area()), + .steps = { + with_variant(make(StoryStepKind::GAME, "locate_save", "a retry may load the save made with the Potions"), STEP_LOCATE_SAVE), + with_target(make(StoryStepKind::ROUTE, "to_catch_grass", "into the catch grass to train", {HAS_POTIONS}), + on(MAP_ROUTE_202, catch_grass())), + with_pose_after(std::move(train), on(MAP_ROUTE_202, story_after_training_area())), + // One walk round the houses ended two tiles short (r252): down + // the road first, then the town plan. + with_variant(with_target(make(StoryStepKind::ROUTE, "into_sandgem", "south down the road to above the Mart", {HAS_POTIONS}), + on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP)})), "robust"), + with_variant(with_target(make(StoryStepKind::ROUTE, "to_center_sign", "below the Center's sign, its doorway in sight", {HAS_POTIONS}), + on(MAP_SANDGEM_TOWN, {below_sign}, Direction::NORTH)), "town"), + with_variant(std::move(below_door), "one_short_ok"), + with_pose_after(make(StoryStepKind::HEAL, "heal_sandgem", "Sandgem Town Pokemon Center", {HAS_POTIONS}), + on(MAP_SANDGEM_TOWN, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR))), + with_variant(std::move(potions), STEP_SANDGEM_POTIONS), + with_pose_after(with_variant(make(StoryStepKind::GAME, "back_to_route_202", + "to the save spot under the ledge, where a retry may start; the trainers are north of it", {POTIONS_SAVED}), STEP_SIGN_GAP), + on(MAP_ROUTE_202, {save_spot})), + with_target(make(StoryStepKind::SAVE, "save_route_202", + "with the Potions, at the save spot on Route 202 (r124a saved beside the Mart)", {POTIONS_SAVED}), + on(MAP_ROUTE_202, {save_spot})), + // From the one save spot: from both at once the planner gives up. + with_target(make(StoryStepKind::ROUTE, "north_to_jubilife", "past Route 202's trainers, fighting who sees us"), + on(MAP_ROUTE_202, jubilife_exit())), + with_variant(std::move(stairs), STEP_JUBILIFE_STAIRS), + make(StoryStepKind::MASH, "dawn_scene", "Dawn walks over while we stand still"), + }, + }; +} + +} + + +const Pokemon::StoryCheckpoint& segment_00_after_catch(){ + static const Pokemon::StoryCheckpoint checkpoint = make_after_catch(); + return checkpoint; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h new file mode 100644 index 0000000000..ab180cde23 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h @@ -0,0 +1,48 @@ +/* BDSP AutoStory - Segment 00 as story steps + * + * From: https://github.com/PokemonAutomation/ + * + * Segment 00's checkpoints written as the shared story steps + * (Pokemon/Navigation/Pokemon_StorySteps.h), plain data for the tests to + * plan. The first is 00.04b, the half of 00.04 after the catch; the rest + * of the segment is still one wrapped function per checkpoint. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_H + +#include "Pokemon/Navigation/Pokemon_StorySteps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The variants 00.04b's steps name, done by Segment 00's own code. +// GAME: with Potions in the Bag, the run resumes from the save after +// them: flags "has_potions" and "potions_saved", and walks back to +// Route 202's save spot if that save is in Sandgem Town. +extern const char* const STEP_LOCATE_SAVE; +// SHOP: Potions from the Poke Mart lady on Route 201, who walks us on to +// Route 202's save spot and saves there (flag "potions_saved"), or after +// two misses the clerk in Sandgem's Poke Mart. +extern const char* const STEP_SANDGEM_POTIONS; +// GAME: from Sandgem's front row north through the gap between the +// signs to Route 202's save spot. +extern const char* const STEP_SIGN_GAP; +// STEP: north into Jubilife, a tile west at a time until the stairs take +// us and Dawn's scene starts. +extern const char* const STEP_JUBILIFE_STAIRS; + +// 00.04b: from the save after the catch on Route 202 to Dawn in Jubilife: +// train, heal in Sandgem, Potions, save on Route 202, north past its +// trainers, Dawn's scene. Poses are in world tiles of story_maps(). +const Pokemon::StoryCheckpoint& segment_00_after_catch(); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index 324ae32dc2..fd8c024af3 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -142,14 +142,6 @@ BDSPAutoStory::~BDSPAutoStory(){ BDSPAutoStory::BDSPAutoStory() : SingleSwitchProgramInstance() - // DIAGNOSTIC ONLY: Remove before pushing to production. - , STOP_AT_FIRST_FAILURE( - "Lab: Stop at the first failure
" - "Save the failure screenshot and stop, leaving the game where it failed for a look, " - "instead of closing it and retrying. Remove before pushing to production.", - LockMode::LOCK_WHILE_RUNNING, - false - ) , DIAGNOSTIC_POST_CATCH_ONLY( "Diagnostic only: Run 00.04 after catch through 00.05
" "Start at the saved position immediately after catching the second Pokemon. " @@ -412,7 +404,7 @@ BDSPAutoStory::BDSPAutoStory() // Options // ----------------------------------------------------------------------- - PA_ADD_OPTION(STOP_AT_FIRST_FAILURE); + PA_ADD_OPTION(RETRIES); PA_ADD_OPTION(DIAGNOSTIC_POST_CATCH_ONLY); PA_ADD_OPTION(DIAGNOSTIC_FAKE_CHECKPOINT_SAVE); PA_ADD_OPTION(DIAGNOSTIC_ROUTE202_TO_SANDGEM_CENTER); @@ -538,7 +530,8 @@ void BDSPAutoStory::program( ProControllerContext& context ){ AutoStoryStats& stats = env.current_stats(); - g_stop_at_first_failure = STOP_AT_FIRST_FAILURE; + // The checkpoints not yet in tables still run checkpoint_reattempt_loop(). + g_stop_at_first_failure = RETRIES.STOP_AT_FIRST_FAILURE; AutoStoryOptions options{ Language::English, @@ -556,6 +549,7 @@ void BDSPAutoStory::program( }, DIAGNOSTIC_FAKE_CHECKPOINT_SAVE ? CheckpointSaveMode::FAKE : CheckpointSaveMode::REAL }; + options.policy = RETRIES.policy(); const bool fake_saves = options.checkpoint_save_mode == CheckpointSaveMode::FAKE; if (fake_saves) options.checkpoint_retry_scope = CheckpointRetryScope::DEFER_TO_SELECTED_RANGE; @@ -690,7 +684,7 @@ void BDSPAutoStory::program( run_selected_range(0); } }catch (OperationFailedException&){ - if (STOP_AT_FIRST_FAILURE){ + if (RETRIES.STOP_AT_FIRST_FAILURE){ env.log("AutoStory failed: leaving the game where it failed (lab option).", COLOR_RED); throw; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h index d42aa4717f..6677d84e2e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.h @@ -10,6 +10,7 @@ #include "CommonTools/Options/StringSelectOption.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "PokemonBDSP_AutoStoryTools.h" +#include "Pokemon/Options/Pokemon_StoryRunOptions.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -39,8 +40,8 @@ class BDSPAutoStory : public SingleSwitchProgramInstance, public ConfigOption::L virtual void on_config_value_changed(void* object) override; void update_ui_state(); private: - // DIAGNOSTIC ONLY: Remove before pushing to production. - BooleanCheckBoxOption STOP_AT_FIRST_FAILURE; + // Attempts per checkpoint and stop at the first failure, as in FRLG. + Pokemon::StoryRunOptions RETRIES; BooleanCheckBoxOption DIAGNOSTIC_POST_CATCH_ONLY; // DIAGNOSTIC ONLY: Remove before pushing to production. BooleanCheckBoxOption DIAGNOSTIC_FAKE_CHECKPOINT_SAVE; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h index 6089ee78d7..143e2b6316 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h @@ -14,6 +14,7 @@ #include #include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTools/ImageBoxes.h" @@ -139,6 +140,8 @@ struct AutoStoryOptions { OutsideHelpConfig outside_help; CheckpointSaveMode checkpoint_save_mode = CheckpointSaveMode::REAL; CheckpointRetryScope checkpoint_retry_scope = CheckpointRetryScope::CHECKPOINT; + // How the story tables retry a checkpoint (StoryRunOptions). + Pokemon::SegmentPolicy policy; }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp new file mode 100644 index 0000000000..ea23657096 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp @@ -0,0 +1,190 @@ +/* BDSP AutoStory Segment 00 Steps Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Follows 00.04b's steps from its start on the story maps (the JSON grids + * built into the program), the way a first run goes: no Potions yet, and + * the Poke Mart clerk's way, which walks back to Route 202 itself. Each + * route is planned with the costs its variant names, from the tiles the + * step before may leave us on. + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "Data/PokemonBDSP_AutoStory_Route202.h" +#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "Data/PokemonBDSP_AutoStory_Segment00Steps.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "Utils/PokemonBDSP_AutoStory_StoryExecutor.h" +#include "PokemonBDSP_AutoStory_Segment00Steps_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Direction; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + +std::string describe(const std::vector& tiles){ + std::string text; + for (const TilePos& pos : tiles){ + const std::optional local = story_maps().to_local(pos); + text += " " + (local ? local->map + " " + Pokemon::to_string(local->pos) : Pokemon::to_string(pos)); + } + return text; +} + +bool contains(const std::vector& tiles, const TilePos& pos){ + return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); +} + +// Plans `step` from `at` and moves `at` to where the plan may end. +std::string plan_route(Logger& logger, std::vector& at, const StoryStep& step){ + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + return step.name + ": no costs named \"" + step.variant + "\"."; + } + const std::vector goal = Pokemon::tiles_of(*step.target); + if (goal.empty()){ + return step.name + ": the target has no tiles."; + } + const auto plan = Pokemon::plan_robust_route(story_maps().world(), at, + [&](const TilePos& pos){ return contains(goal, pos); }, *costs); + if (!plan){ + return step.name + ": no plan from" + describe(at) + "."; + } + std::string text = std::format("00.04b {}: {} legs from{}", step.name, plan->size(), describe(at)); + for (const Pokemon::PlannedLeg& leg : *plan){ + text += "\n " + to_string(leg.leg) + " to" + describe(leg.after); + } + logger.log(text); + if (!plan->empty()){ + at = plan->back().after; + } + for (const TilePos& pos : at){ + if (!contains(goal, pos)){ + return step.name + ": the plan may end off the target, at" + describe({pos}) + "."; + } + } + return ""; +} + +// Walks `step`'s legs tile by tile from each of `at`, a tile short too +// when the step allows it. +std::string walk_legs(std::vector& at, const StoryStep& step){ + const Pokemon::TileGrid& world = story_maps().world(); + for (const Pokemon::StoryLeg& leg : step.legs){ + std::vector next; + for (TilePos pos : at){ + for (int i = 0; i < leg.tiles; i++){ + const std::optional stepped = world.step(pos, leg.direction); + if (!stepped){ + return step.name + ": " + direction_name(leg.direction) + " is blocked at" + describe({pos}) + "."; + } + if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ + next.push_back(pos); + } + pos = *stepped; + } + next.push_back(pos); + } + at = std::move(next); + } + return ""; +} + +class Test_AfterCatchSteps : public UnitTest{ +public: + Test_AfterCatchSteps() + : UnitTest("PokemonBDSP::AutoStory - 00.04b steps: lint, and every route plans on the story maps") + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return check(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + static UnitTestResult check(Logger& logger){ + const Pokemon::StoryCheckpoint& data = segment_00_after_catch(); + const std::vector problems = Pokemon::lint_story_checkpoint(data); + if (!problems.empty()){ + return problems[0]; + } + if (!data.start || Pokemon::tiles_of(*data.start).empty()){ + return "00.04b has no start tiles."; + } + const TilePos save_spot = story_maps().to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); + std::vector at = Pokemon::tiles_of(*data.start); + for (const StoryStep& step : data.steps){ + std::string error; + switch (step.kind){ + case StoryStepKind::ROUTE: + error = plan_route(logger, at, step); + break; + case StoryStepKind::LEGS: + error = walk_legs(at, step); + break; + case StoryStepKind::SHOP: + // The clerk's way: out of the Mart's door. (The lady's ends + // on the save spot, where the save step stands too.) + at = story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); + break; + case StoryStepKind::SAVE: + if (!step.target || step.target->map != MAP_ROUTE_202 || at != std::vector{save_spot}){ + error = step.name + ": expected a save on Route 202's save spot, from" + describe(at) + "."; + } + break; + default: + break; + } + if (!error.empty()){ + return error; + } + if (step.pose_after){ + at = Pokemon::tiles_of(*step.pose_after); + } + // From both save spots at once the planner gives up: the walk + // north must start from the one. + if (step.name == "back_to_route_202" && at != std::vector{save_spot}){ + return step.name + ": expected to end on the save spot."; + } + } + for (const TilePos& pos : at){ + if (!story_at_jubilife_exit(pos)){ + return "00.04b's walk north may end off Jubilife's exit, at" + describe({pos}) + "."; + } + } + return true; + } +}; + +} + + +void add_tests_AutoStory_Segment00Steps(UnitTestDatabase& database){ + database.add(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h new file mode 100644 index 0000000000..e6108e9f73 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h @@ -0,0 +1,26 @@ +/* BDSP AutoStory Segment 00 Steps Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Offline checks of 00.04b as story steps: it lints clean, and every route +// plans on the story maps from where the step before leaves the player. +void add_tests_AutoStory_Segment00Steps(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 68dd1d4d85..b17689395a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -11,11 +11,13 @@ #include "../Data/PokemonBDSP_AutoStory_JubilifeCity.h" #include "../Data/PokemonBDSP_AutoStory_Route202.h" #include "../Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "../Data/PokemonBDSP_AutoStory_Segment00Steps.h" #include "../Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "../Utils/PokemonBDSP_AutoStory_Location.h" #include "../Utils/PokemonBDSP_AutoStory_PokemonCenter.h" #include "../Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "../Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "../Utils/PokemonBDSP_AutoStory_StoryExecutor.h" #include "../Utils/PokemonBDSP_AutoStory_Walk.h" #include "../Utils/PokemonBDSP_OverworldMatchers.h" @@ -620,40 +622,6 @@ namespace PokemonAutomation { align_to_tutorial_landmarks(stream, context, match_sandgem_junction, "Sandgem junction"); } - // What the post-catch half of 00.04 does, logged up front and as each - // step starts. A Lv5 Piplup fainted to Route 202's trainers (r72), so: - // training, a heal at the Center, and Potions (they also saved the - // Barry fight in r85) before Route 202's trainers: the Poke Mart lady's - // 10 on Route 201 (she stands still, as in the decomp), the clerk's 5 after two misses. - static const char* const AFTER_CATCH_STEPS[] = { - "Train: fight 3 wild Pokemon in the catch grass, with the best move", - "Walk south into Sandgem Town and heal at its Pokemon Center", - "Get 10 Potions from the Poke Mart lady on Route 201 (the clerk's 5 after two misses), and check the Bag", - "Walk back to Route 202's save spot and save", - "Walk north up Route 202 to the Jubilife exit, fighting trainers who see us; after each, use a Potion if HP is under half", - "Enter Jubilife and get through Dawn's scene", - }; - constexpr size_t AFTER_CATCH_STEP_COUNT = sizeof(AFTER_CATCH_STEPS) / sizeof(AFTER_CATCH_STEPS[0]); - - static void log_after_catch_steps(VideoStream& stream) { - std::string text = "[00.04] After the catch, the steps are:"; - for (size_t i = 0; i < AFTER_CATCH_STEP_COUNT; i++) { - text += "\n " + std::to_string(i + 1) + ". " + AFTER_CATCH_STEPS[i]; - } - stream.log(text, COLOR_BLUE); - } - - static void after_catch_step(VideoStream& stream, size_t number) { - const std::string count = std::to_string(number) + "/" + std::to_string(AFTER_CATCH_STEP_COUNT); - std::string text = "[00.04 step " + count + "] " + AFTER_CATCH_STEPS[number - 1]; - text += number < AFTER_CATCH_STEP_COUNT ? std::string(" -- next: ") + AFTER_CATCH_STEPS[number] : std::string(" -- last step"); - stream.log(text, COLOR_BLUE); - add_wrapped_log(stream.overlay(), "00.04 step " + count + ": " + AFTER_CATCH_STEPS[number - 1], COLOR_WHITE); - if (number < AFTER_CATCH_STEP_COUNT) { - add_wrapped_log(stream.overlay(), "Next: " + std::string(AFTER_CATCH_STEPS[number]), COLOR_GRAY); - } - } - // Fights a trainer whose dialogue has opened, with whatever starter we // have. Someone who only talks leaves no battle to fight. static void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name) { @@ -674,60 +642,6 @@ namespace PokemonAutomation { use_potion_first_pokemon(stream, context, 0.5); } - // The starter starts around level 5 with only a Normal move; the Route - // 202 trainers need a level or two more. Starts from either save: the - // catch spot (00.03's save, in the grass) or the save spot under the - // ledge. - static void train_in_catch_grass( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector& trainers, const RouteTrainerFight& fight - ) { - // From where the catch's save may stand (a save on the save spot - // has Potions and skips training) into the training column's grass. - const std::vector grass = story_after_training_area(); - walk_route(stream, context, maps, story_catch_save_area(), - [&](const TilePos& pos) { - return maps.world().at(pos) == Pokemon::TileType::TALL_GRASS && - std::find(grass.begin(), grass.end(), pos) != grass.end(); - }, - trainers, fight, "To the catch grass"); - for (size_t battle = 0; battle < 3; battle++) { - route_status(stream, "Training battle " + std::to_string(battle + 1) + "/3"); - battle_1_pokemon(stream, context); - fail_if_blacked_out(stream, context, "Training battle " + std::to_string(battle + 1)); - } - } - - // South into Sandgem Town in two walks (down the road to where it stops - // above the Mart, then the town plan into the Center's sign; one walk - // went round the houses and ended two tiles short, r252), three taps - // west below the door, and in by sight. Returns: below the door. - static std::vector heal_at_sandgem_center( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector& trainers, const RouteTrainerFight& fight - ) { - const TilePos entry = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); - const TilePos below_sign = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Pokemon::Direction::SOUTH)); - const std::vector at = walk_route(stream, context, maps, story_after_training_area(), - [&](const TilePos& pos) { return pos == entry; }, trainers, fight, "Into Sandgem Town", {}, Pokemon::RobustPlanCosts()); - walk_route(stream, context, maps, at, - [&](const TilePos& pos) { return pos == below_sign; }, trainers, fight, "To the Pokemon Center", {}, town_plan_costs()); - // Below the door by D-pad taps before lining up by sight: from below - // the sign, the finder read the signs' white panels as doorways and - // stepped between them (r252, r255). We face north, and a tap the - // other way only turns (r256): turn first. One tile short is fine. - route_turn(stream, context, Pokemon::Direction::WEST); - const size_t to_door = (size_t)(SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x); - const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, to_door, "west below the Center's door"); - stream.log("[00.04] Taps west below the Center's door moved " + std::to_string(west.moved_chunks) + " of " + std::to_string(to_door) + " tiles."); - if (west.moved_chunks + 1 < to_door) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Sandgem Town: the taps west below the Center's door moved " + std::to_string(west.moved_chunks) + " tiles.", stream); - } - heal_at_pokemon_center(stream, context, "Sandgem Town Pokemon Center"); - return story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); - } - // How many Potions to buy. A Potion is 300 and a new game starts with // 3000; the money on screen cannot be read yet. Five heal the starter // through Route 202's four trainers and leave half the money. @@ -903,19 +817,6 @@ namespace PokemonAutomation { [&](const TilePos& pos) { return pos == save_spot; }, trainers, fight, "Back to Route 202"); } - // Step 4: the walk above, then the save, which the save screen's own - // location must confirm (r124a saved beside the Mart, sure it stood - // on Route 202). A save in Sandgem would need the training walk to - // plan from town as well. - static void save_at_route_202_save_spot( - SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector at, std::vector& trainers, const RouteTrainerFight& fight - ) { - walk_to_route_202_save_spot(stream, context, maps, std::move(at), trainers, fight); - stream.log("[00.04] Saving with the Potions, at the save spot on Route 202."); - save_game(env, context, MAP_ROUTE_202); - } - // From anywhere on Sandgem Town's open ground, to tiles the map can // confirm: east until the trees or a wall stop us, then north until a // wall. The columns under the north exit run on to Route 202, whose @@ -992,13 +893,14 @@ namespace PokemonAutomation { walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); } - static void meet_dawn_in_jubilife(VideoStream& stream, ProControllerContext& context) { - // Jubilife's south edge is a wall with stairs in it, west of where the - // route may leave us (r78 stood at its east end). Entering starts a - // scene: Dawn walks over while we stand still. So walk north; if the - // wall stops us and no dialogue follows, the stairs are further west. - bool met_dawn = false; - for (size_t attempt = 0; attempt < 6 && !met_dawn; attempt++) { + // North into Jubilife until Dawn's scene starts. Jubilife's south edge + // is a wall with stairs in it, west of where the route may leave us + // (r78 stood at its east end). Entering starts a scene: Dawn walks + // over while we stand still. So walk north; if the wall stops us and + // no dialogue follows, the stairs are further west. Dawn's scene + // itself is the next step (a MASH). + static void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { + for (size_t attempt = 0; attempt < 6; attempt++) { const WalkResult walk = walk_until(stream, context, 0, 1, 4000ms, "to Dawn in Jubilife"); if (walk.stop == WalkStop::TRANSITION) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, @@ -1006,73 +908,81 @@ namespace PokemonAutomation { } ShortDialogWatcher dialog(COLOR_CYAN); if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0) { - met_dawn = true; - break; + return; } stream.log("[00.04] Jubilife: the wall stopped us; looking for the stairs one tile west."); route_turn(stream, context, Pokemon::Direction::WEST); pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); context.wait_for_all_requests(); } - if (!met_dawn) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Jubilife: no stairs found and no Dawn after six tiles west.", stream); - } - wait_for_dialogue(stream, context, "Phase 21 (dawn Jubilife dialog)"); - - pbf_mash_button(context, BUTTON_B, 5000ms); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: no stairs found and no Dawn after six tiles west.", stream); } - // Resumes from the save made after catching the second Pokemon, or - // from step 4's save. The catch save has no Potions; step 4's has - // them, with the starter trained and healed, so Potions in the Bag - // mean steps 1 to 4 are done; where that save stands is read off the - // save screen (resume_with_potions()). // Mart lady runs that failed this program run; the clerk after two. static size_t g_mart_lady_misses = 0; - static void tutorial_part_3_after_catch_navigation(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { - log_after_catch_steps(stream); - - const MapSet& maps = story_maps(); - std::vector trainers = story_trainer_sights(); - const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; - pbf_wait(context, 1500ms); - if (bag_has_item(stream, context, "potion", 1)) { - resume_with_potions(stream, context, maps, trainers, fight); - } else { - after_catch_step(stream, 1); - train_in_catch_grass(stream, context, maps, trainers, fight); + static Pokemon::Pose pose_at_save_spot() { + return Pokemon::tile_pose(MAP_ROUTE_202, {story_maps().to_world(MAP_ROUTE_202, Route202::SAVE_SPOT)}, Pokemon::Direction::EAST); + } - after_catch_step(stream, 2); - heal_at_sandgem_center(stream, context, maps, trainers, fight); + // 00.04b's own steps (Data/PokemonBDSP_AutoStory_Segment00Steps.h). + // With Potions in the Bag, steps 1 to 4 of the old function are done: + // the save is the one made with them, which the save screen places + // (resume_with_potions()); from there on to Route 202's trainers. + static Pokemon::Outcome locate_save_step(ConsoleStoryExecutor& executor, StoryConsole& c, Pokemon::RunState& state, const Pokemon::StoryStep&) { + pbf_wait(c.context, 1500ms); + if (!bag_has_item(c.env.console, c.context, "potion", 1)) { + return Pokemon::outcome_ok(); + } + resume_with_potions(c.env.console, c.context, executor.maps(), executor.trainers(), executor.fight(c)); + state.flags.insert("has_potions"); + state.flags.insert("potions_saved"); + return Pokemon::outcome_ok(pose_at_save_spot()); + } - after_catch_step(stream, 3); - if (g_mart_lady_misses < 2) { - // Her events walk on to Route 202's save spot and save there (step 4). - try { diagnostic_sandgem_mart_lady(env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } - } else { - const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); - after_catch_step(stream, 4); - save_at_route_202_save_spot(env, stream, context, maps, at, trainers, fight); - } - if (!bag_has_item(stream, context, "potion", 1)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after step 3.", stream); - } + // Potions: the Poke Mart lady on Route 201 (her events walk on to + // Route 202's save spot and save there), the clerk after two misses. + // The Bag is checked after, since neither reads the quantity. + static Pokemon::Outcome sandgem_potions_step(ConsoleStoryExecutor& executor, StoryConsole& c, Pokemon::RunState& state, const Pokemon::StoryStep&) { + std::optional pose; + if (g_mart_lady_misses < 2) { + try { diagnostic_sandgem_mart_lady(c.env, c.context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } + state.flags.insert("potions_saved"); + pose = pose_at_save_spot(); + } else { + const std::vector at = buy_potions_in_sandgem(c.env.console, c.context, executor.maps(), executor.trainers(), executor.fight(c)); + pose = Pokemon::tile_pose(MAP_SANDGEM_TOWN, at); } + if (!bag_has_item(c.env.console, c.context, "potion", 1)) { + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after the shop.", c.env.console); + } + return Pokemon::outcome_ok(std::move(*pose)); + } - after_catch_step(stream, 5); - // From the save spot under the ledge, where every way here ends: - // step 4 saves on it, and a resume with Potions is either that - // save or walks back to it first. From both save spots at once - // the planner gives up: the route costs' exact open legs move the - // pair in step without ever joining it, so the sets of two tiles - // run past its cap (no plan within 400000 sets; 1300 from here). - const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); - walk_route(stream, context, maps, {save_spot}, story_at_jubilife_exit, trainers, fight, "To Jubilife"); + static Pokemon::Outcome sign_gap_step(ConsoleStoryExecutor& executor, StoryConsole& c, Pokemon::RunState& state, const Pokemon::StoryStep&) { + walk_to_route_202_save_spot(c.env.console, c.context, executor.maps(), Pokemon::tiles_of(state.pose), executor.trainers(), executor.fight(c)); + return Pokemon::outcome_ok(pose_at_save_spot()); + } + + static Pokemon::Outcome jubilife_stairs_step(ConsoleStoryExecutor&, StoryConsole& c, Pokemon::RunState&, const Pokemon::StoryStep&) { + up_jubilife_stairs(c.env.console, c.context); + return Pokemon::outcome_ok(); + } - after_catch_step(stream, 6); - meet_dawn_in_jubilife(stream, context); + // 00.04b's executor: the story maps, Route 202's trainers, and the + // steps above by their variants. + static std::shared_ptr after_catch_executor(StoryStepRunner& runner) { + return std::make_shared( + runner, segment_00_after_catch().id, story_maps(), story_trainer_sights(), + [](StoryConsole& c, const std::string& name) { fight_route_trainer(c.env.console, c.context, name); }, + std::map{ + {STEP_LOCATE_SAVE, locate_save_step}, + {STEP_SANDGEM_POTIONS, sandgem_potions_step}, + {STEP_SIGN_GAP, sign_gap_step}, + {STEP_JUBILIFE_STAIRS, jubilife_stairs_step}, + } + ); } // --------------------------------------------------------------------------- // End of Segment 00 - 04 @@ -1482,7 +1392,8 @@ namespace PokemonAutomation { // 00.04 and 00.05: the catch, Sandgem, and Jubilife to the Poketch. // 00.04 saves twice: a retry after the catch starts from the - // caught-Pokemon save, not the old route, so it is two rows. + // caught-Pokemon save, not the old route, so it is two rows; the + // second, 00.04b, is made in make_segment_00(). static std::vector segment_00_town_rows() { const StoryFailure retry = StoryFailure::RELOAD_AND_RETRY; return { @@ -1490,10 +1401,6 @@ namespace PokemonAutomation { {"tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", [](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); }}, }), - segment_00_row("00.04b", "After the catch, to Dawn in Jubilife", retry, { - {"tutorial_part_3_after_catch_navigation", "train, heal and buy Potions in Sandgem, save on Route 202, walk to Jubilife and meet Dawn", - [](StoryConsole& c) { tutorial_part_3_after_catch_navigation(c.env, c.env.console, c.context); }}, - }), segment_00_row("00.05", "Jubilife After Dawn", retry, { {"tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC", [](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); }}, @@ -1501,12 +1408,19 @@ namespace PokemonAutomation { }; } + // 00.04b, after the catch, is written as story steps; the other rows + // still wrap one function each. Pokemon::Segment make_segment_00(StoryStepRunner& runner) { std::vector rows = segment_00_intro_rows(); for (StoryCheckpointSpec& row : segment_00_town_rows()) { rows.push_back(std::move(row)); } - return story_segment(runner, "00", std::move(rows)); + Pokemon::Segment segment = story_segment(runner, "00", std::move(rows)); + const auto after_catch = std::find_if(segment.checkpoints.begin(), segment.checkpoints.end(), + [](const Pokemon::CheckpointSpec& row) { return row.id == "00.05"; }); + segment.checkpoints.insert(after_catch, + story_steps_checkpoint(runner, segment_00_after_catch(), after_catch_executor(runner))); + return segment; } // DIAGNOSTIC ONLY: Remove before pushing to production. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp new file mode 100644 index 0000000000..60b13ab45a --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -0,0 +1,212 @@ +/* BDSP AutoStory - Story step executor + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP_AutoStory_PokemonCenter.h" +#include "PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "PokemonBDSP_AutoStory_Walk.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Outcome; +using Pokemon::RunState; +using Pokemon::TilePos; + + +std::optional story_route_costs(const std::string& variant){ + if (variant.empty()){ + return route_plan_costs(); + } + if (variant == "town"){ + return town_plan_costs(); + } + if (variant == "robust"){ + return Pokemon::RobustPlanCosts(); + } + return std::nullopt; +} + +std::pair dpad_of(Direction direction){ + switch (direction){ + case Direction::NORTH: + return {0, 1}; + case Direction::EAST: + return {1, 0}; + case Direction::SOUTH: + return {0, -1}; + case Direction::WEST: + return {-1, 0}; + } + return {0, 0}; +} + + +ConsoleStoryExecutor::ConsoleStoryExecutor( + StoryStepRunner& runner, std::string checkpoint, + const Pokemon::MapSet& maps, std::vector trainers, + TrainerFight fight, std::map variants +) + : m_runner(runner) + , m_checkpoint(std::move(checkpoint)) + , m_maps(maps) + , m_all_trainers(std::move(trainers)) + , m_trainers(m_all_trainers) + , m_fight(std::move(fight)) + , m_variants(std::move(variants)) +{} + +void ConsoleStoryExecutor::begin_attempt(){ + m_trainers = m_all_trainers; +} + +RouteTrainerFight ConsoleStoryExecutor::fight(StoryConsole& console){ + return [this, &console](const std::string& trainer){ + m_fight(console, trainer); + }; +} + +StoryConsole& ConsoleStoryExecutor::console(){ + if (m_console == nullptr){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "A story step ran outside ConsoleStoryExecutor::run()."); + } + return *m_console; +} + +Outcome ConsoleStoryExecutor::run(RunState& state, const Pokemon::StoryStep& step){ + Outcome result = Pokemon::outcome_ok(); + const Outcome ran = m_runner.run_step(m_checkpoint, StoryFailure::RELOAD_AND_RETRY, [&](StoryConsole& console){ + m_console = &console; + const auto variant = m_variants.find(step.variant); + result = variant == m_variants.end() + ? Pokemon::StoryStepExecutor::run(state, step) + : variant->second(*this, console, state, step); + m_console = nullptr; + }); + m_console = nullptr; + return ran.succeeded() ? result : ran; +} + +Outcome ConsoleStoryExecutor::route(RunState& state, const Pokemon::StoryStep& step){ + const std::vector from = Pokemon::tiles_of(state.pose); + if (from.empty()){ + return Pokemon::outcome_failed("no_pose", Pokemon::to_string(state.pose)); + } + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + return Pokemon::outcome_failed("unknown_variant", step.variant); + } + const std::vector goal = Pokemon::tiles_of(*step.target); + StoryConsole& c = console(); + const std::vector at = walk_route( + c.env.console, c.context, m_maps, from, + [&](const TilePos& pos){ return std::find(goal.begin(), goal.end(), pos) != goal.end(); }, + m_trainers, fight(c), step.name, {}, *costs + ); + return Pokemon::outcome_ok(Pokemon::tile_pose(step.target->map, at)); +} + +Outcome ConsoleStoryExecutor::legs(RunState& state, const Pokemon::StoryStep& step){ + const bool one_short_ok = step.variant == "one_short_ok"; + if (!one_short_ok && !step.variant.empty()){ + return Pokemon::outcome_failed("unknown_variant", step.variant); + } + StoryConsole& c = console(); + std::vector at = Pokemon::tiles_of(state.pose); + for (const Pokemon::StoryLeg& leg : step.legs){ + // Facing another way, a tap only turns (r256): turn first. + route_turn(c.env.console, c.context, leg.direction); + const auto [x, y] = dpad_of(leg.direction); + const size_t tiles = (size_t)leg.tiles; + const WalkResult walk = walk_tiles_dpad(c.env.console, c.context, x, y, tiles, step.name); + c.env.console.log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); + if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ + return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); + } + // Where we may be: the end, or a tile short of it. + std::vector next; + for (const TilePos& start : at){ + TilePos end = start; + for (size_t i = 0; i < tiles; i++){ + if (one_short_ok && i + 1 == tiles){ + next.push_back(end); + } + end = end.next(leg.direction); + } + next.push_back(end); + } + at = std::move(next); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.legs.back().direction)); +} + +Outcome ConsoleStoryExecutor::train(RunState&, const Pokemon::StoryStep& step){ + StoryConsole& c = console(); + for (size_t battle = 0; battle < step.battles; battle++){ + const std::string label = std::format("Training battle {}/{}", battle + 1, step.battles); + route_status(c.env.console, label); + battle_1_pokemon(c.env.console, c.context); + fail_if_blacked_out(c.env.console, c.context, label); + } + return Pokemon::outcome_ok(); +} + +Outcome ConsoleStoryExecutor::heal(RunState&, const Pokemon::StoryStep& step){ + StoryConsole& c = console(); + heal_at_pokemon_center(c.env.console, c.context, step.why.empty() ? step.name : step.why); + return Pokemon::outcome_ok(); +} + +Outcome ConsoleStoryExecutor::save(RunState&, const Pokemon::StoryStep& step){ + StoryConsole& c = console(); + c.env.console.log(std::format("[{}] Saving: {}.", m_checkpoint, step.why)); + save_game(c.env, c.context, step.target ? step.target->map : ""); + return Pokemon::outcome_ok(); +} + +Outcome ConsoleStoryExecutor::mash(RunState&, const Pokemon::StoryStep& step){ + StoryConsole& c = console(); + wait_for_dialogue(c.env.console, c.context, step.name); + pbf_mash_button(c.context, BUTTON_B, 5000ms); + c.context.wait_for_all_requests(); + return Pokemon::outcome_ok(); +} + + +Pokemon::CheckpointSpec story_steps_checkpoint( + StoryStepRunner& runner, const Pokemon::StoryCheckpoint& data, + std::shared_ptr executor +){ + // The table's own row with no steps: its save at the start. + Pokemon::CheckpointSpec row = story_checkpoint(runner, StoryCheckpointSpec{ + .id = data.id, .title = data.title, .on_failure = StoryFailure::RELOAD_AND_RETRY, .steps = {}, + }); + row.build = [save = std::move(row.build), &data, executor = std::move(executor)]() mutable{ + executor->begin_attempt(); + Pokemon::Checkpoint checkpoint = save(); + Pokemon::Checkpoint steps = Pokemon::build_story_checkpoint(data, *executor); + for (Pokemon::Event& event : steps.events){ + checkpoint.events.push_back(std::move(event)); + } + return checkpoint; + }; + return row; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h new file mode 100644 index 0000000000..c27c10f8e2 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h @@ -0,0 +1,125 @@ +/* BDSP AutoStory - Story step executor + * + * From: https://github.com/PokemonAutomation/ + * + * BDSP's way of doing the shared story steps + * (Pokemon/Navigation/Pokemon_StorySteps.h) on the console. Routes are + * planned on the story's map set from the tiles the run's pose holds, + * with the trainers who can still see us fought on the way; a route's + * variant names its plan costs. A segment adds its own steps by variant + * (the Mart lady, a walk through a gap in town): a variant registered + * here is used for its step whatever the kind. + * + * Each step runs through the table's StoryStepRunner, so a failed step + * is answered as the table's other rows are: a screenshot, the + * "checkpoint_reattempt_loop: attempt N failed" line the lab watches + * for, and a failed Outcome that the segment's policy answers. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryExecutor_H + +#include +#include +#include +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// The plan costs a ROUTE step's variant names: "" the route costs, +// "town" town_plan_costs(), "robust" the planner's defaults. Nothing for +// another name. +std::optional story_route_costs(const std::string& variant); + +// The D-pad direction (x, y) of `direction`: y is up the screen. +std::pair dpad_of(Pokemon::Direction direction); + + +// Does BDSP's kinds of story step for one checkpoint of a table. +class ConsoleStoryExecutor : public Pokemon::StoryStepExecutor{ +public: + // A segment's own way of doing a step, by the step's variant. It may + // set run flags for later steps' skip_if, and returns where it left + // the player if it knows. + using Variant = std::function; + // Fights a trainer whose dialogue has opened. + using TrainerFight = std::function; + + ConsoleStoryExecutor( + StoryStepRunner& runner, std::string checkpoint, + const Pokemon::MapSet& maps, std::vector trainers, + TrainerFight fight, std::map variants + ); + + // A new attempt of the checkpoint: every trainer can see us again. + void begin_attempt(); + + const Pokemon::MapSet& maps() const{ + return m_maps; + } + // The trainers who can still see us, and how to fight one, for a + // variant that walks with walk_route(). + std::vector& trainers(){ + return m_trainers; + } + RouteTrainerFight fight(StoryConsole& console); + + virtual Pokemon::Outcome run(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + + virtual Pokemon::Outcome route(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // Each leg in D-pad taps, turning first. Variant "one_short_ok": a leg + // may end a tile short (a door lined up by sight after). + virtual Pokemon::Outcome legs(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + virtual Pokemon::Outcome train(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // In front of a Pokemon Center, its lit doorway on screen: in, the + // nurse, and out (heal_at_pokemon_center()). + virtual Pokemon::Outcome heal(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // Saves; the save screen must read the target's map, if there is one. + virtual Pokemon::Outcome save(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + // A dialogue opens (or has), then B through it. + virtual Pokemon::Outcome mash(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; + +private: + StoryConsole& console(); + + StoryStepRunner& m_runner; + std::string m_checkpoint; + const Pokemon::MapSet& m_maps; + std::vector m_all_trainers; + std::vector m_trainers; + TrainerFight m_fight; + std::map m_variants; + // The console of the step running now. + StoryConsole* m_console = nullptr; +}; + + +// `data` as a row of a story table: it saves at its start like the +// table's other rows, then runs its steps with `executor`, which starts a +// new attempt each time the row is built. `data` must outlive the +// segment; the row keeps the executor. +Pokemon::CheckpointSpec story_steps_checkpoint( + StoryStepRunner& runner, const Pokemon::StoryCheckpoint& data, + std::shared_ptr executor +); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp index 5544ede503..71fa836117 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp @@ -29,9 +29,6 @@ using Pokemon::RunState; namespace{ -// The attempts checkpoint_reattempt_loop() gives a checkpoint. -constexpr size_t STORY_ATTEMPTS = 20; - Event save_event(StoryStepRunner& runner, const std::string& id){ Event event; event.name = "save"; @@ -119,8 +116,9 @@ Outcome ConsoleStoryRunner::run_step(const std::string& id, StoryFailure on_fail if (m_console.options.checkpoint_retry_scope == CheckpointRetryScope::DEFER_TO_SELECTED_RANGE){ throw; } + // Attempts are the segment's policy (AutoStoryOptions::policy); this + // only counts them for the log line the lab's run watcher reads. const size_t attempt = m_failures[id]++; - // The lab's run watcher reads this line as a failed attempt. env.console.log(std::format("checkpoint_reattempt_loop: attempt {} failed: {}: {}", attempt, id, e.message()), COLOR_RED); // Keep what the screen showed at the failure; the retry resets the game. if (VideoSnapshot screen = env.console.video().snapshot()){ @@ -132,13 +130,6 @@ Outcome ConsoleStoryRunner::run_step(const std::string& id, StoryFailure on_fail } m_console.stats.m_reset++; env.update_stats(); - if (attempt + 1 >= STORY_ATTEMPTS){ - throw; - } - if (g_stop_at_first_failure){ - env.console.log("Stopping at the first failure; the game stays where it failed.", COLOR_ORANGE); - throw; - } return Pokemon::outcome_failed("operation_failed", e.message()); } } @@ -168,8 +159,7 @@ void ConsoleStoryRunner::run(Pokemon::Segment& segment, const Pokemon::SegmentRa hooks.log = [&env](const std::string& line){ env.log(line, COLOR_BLUE); }; - Pokemon::SegmentPolicy policy; - policy.attempts_per_checkpoint = STORY_ATTEMPTS; + const Pokemon::SegmentPolicy& policy = m_console.options.policy; RunState state; const Pokemon::SegmentReport report = Pokemon::run_segment(state, segment, range, policy, hooks); if (!report.completed){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h index 842290c25a..fbd9e03b9c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h @@ -8,7 +8,8 @@ * checkpoint saves first, as checkpoint_reattempt_loop() did before its * first attempt; a step that fails is answered the way that loop answered * it: a screenshot, a reset to the save, and the checkpoint again from its - * first step, up to the same 20 attempts. + * first step, up to the attempts of the shared policy the program's retry + * options set (AutoStoryOptions::policy; 3 by default, where the loop had 20). * */ diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index c7eae6ade8..b86734c1a1 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1576,6 +1576,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_OreburghGym.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route204.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h @@ -1589,6 +1591,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -1611,6 +1615,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Segment_00_tools.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_Autostory_Segment_01_tools.h Source/PokemonBDSP/Inference/Battles/PokemonBDSP_BattleBallReader.cpp From f1408301ae52e39668f1a22c05a4bc21bf8fb29e Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 10:09:39 -0800 Subject: [PATCH 04/13] Pokemon navigation: story executors hear of each attempt (begin_attempt) build_story_checkpoint() calls StoryStepExecutor::begin_attempt() before building, once per attempt, so a game can start the attempt afresh (BDSP: the trainers who can see us again; the row's failure mode). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/Pokemon/Navigation/Pokemon_StorySteps.cpp | 3 +++ .../Source/Pokemon/Navigation/Pokemon_StorySteps.h | 5 +++++ .../Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp | 10 ++++++++++ 3 files changed, 18 insertions(+) diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp index b0ecdd4514..75422b02cc 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp @@ -59,6 +59,8 @@ Outcome StoryStepExecutor::run(RunState& state, const StoryStep& step){ return dispatch_story_step(*this, state, step); } +void StoryStepExecutor::begin_attempt(const StoryCheckpoint&){} + bool StoryStepExecutor::holds(const std::string& condition, RunState& state){ return state.flags.contains(condition); } @@ -178,6 +180,7 @@ Event from_save_event(const Pose& start){ } Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor){ + executor.begin_attempt(data); Checkpoint checkpoint; checkpoint.id = data.id; if (data.start){ diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h index 41fe51fb77..0c877739ad 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h @@ -119,6 +119,11 @@ class StoryStepExecutor{ public: virtual ~StoryStepExecutor() = default; + // A checkpoint is being built for a new attempt: build_story_checkpoint() + // calls this first, so a game can start the attempt afresh (trainers + // who can see us again, the row's own settings). Does nothing by default. + virtual void begin_attempt(const StoryCheckpoint& data); + // Runs `step`: dispatch_story_step() by default. A game overrides it // to wrap every step, e.g. to turn a failed console step into a failed // Outcome. diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp index fe0cc60623..bb73f89676 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp @@ -42,6 +42,12 @@ class FakeExecutor : public StoryStepExecutor{ std::map failures_left; // The pose each step started from, by name. std::vector> poses_seen; + // The checkpoints begun, one per attempt. + std::vector attempts; + + virtual void begin_attempt(const StoryCheckpoint& data) override{ + attempts.push_back(data.id); + } virtual Outcome run(RunState& state, const StoryStep& step) override{ poses_seen.emplace_back(step.name, to_string(state.pose)); @@ -329,6 +335,10 @@ UnitTestResult test_segment_of_tables(){ if (!report.completed || report.reloads != 1 || !tail_matches){ return "Expected 00.04 reloaded once and passed:" + joined(executor.trace) + joined(lines); } + // Each attempt is begun anew: 00.03 once, 00.04 twice. + if (executor.attempts != std::vector{"00.03", "00.04", "00.04"}){ + return "Expected the executor told of each attempt:" + joined(executor.attempts); + } // The report and the log tell a one-shot pass from a retried one. const bool runs_ok = report.runs.size() == 2 && report.runs[0].one_shot() && report.runs[0].attempts == 1 From 5da607b23c8e166f5cdc9e3ab1e9d2fa9adecd28 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 10:15:47 -0800 Subject: [PATCH 05/13] BDSP AutoStory: segments 00 and 01 as one story-step table, run end to end in tests The table (PokemonBDSP_AutoStory_StoryTable) now reads like FRLG's: rows of the shared StorySteps, 00.00 to 01.11, plain data. 00.04b to 00.04d (from bdsp/mvp-v3's split) are declarative: route to the catch grass, train 3, into Sandgem (robust costs), to the Center's sign (town costs), taps west (one short is fine), heal; the Potions shop; north past Route 202's trainers, up the stairs, Dawn's scene. Every other step calls its old function through a variant the segment registers (segment_00_variants(), segment_01_variants()). story_table_segment() builds rows the way the old StoryCheckpointSpec rows were built: save first unless the row stops on failure, then the steps through ConsoleStoryExecutor, which goes through the runner so failures keep their screenshot and attempt log line. The console-action alias is renamed StoryAction, apart from Pokemon::StoryStep. leave_door_south()'s stop decision is door_walk_stops(), so it can be tested. Tests: Test_StorySimulation runs both segments end to end against a fake game (routes and legs planned on the built-in maps): checkpoint order and saves, start/end ranges, a blackout in 00.04b's training resetting to its save, a step failing three times then stopping (and stop-at-first-failure), start poses chaining, every called function registered, and the door exit's looks. Test_MartLadyEvents gains the Lab sign re-anchor case. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 4 +- .../PokemonBDSP_AutoStory_Segment00Steps.cpp | 157 ------ .../PokemonBDSP_AutoStory_Segment00Steps.h | 48 -- ...monBDSP_AutoStory_Segment00Steps_Tests.cpp | 190 ------- ...kemonBDSP_AutoStory_Segment00Steps_Tests.h | 26 - ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 484 ++++++++++++++++++ ...emonBDSP_AutoStory_StorySimulation_Tests.h | 27 + .../PokemonBDSP_AutoStory_StoryTable.cpp | 252 +++++++++ .../PokemonBDSP_AutoStory_StoryTable.h | 42 ++ ...okemonBDSP_AutoStory_StoryTables_Tests.cpp | 2 +- .../AutoStory/PokemonBDSP_AutoStory_Tests.cpp | 26 +- .../PokemonBDSP_AutoStory_Segment_00.cpp | 251 ++------- .../PokemonBDSP_AutoStory_Segment_00.h | 12 +- .../PokemonBDSP_AutoStory_Segment_01.cpp | 84 +-- .../PokemonBDSP_AutoStory_Segment_01.h | 9 +- .../Utils/PokemonBDSP_AutoStory_RouteWalk.cpp | 8 +- .../Utils/PokemonBDSP_AutoStory_RouteWalk.h | 4 + .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 36 +- .../PokemonBDSP_AutoStory_StoryExecutor.h | 22 +- .../PokemonBDSP_AutoStory_StoryTables.cpp | 65 ++- .../Utils/PokemonBDSP_AutoStory_StoryTables.h | 41 +- SerialPrograms/cmake/SourceFiles.cmake | 8 +- 22 files changed, 1008 insertions(+), 790 deletions(-) delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp delete mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 2a81ec1000..1d98998358 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -21,7 +21,7 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_MapGrid_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" -#include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h" @@ -297,7 +297,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_MapGrids(database); add_tests_AutoStory(database); add_tests_AutoStory_Segment00(database); - add_tests_AutoStory_Segment00Steps(database); + add_tests_AutoStory_StorySimulation(database); add_tests_AutoStory_StoryTables(database); add_tests_AutoStory_Indoors(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp deleted file mode 100644 index 89231f8f7a..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp +++ /dev/null @@ -1,157 +0,0 @@ -/* BDSP AutoStory - Segment 00 as story steps - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include "PokemonBDSP_AutoStory_Route202.h" -#include "PokemonBDSP_AutoStory_SandgemTown.h" -#include "PokemonBDSP_AutoStory_StoryMaps.h" -#include "PokemonBDSP_AutoStory_Segment00Steps.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - -using Pokemon::Direction; -using Pokemon::Pose; -using Pokemon::StoryStep; -using Pokemon::StoryStepKind; -using Pokemon::TilePos; - - -const char* const STEP_LOCATE_SAVE = "locate_save"; -const char* const STEP_SANDGEM_POTIONS = "sandgem_potions"; -const char* const STEP_SIGN_GAP = "sign_gap"; -const char* const STEP_JUBILIFE_STAIRS = "jubilife_stairs"; - - -namespace{ - -// Steps 2 to 8 are done once the Bag has Potions; 9 and 10 once a save -// with them stands on Route 202. -const char* const HAS_POTIONS = "has_potions"; -const char* const POTIONS_SAVED = "potions_saved"; - -Pose on(const char* map, std::vector world, std::optional facing = std::nullopt){ - return Pokemon::tile_pose(map, std::move(world), facing); -} - -TilePos world(const char* map, const TilePos& local){ - return story_maps().to_world(map, local); -} - -// The catch grass the training walks to: tall grass in the training area. -std::vector catch_grass(){ - std::vector grass = story_after_training_area(); - std::erase_if(grass, [](const TilePos& pos){ - return story_maps().world().at(pos) != Pokemon::TileType::TALL_GRASS; - }); - return grass; -} - -// Route 202's open rows at the top: north from any of them is Jubilife. -std::vector jubilife_exit(){ - std::vector exit; - const Pokemon::TileGrid& grid = Route202::grid(); - for (int y = 0; y <= 3; y++){ - for (int x = 0; x < grid.width(); x++){ - const TilePos pos = world(MAP_ROUTE_202, {x, y}); - if (story_maps().world().standable(pos) && story_at_jubilife_exit(pos)){ - exit.push_back(pos); - } - } - } - return exit; -} - -StoryStep make(StoryStepKind kind, std::string name, std::string why, std::vector skip_if = {}){ - StoryStep step; - step.kind = kind; - step.name = std::move(name); - step.why = std::move(why); - step.skip_if = std::move(skip_if); - return step; -} - -StoryStep with_variant(StoryStep step, std::string variant){ - step.variant = std::move(variant); - return step; -} - -StoryStep with_target(StoryStep step, Pose target){ - step.target = std::move(target); - return step; -} - -StoryStep with_pose_after(StoryStep step, Pose pose){ - step.pose_after = std::move(pose); - return step; -} - -Pokemon::StoryCheckpoint make_after_catch(){ - const TilePos save_spot = world(MAP_ROUTE_202, Route202::SAVE_SPOT); - const TilePos below_sign = world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH)); - const int to_door = SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x; - - StoryStep train = make(StoryStepKind::TRAIN, "train", "a Lv5 starter fainted to Route 202's trainers (r72)", {HAS_POTIONS}); - train.battles = 3; - StoryStep below_door = make(StoryStepKind::LEGS, "below_center_door", - "taps west below the Center's door; the finder read the signs as doorways from below the sign (r252, r255)", {HAS_POTIONS}); - below_door.legs = {{.direction = Direction::WEST, .tiles = to_door}}; - StoryStep potions = make(StoryStepKind::SHOP, "potions", - "Potions before Route 202's trainers; the lady's 10 are free", {HAS_POTIONS}); - potions.items = {{.item = "potion", .quantity = 10}}; - StoryStep stairs = make(StoryStepKind::STEP, "into_jubilife", - "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); - stairs.direction = Direction::NORTH; - - return Pokemon::StoryCheckpoint{ - .id = "00.04b", - .title = "After the catch, to Dawn in Jubilife", - .start = on(MAP_ROUTE_202, story_catch_save_area()), - .steps = { - with_variant(make(StoryStepKind::GAME, "locate_save", "a retry may load the save made with the Potions"), STEP_LOCATE_SAVE), - with_target(make(StoryStepKind::ROUTE, "to_catch_grass", "into the catch grass to train", {HAS_POTIONS}), - on(MAP_ROUTE_202, catch_grass())), - with_pose_after(std::move(train), on(MAP_ROUTE_202, story_after_training_area())), - // One walk round the houses ended two tiles short (r252): down - // the road first, then the town plan. - with_variant(with_target(make(StoryStepKind::ROUTE, "into_sandgem", "south down the road to above the Mart", {HAS_POTIONS}), - on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP)})), "robust"), - with_variant(with_target(make(StoryStepKind::ROUTE, "to_center_sign", "below the Center's sign, its doorway in sight", {HAS_POTIONS}), - on(MAP_SANDGEM_TOWN, {below_sign}, Direction::NORTH)), "town"), - with_variant(std::move(below_door), "one_short_ok"), - with_pose_after(make(StoryStepKind::HEAL, "heal_sandgem", "Sandgem Town Pokemon Center", {HAS_POTIONS}), - on(MAP_SANDGEM_TOWN, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR))), - with_variant(std::move(potions), STEP_SANDGEM_POTIONS), - with_pose_after(with_variant(make(StoryStepKind::GAME, "back_to_route_202", - "to the save spot under the ledge, where a retry may start; the trainers are north of it", {POTIONS_SAVED}), STEP_SIGN_GAP), - on(MAP_ROUTE_202, {save_spot})), - with_target(make(StoryStepKind::SAVE, "save_route_202", - "with the Potions, at the save spot on Route 202 (r124a saved beside the Mart)", {POTIONS_SAVED}), - on(MAP_ROUTE_202, {save_spot})), - // From the one save spot: from both at once the planner gives up. - with_target(make(StoryStepKind::ROUTE, "north_to_jubilife", "past Route 202's trainers, fighting who sees us"), - on(MAP_ROUTE_202, jubilife_exit())), - with_variant(std::move(stairs), STEP_JUBILIFE_STAIRS), - make(StoryStepKind::MASH, "dawn_scene", "Dawn walks over while we stand still"), - }, - }; -} - -} - - -const Pokemon::StoryCheckpoint& segment_00_after_catch(){ - static const Pokemon::StoryCheckpoint checkpoint = make_after_catch(); - return checkpoint; -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h deleted file mode 100644 index ab180cde23..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h +++ /dev/null @@ -1,48 +0,0 @@ -/* BDSP AutoStory - Segment 00 as story steps - * - * From: https://github.com/PokemonAutomation/ - * - * Segment 00's checkpoints written as the shared story steps - * (Pokemon/Navigation/Pokemon_StorySteps.h), plain data for the tests to - * plan. The first is 00.04b, the half of 00.04 after the catch; the rest - * of the segment is still one wrapped function per checkpoint. - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_H - -#include "Pokemon/Navigation/Pokemon_StorySteps.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// The variants 00.04b's steps name, done by Segment 00's own code. -// GAME: with Potions in the Bag, the run resumes from the save after -// them: flags "has_potions" and "potions_saved", and walks back to -// Route 202's save spot if that save is in Sandgem Town. -extern const char* const STEP_LOCATE_SAVE; -// SHOP: Potions from the Poke Mart lady on Route 201, who walks us on to -// Route 202's save spot and saves there (flag "potions_saved"), or after -// two misses the clerk in Sandgem's Poke Mart. -extern const char* const STEP_SANDGEM_POTIONS; -// GAME: from Sandgem's front row north through the gap between the -// signs to Route 202's save spot. -extern const char* const STEP_SIGN_GAP; -// STEP: north into Jubilife, a tile west at a time until the stairs take -// us and Dawn's scene starts. -extern const char* const STEP_JUBILIFE_STAIRS; - -// 00.04b: from the save after the catch on Route 202 to Dawn in Jubilife: -// train, heal in Sandgem, Potions, save on Route 202, north past its -// trainers, Dawn's scene. Poses are in world tiles of story_maps(). -const Pokemon::StoryCheckpoint& segment_00_after_catch(); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp deleted file mode 100644 index ea23657096..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/* BDSP AutoStory Segment 00 Steps Tests - * - * From: https://github.com/PokemonAutomation/ - * - * Follows 00.04b's steps from its start on the story maps (the JSON grids - * built into the program), the way a first run goes: no Potions yet, and - * the Poke Mart clerk's way, which walks back to Route 202 itself. Each - * route is planned with the costs its variant names, from the tiles the - * step before may leave us on. - * - */ - -#include -#include -#include -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/TestRunners/UnitTest.h" -#include "Pokemon/Navigation/Pokemon_MapSet.h" -#include "Pokemon/Navigation/Pokemon_StorySteps.h" -#include "Pokemon/Navigation/Pokemon_TileGrid.h" -#include "Data/PokemonBDSP_AutoStory_Route202.h" -#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" -#include "Data/PokemonBDSP_AutoStory_Segment00Steps.h" -#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" -#include "Utils/PokemonBDSP_AutoStory_StoryExecutor.h" -#include "PokemonBDSP_AutoStory_Segment00Steps_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -using Pokemon::Direction; -using Pokemon::StoryStep; -using Pokemon::StoryStepKind; -using Pokemon::TilePos; - -std::string describe(const std::vector& tiles){ - std::string text; - for (const TilePos& pos : tiles){ - const std::optional local = story_maps().to_local(pos); - text += " " + (local ? local->map + " " + Pokemon::to_string(local->pos) : Pokemon::to_string(pos)); - } - return text; -} - -bool contains(const std::vector& tiles, const TilePos& pos){ - return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); -} - -// Plans `step` from `at` and moves `at` to where the plan may end. -std::string plan_route(Logger& logger, std::vector& at, const StoryStep& step){ - const std::optional costs = story_route_costs(step.variant); - if (!costs){ - return step.name + ": no costs named \"" + step.variant + "\"."; - } - const std::vector goal = Pokemon::tiles_of(*step.target); - if (goal.empty()){ - return step.name + ": the target has no tiles."; - } - const auto plan = Pokemon::plan_robust_route(story_maps().world(), at, - [&](const TilePos& pos){ return contains(goal, pos); }, *costs); - if (!plan){ - return step.name + ": no plan from" + describe(at) + "."; - } - std::string text = std::format("00.04b {}: {} legs from{}", step.name, plan->size(), describe(at)); - for (const Pokemon::PlannedLeg& leg : *plan){ - text += "\n " + to_string(leg.leg) + " to" + describe(leg.after); - } - logger.log(text); - if (!plan->empty()){ - at = plan->back().after; - } - for (const TilePos& pos : at){ - if (!contains(goal, pos)){ - return step.name + ": the plan may end off the target, at" + describe({pos}) + "."; - } - } - return ""; -} - -// Walks `step`'s legs tile by tile from each of `at`, a tile short too -// when the step allows it. -std::string walk_legs(std::vector& at, const StoryStep& step){ - const Pokemon::TileGrid& world = story_maps().world(); - for (const Pokemon::StoryLeg& leg : step.legs){ - std::vector next; - for (TilePos pos : at){ - for (int i = 0; i < leg.tiles; i++){ - const std::optional stepped = world.step(pos, leg.direction); - if (!stepped){ - return step.name + ": " + direction_name(leg.direction) + " is blocked at" + describe({pos}) + "."; - } - if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ - next.push_back(pos); - } - pos = *stepped; - } - next.push_back(pos); - } - at = std::move(next); - } - return ""; -} - -class Test_AfterCatchSteps : public UnitTest{ -public: - Test_AfterCatchSteps() - : UnitTest("PokemonBDSP::AutoStory - 00.04b steps: lint, and every route plans on the story maps") - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - try{ - return check(logger); - }catch (Exception& e){ - return std::string(e.name()) + ": " + e.message(); - } - } - -private: - static UnitTestResult check(Logger& logger){ - const Pokemon::StoryCheckpoint& data = segment_00_after_catch(); - const std::vector problems = Pokemon::lint_story_checkpoint(data); - if (!problems.empty()){ - return problems[0]; - } - if (!data.start || Pokemon::tiles_of(*data.start).empty()){ - return "00.04b has no start tiles."; - } - const TilePos save_spot = story_maps().to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); - std::vector at = Pokemon::tiles_of(*data.start); - for (const StoryStep& step : data.steps){ - std::string error; - switch (step.kind){ - case StoryStepKind::ROUTE: - error = plan_route(logger, at, step); - break; - case StoryStepKind::LEGS: - error = walk_legs(at, step); - break; - case StoryStepKind::SHOP: - // The clerk's way: out of the Mart's door. (The lady's ends - // on the save spot, where the save step stands too.) - at = story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKE_MART_DOOR); - break; - case StoryStepKind::SAVE: - if (!step.target || step.target->map != MAP_ROUTE_202 || at != std::vector{save_spot}){ - error = step.name + ": expected a save on Route 202's save spot, from" + describe(at) + "."; - } - break; - default: - break; - } - if (!error.empty()){ - return error; - } - if (step.pose_after){ - at = Pokemon::tiles_of(*step.pose_after); - } - // From both save spots at once the planner gives up: the walk - // north must start from the one. - if (step.name == "back_to_route_202" && at != std::vector{save_spot}){ - return step.name + ": expected to end on the save spot."; - } - } - for (const TilePos& pos : at){ - if (!story_at_jubilife_exit(pos)){ - return "00.04b's walk north may end off Jubilife's exit, at" + describe({pos}) + "."; - } - } - return true; - } -}; - -} - - -void add_tests_AutoStory_Segment00Steps(UnitTestDatabase& database){ - database.add(); -} - - - -} -} -} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h deleted file mode 100644 index e6108e9f73..0000000000 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h +++ /dev/null @@ -1,26 +0,0 @@ -/* BDSP AutoStory Segment 00 Steps Tests - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_Tests_H -#define PokemonAutomation_PokemonBDSP_AutoStory_Segment00Steps_Tests_H - -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -// Offline checks of 00.04b as story steps: it lints clean, and every route -// plans on the story maps from where the step before leaves the player. -void add_tests_AutoStory_Segment00Steps(UnitTestDatabase& database); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp new file mode 100644 index 0000000000..c777a6a8ba --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -0,0 +1,484 @@ +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + * A run of the whole story table, new game to Floaroma, without the + * console: segment_00_table() and segment_01_table() built into segments + * as the program builds them (story_table_segment()), run by the shared + * runner with the program's retry policy, against a fake game. The fake + * plans each ROUTE on the built-in JSON maps from where the run's pose + * says we stand, walks each LEGS tile by tile on them, and passes every + * other step unless told to fail it. Steps that still call an old + * function pass as a whole. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Navigation/Pokemon_MapSet.h" +#include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" +#include "Pokemon/Navigation/Pokemon_TileGrid.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_00.h" +#include "StorySegments/PokemonBDSP_AutoStory_Segment_01.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "Utils/PokemonBDSP_AutoStory_StoryExecutor.h" +#include "Utils/PokemonBDSP_AutoStory_StoryTables.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StorySimulation_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using Pokemon::Outcome; +using Pokemon::RunState; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + +std::string describe(const std::vector& tiles){ + std::string text; + for (const TilePos& pos : tiles){ + const std::optional local = story_maps().to_local(pos); + text += " " + (local ? local->map + " " + Pokemon::to_string(local->pos) : Pokemon::to_string(pos)); + } + return text; +} + +std::string joined(const std::vector& lines){ + std::string text; + for (const std::string& line : lines){ + text += "\n " + line; + } + return text; +} + +bool contains(const std::vector& tiles, const TilePos& pos){ + return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); +} + +// Where a ROUTE step ends from `from`, planned as ConsoleStoryExecutor +// plans it, or why it does not plan. +struct Walked{ + std::vector at; + std::string error; + std::string plan; +}; +Walked plan_route(const std::vector& from, const Pokemon::StoryStep& step){ + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + return {{}, step.name + ": no plan costs named \"" + step.variant + "\".", ""}; + } + const std::vector goal = Pokemon::tiles_of(*step.target); + const auto plan = Pokemon::plan_robust_route(story_maps().world(), from, + [&](const TilePos& pos){ return contains(goal, pos); }, *costs); + if (!plan){ + return {{}, step.name + ": no plan from" + describe(from) + ".", ""}; + } + Walked walked{from, "", std::format("{}: {} legs from{}", step.name, plan->size(), describe(from))}; + for (const Pokemon::PlannedLeg& leg : *plan){ + walked.plan += "\n " + to_string(leg.leg) + " to" + describe(leg.after); + } + if (!plan->empty()){ + walked.at = plan->back().after; + } + for (const TilePos& pos : walked.at){ + if (!contains(goal, pos)){ + return {{}, step.name + ": the plan may end off the target, at" + describe({pos}) + ".", walked.plan}; + } + } + return walked; +} + +// Where a LEGS step ends from `from`, walked tile by tile on the maps, a +// tile short too where the step allows it. +Walked walk_legs(const std::vector& from, const Pokemon::StoryStep& step){ + const Pokemon::TileGrid& world = story_maps().world(); + std::vector at = from; + for (const Pokemon::StoryLeg& leg : step.legs){ + std::vector next; + for (TilePos pos : at){ + for (int i = 0; i < leg.tiles; i++){ + const std::optional stepped = world.step(pos, leg.direction); + if (!stepped){ + return {{}, step.name + ": " + direction_name(leg.direction) + " is blocked at" + describe({pos}) + ".", ""}; + } + if (step.variant == "one_short_ok" && i + 1 == leg.tiles){ + next.push_back(pos); + } + pos = *stepped; + } + next.push_back(pos); + } + at = std::move(next); + } + return {at, "", ""}; +} + + +// Saves at a row's start into the trace, as the console runner saves. +class FakeRunner : public StoryStepRunner{ +public: + std::vector& trace; + std::set saved; + + explicit FakeRunner(std::vector& trace) + : trace(trace) + {} + virtual bool saved_at_start(const std::string& id) const override{ + return saved.contains(id); + } + virtual Outcome save_at_start(const std::string& id) override{ + saved.insert(id); + trace.push_back("save " + id); + return Pokemon::outcome_ok(); + } + virtual Outcome run_step(const std::string&, StoryFailure, const StoryAction&) override{ + return Pokemon::outcome_failed("no_console"); + } +}; + +// The game: steps pass, routes and legs move the pose on the maps, and +// the steps in `failures` ("/") fail, so many times, as said. +class FakeGame : public Pokemon::StoryStepExecutor{ +public: + struct Failure{ + std::string failure; + size_t times = 0; + }; + + std::vector& trace; + std::map failures; + // Every route's plan, for the log. + std::vector plans; + + explicit FakeGame(std::vector& trace) + : trace(trace) + {} + + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ + m_row = data.id; + trace.push_back("begin " + data.id); + } + virtual Outcome run(RunState& state, const Pokemon::StoryStep& step) override{ + const std::string key = m_row + "/" + step.name; + trace.push_back(key); + auto failure = failures.find(key); + if (failure != failures.end() && failure->second.times > 0){ + failure->second.times--; + return Pokemon::outcome_failed(failure->second.failure, key); + } + return Pokemon::StoryStepExecutor::run(state, step); + } + virtual Outcome route(RunState& state, const Pokemon::StoryStep& step) override{ + const Walked walked = plan_route(Pokemon::tiles_of(state.pose), step); + if (!walked.error.empty()){ + return Pokemon::outcome_failed("no_plan", walked.error); + } + plans.push_back(walked.plan); + return Pokemon::outcome_ok(Pokemon::tile_pose(step.target->map, walked.at)); + } + virtual Outcome legs(RunState& state, const Pokemon::StoryStep& step) override{ + const Walked walked = walk_legs(Pokemon::tiles_of(state.pose), step); + if (!walked.error.empty()){ + return Pokemon::outcome_failed("blocked", walked.error); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, walked.at)); + } + virtual Outcome step(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome mash(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome battle(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome train(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome heal(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome shop(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + virtual Outcome save(RunState&, const Pokemon::StoryStep&) override{ + trace.push_back("save in " + m_row); + return Pokemon::outcome_ok(); + } + virtual Outcome game(RunState&, const Pokemon::StoryStep&) override{ return Pokemon::outcome_ok(); } + +private: + std::string m_row; +}; + + +// One simulated run of both segments, as the program runs them. +struct Simulation{ + std::vector trace; + std::vector lines; + FakeRunner runner{trace}; + std::shared_ptr game = std::make_shared(trace); + Pokemon::SegmentPolicy policy; + + Pokemon::SegmentReport run(const std::string& id, const std::vector& table, const Pokemon::SegmentRange& range){ + Pokemon::Segment segment = story_table_segment(runner, id, table, game); + Pokemon::SegmentHooks hooks; + hooks.reload = [this](RunState&){ + trace.push_back("reload"); + return Pokemon::outcome_ok(); + }; + hooks.log = [this](const std::string& line){ lines.push_back(line); }; + RunState state; + return Pokemon::run_segment(state, segment, range, policy, hooks); + } + // The rows begun, in order, one per attempt. + std::vector begun() const{ + std::vector ids; + for (const std::string& item : trace){ + if (item.starts_with("begin ")){ + ids.push_back(item.substr(6)); + } + } + return ids; + } + size_t count(const std::string& item) const{ + return (size_t)std::count(trace.begin(), trace.end(), item); + } + bool logged(const std::string& line) const{ + return std::find(lines.begin(), lines.end(), line) != lines.end(); + } +}; + +std::vector ids_of(const std::vector& table){ + std::vector ids; + for (const StoryRow& row : table){ + ids.push_back(row.id); + } + return ids; +} + + +UnitTestResult test_whole_table(Logger& logger){ + Simulation sim; + const Pokemon::SegmentReport first = sim.run("00", segment_00_table(), {}); + const Pokemon::SegmentReport second = sim.run("01", segment_01_table(), {}); + for (const std::string& plan : sim.game->plans){ + logger.log(plan); + } + if (!first.completed || !second.completed){ + return "Expected both segments through:" + joined(sim.lines); + } + std::vector expected = ids_of(segment_00_table()); + for (const std::string& id : ids_of(segment_01_table())){ + expected.push_back(id); + } + if (sim.begun() != expected){ + return "Unexpected checkpoint order:" + joined(sim.begun()); + } + // Every row saves first but 00.00, which a failure ends; 01.10 and + // 01.11 save again at their ends. + for (const std::string& id : expected){ + const size_t saves = sim.count("save " + id); + if (saves != (id == "00.00" ? 0 : 1)){ + return std::format("{} saved {} times at its start:{}", id, saves, joined(sim.trace)); + } + } + if (sim.count("save in 01.10") != 1 || sim.count("save in 01.11") != 1){ + return "Expected 01.10 and 01.11 to save at their ends:" + joined(sim.trace); + } + if (first.one_shot_passes() != first.checkpoints_run || second.one_shot_passes() != second.checkpoints_run){ + return "Expected every checkpoint one-shot:" + joined(sim.lines); + } + return true; +} + +UnitTestResult test_start_and_end(Logger&){ + Simulation sim; + Pokemon::SegmentReport report = sim.run("00", segment_00_table(), {.from = "00.04b", .to = "00.04d"}); + if (!report.completed || sim.begun() != std::vector{"00.04b", "00.04c", "00.04d"}){ + return "Expected 00.04b to 00.04d only:" + joined(sim.begun()); + } + Simulation tail; + report = tail.run("01", segment_01_table(), {.from = "01.10", .to = std::nullopt}); + if (!report.completed || tail.begun() != std::vector{"01.10", "01.11"}){ + return "Expected 01.10 to the end:" + joined(tail.begun()); + } + Simulation one; + report = one.run("00", segment_00_table(), {.from = "00.02", .to = "00.02"}); + if (!report.completed || one.begun() != std::vector{"00.02"}){ + return "Expected 00.02 alone:" + joined(one.begun()); + } + return true; +} + +// r72: a blackout in 00.04b's training. The checkpoint resets to the save +// it made at its start, does not save again, and walks from that save's +// pose to the grass again. +UnitTestResult test_training_blackout(Logger&){ + Simulation sim; + sim.game->failures["00.04b/train"] = {.failure = "blackout", .times = 1}; + const Pokemon::SegmentReport report = sim.run("00", segment_00_table(), {.from = "00.04b", .to = "00.04d"}); + const std::vector expected{"00.04b", "00.04b", "00.04c", "00.04d"}; + if (!report.completed || report.reloads != 1 || sim.begun() != expected){ + return "Expected 00.04b reloaded once, then on:" + joined(sim.trace); + } + if (sim.count("save 00.04b") != 1 || sim.count("00.04b/to_catch_grass") != 2){ + return "Expected the retry to start from 00.04b's one save and walk to the grass again:" + joined(sim.trace); + } + if (!sim.logged("[Checkpoint] 00.04b: passed on attempt 2 (1 reset)")){ + return "Expected the retried pass logged:" + joined(sim.lines); + } + return true; +} + +// A step that fails every time: three attempts from the checkpoint's +// save, then the segment stops on it and runs nothing after. +UnitTestResult test_gives_up_after_attempts(Logger&){ + Simulation sim; + sim.game->failures["01.04/oreburgh_gym"] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport report = sim.run("01", segment_01_table(), {}); + if (report.completed || report.reloads != 2 || report.failed_checkpoint != "01.04" || report.failed_event != "oreburgh_gym" + || report.runs.empty() || report.runs.back().attempts != 3 || sim.count("01.04/oreburgh_gym") != 3 + || sim.count("save 01.04") != 1 || sim.count("begin 01.05") != 0 + ){ + return "Expected 01.04 tried 3 times from its save, then a stop:" + joined(sim.trace) + joined(sim.lines); + } + // The stop-at-first-failure option: no reload. + Simulation lab; + lab.policy.stop_at_first_failure = true; + lab.game->failures["01.04/oreburgh_gym"] = {.failure = "operation_failed", .times = 10}; + const Pokemon::SegmentReport stopped = lab.run("01", segment_01_table(), {}); + if (stopped.completed || !stopped.stopped_at_first_failure || lab.count("reload") != 0){ + return "Expected a stop at the first failure without a reload:" + joined(lab.trace); + } + return true; +} + +// Each row that has a start pose starts where the row before it leaves +// the player, by the same planning and walking the fake run does. +UnitTestResult test_poses_chain(Logger&){ + for (const std::vector* table : {&segment_00_table(), &segment_01_table()}){ + std::optional> at; + std::string last = ""; + for (const StoryRow& row : *table){ + if (row.start){ + const std::vector start = Pokemon::tiles_of(*row.start); + if (at && !std::ranges::all_of(*at, [&](const TilePos& pos){ return contains(start, pos); })){ + return std::format("{} starts at{}, but {} may end at{}.", row.id, describe(start), last, describe(*at)); + } + at = start; + }else{ + at.reset(); + } + for (const Pokemon::StoryStep& step : row.steps){ + if (step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS){ + if (!at){ + return row.id + " " + step.name + ": walks from an unknown pose."; + } + const Walked walked = step.kind == StoryStepKind::ROUTE ? plan_route(*at, step) : walk_legs(*at, step); + if (!walked.error.empty()){ + return row.id + " " + walked.error; + } + at = walked.at; + }else if (step.kind == StoryStepKind::GAME || step.kind == StoryStepKind::SHOP){ + // A call to an old function: where it ends is the table's to say. + at.reset(); + } + if (step.pose_after){ + at = Pokemon::tiles_of(*step.pose_after); + } + } + last = row.id; + } + } + return true; +} + +// Every step that calls an old function names one the segment registers, +// and each table lints clean. +UnitTestResult test_variants_registered(Logger&){ + const struct{ const std::vector* table; std::map variants; } segments[] = { + {.table = &segment_00_table(), .variants = segment_00_variants()}, + {.table = &segment_01_table(), .variants = segment_01_variants()}, + }; + const std::set built_in{"", "town", "robust", "one_short_ok"}; + for (const auto& segment : segments){ + for (const StoryRow& row : *segment.table){ + const std::vector problems = Pokemon::lint_story_checkpoint(row); + if (!problems.empty()){ + return problems[0]; + } + for (const Pokemon::StoryStep& step : row.steps){ + if (!built_in.contains(step.variant) && !segment.variants.contains(step.variant)){ + return std::format("{} {}: no variant \"{}\" is registered.", row.id, step.name, step.variant); + } + } + } + } + return true; +} + +// r261: out of the Center the walk saw no dark fade and walked on three +// rows. The walk stops at the first look that is not a room, fade or not. +UnitTestResult test_door_exit_looks(Logger&){ + const struct{ const char* name; std::vector> looks; size_t stops_at; } cases[] = { + {.name = "no fade: the first look outside", .looks = {{180, true}, {175, true}, {230, false}}, .stops_at = 2}, + {.name = "the fade, then outside", .looks = {{180, true}, {40, true}, {210, false}}, .stops_at = 2}, + {.name = "a dim look outside", .looks = {{180, true}, {150, false}, {230, false}}, .stops_at = 1}, + {.name = "still a room", .looks = {{180, true}, {40, true}, {190, true}}, .stops_at = 3}, + }; + for (const auto& c : cases){ + DoorFadeWatch watch; + size_t stopped = c.looks.size(); + for (size_t i = 0; i < c.looks.size(); i++){ + if (door_walk_stops(watch, c.looks[i].first, c.looks[i].second)){ + stopped = i; + break; + } + } + if (stopped != c.stops_at){ + return std::format("{}: stopped at look {}, expected {}.", c.name, stopped, c.stops_at); + } + } + return true; +} + + +class Test_StorySimulation : public UnitTest{ +public: + Test_StorySimulation(std::string name, std::function body) + : UnitTest("PokemonBDSP::AutoStory - Simulation: " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + +} + + +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database){ + database.add("00.00 to Floaroma, in order, each row saved first", test_whole_table); + database.add("start and end checkpoints", test_start_and_end); + database.add("a blackout in 00.04b's training resets to its save", test_training_blackout); + database.add("a step that keeps failing stops after 3 attempts", test_gives_up_after_attempts); + database.add("each start pose is where the row before ends; routes plan on the maps", test_poses_chain); + database.add("every called function is registered; tables lint", test_variants_registered); + database.add("out of a door: the first look that is not a room", test_door_exit_looks); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h new file mode 100644 index 0000000000..b893232ca3 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h @@ -0,0 +1,27 @@ +/* BDSP AutoStory Story Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StorySimulation_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// Segments 00 and 01 run end to end without a console: the story table, +// the shared segment runner and its retry policy, against a fake game +// that plans every route and walk on the built-in maps. +void add_tests_AutoStory_StorySimulation(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp new file mode 100644 index 0000000000..cc6e2f4c9e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -0,0 +1,252 @@ +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Data/PokemonBDSP_AutoStory_Route202.h" +#include "Data/PokemonBDSP_AutoStory_SandgemTown.h" +#include "Data/PokemonBDSP_AutoStory_StoryMaps.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::Direction; +using Pokemon::Pose; +using Pokemon::StoryLeg; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +namespace{ + +constexpr Direction NORTH = Direction::NORTH; +constexpr Direction WEST = Direction::WEST; + +TilePos world(const char* map, const TilePos& local){ + return story_maps().to_world(map, local); +} + +// A pose on any of `tiles`, world tiles of story_maps(). +Pose on(const char* map, std::vector tiles, std::optional facing = std::nullopt){ + return Pokemon::tile_pose(map, std::move(tiles), facing); +} + +Pose save_spot(){ + return on(MAP_ROUTE_202, {world(MAP_ROUTE_202, Route202::SAVE_SPOT)}); +} + +// The catch grass training walks into: the tall grass of the training area. +std::vector catch_grass(){ + std::vector grass = story_after_training_area(); + std::erase_if(grass, [](const TilePos& pos){ + return story_maps().world().at(pos) != Pokemon::TileType::TALL_GRASS; + }); + return grass; +} + +// Route 202's open rows at the top: north from any of them is Jubilife. +std::vector jubilife_exit(){ + std::vector exit; + const Pokemon::TileGrid& grid = Route202::grid(); + for (int y = 0; y <= 3; y++){ + for (int x = 0; x < grid.width(); x++){ + const TilePos pos = world(MAP_ROUTE_202, {x, y}); + if (story_maps().world().standable(pos) && story_at_jubilife_exit(pos)){ + exit.push_back(pos); + } + } + } + return exit; +} + + +StoryStep make(StoryStepKind kind, std::string name, std::string why){ + StoryStep step; + step.kind = kind; + step.name = std::move(name); + step.why = std::move(why); + return step; +} + +// A step that calls the old function named `name` (the segment's variant). +StoryStep call(std::string name, std::string why, std::optional pose_after = std::nullopt){ + StoryStep step = make(StoryStepKind::GAME, name, std::move(why)); + step.variant = std::move(name); + step.pose_after = std::move(pose_after); + return step; +} + +StoryStep route(std::string name, Pose target, std::string why, std::string costs = ""){ + StoryStep step = make(StoryStepKind::ROUTE, std::move(name), std::move(why)); + step.target = std::move(target); + step.variant = std::move(costs); + return step; +} + +StoryStep legs(std::string name, std::vector legs, std::string why, std::string variant = ""){ + StoryStep step = make(StoryStepKind::LEGS, std::move(name), std::move(why)); + step.legs = std::move(legs); + step.variant = std::move(variant); + return step; +} + +StoryStep train(size_t battles, Pose pose_after, std::string why){ + StoryStep step = make(StoryStepKind::TRAIN, "train", std::move(why)); + step.battles = battles; + step.pose_after = std::move(pose_after); + return step; +} + +StoryStep heal(std::string name, Pose pose_after, std::string why){ + StoryStep step = make(StoryStepKind::HEAL, std::move(name), std::move(why)); + step.pose_after = std::move(pose_after); + return step; +} + +StoryStep with_variant(StoryStep step, std::string variant){ + step.variant = std::move(variant); + return step; +} + +StoryRow row(std::string id, std::string title, std::optional start, std::vector steps, + StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY +){ + StoryRow ret; + ret.id = std::move(id); + ret.title = std::move(title); + ret.start = std::move(start); + ret.steps = std::move(steps); + ret.on_failure = on_failure; + return ret; +} + + +std::vector make_segment_00(){ + const int to_center_door = SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x; + const Pose center_door_front = on(MAP_SANDGEM_TOWN, story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR)); + StoryStep potions = make(StoryStepKind::SHOP, "potions", + "Potions before Route 202's trainers: the Poke Mart lady's 10 (she stands still, as in the decomp), the clerk's 5 after two misses"); + potions.items = {{.item = "potion", .quantity = 10}}; + potions.variant = "sandgem_potions"; + potions.pose_after = save_spot(); + StoryStep stairs = make(StoryStepKind::STEP, "into_jubilife", + "Jubilife's south edge is a wall with stairs in it, west of where the route may leave us (r78)"); + stairs.direction = NORTH; + stairs.variant = "jubilife_stairs"; + + return { + row("00.00", "Verify Settings", std::nullopt, { + call("verify_settings", "before any route movement or checkpoint save: fail visibly if options cannot be read"), + }, StoryFailure::STOP), + row("00.01", "After TV", std::nullopt, { + call("tutorial_part_1_navigation", "intro and starter selection: Butters' recorded stick holds to the lake"), + }), + row("00.02", "Outside Verity before starter", std::nullopt, { + call("starter_nav", "into the lake's scene"), + call("select_starter", "the starter from the program's option"), + with_variant(make(StoryStepKind::BATTLE, "fight_starly", "the Starly battle"), "fight_starly"), + with_variant(make(StoryStepKind::MASH, "post_starly_cleanup", "the scene after the battle"), "post_starly_cleanup"), + }), + row("00.03", "Outside Verity after starter", std::nullopt, { + call("tutorial_part_2_navigation", "Pokedex acquisition: back through Route 201 to Rowan's lab"), + }), + row("00.04", "After Pokedex, to the catch", std::nullopt, { + call("tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", + on(MAP_ROUTE_202, story_catch_save_area())), + }), + // A Lv5 starter fainted to Route 202's trainers (r72): training, a + // heal, and Potions first, each row from the save the one before ends on. + row("00.04b", "After the catch: train and heal in Sandgem", on(MAP_ROUTE_202, story_catch_save_area()), { + route("to_catch_grass", on(MAP_ROUTE_202, catch_grass()), "into the training column's grass"), + train(3, on(MAP_ROUTE_202, story_after_training_area()), "three wild battles with the best move"), + // One walk round the houses ended two tiles short (r252): down + // the road first, then the town plan. + route("into_sandgem", on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP)}), + "south down the road to above the Mart", "robust"), + route("to_center_sign", on(MAP_SANDGEM_TOWN, {world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Direction::SOUTH))}), + "below the Center's sign, its doorway in sight", "town"), + // From below the sign the finder read the signs' panels as + // doorways (r252, r255): taps instead. One tile short is fine. + legs("below_center_door", {{.direction = WEST, .tiles = to_center_door}}, "taps west below the Center's door", "one_short_ok"), + heal("heal_sandgem", center_door_front, "Sandgem Town Pokemon Center"), + }), + row("00.04c", "Potions, back to Route 202's save spot", center_door_front, { + potions, + }), + // From the one save spot: from both at once the planner gives up. + row("00.04d", "Route 202's trainers, Dawn in Jubilife", save_spot(), { + route("north_to_jubilife", on(MAP_ROUTE_202, jubilife_exit()), "north past Route 202's trainers, fighting who sees us"), + stairs, + make(StoryStepKind::MASH, "dawn_scene", "Dawn walks over while we stand still"), + }), + row("00.05", "Jubilife After Dawn", std::nullopt, { + call("tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC"), + }), + }; +} + +std::vector make_segment_01(){ + StoryStep save = make(StoryStepKind::SAVE, "save_here", "the next checkpoint starts here"); + return { + row("01.01", "Jubilife to Barry", std::nullopt, { + call("jubilife_to_barry", "from the Center's PC out to Barry on Route 203"), + }), + row("01.02", "Route 203 to Oreburgh", std::nullopt, { + call("route203_to_oreburgh", "east through the Oreburgh Gate into Oreburgh City"), + }), + row("01.03", "Oreburgh Mine to Roark", std::nullopt, { + call("oreburgh_to_roark", "into the Mine to Roark and back to the Center"), + }), + row("01.04", "Oreburgh Gym", std::nullopt, { + call("oreburgh_gym", "the Gym's trainers and Roark for the Coal Badge"), + }), + row("01.05", "Oreburgh to Route 203", std::nullopt, { + call("oreburgh_to_route203", "heal, then west out of the Oreburgh Gate"), + }), + row("01.06", "Route 203 to Jubilife", std::nullopt, { + call("route203_to_jubilife", "west along Route 203 into Jubilife"), + }), + row("01.07", "Jubilife through Rowan's scene", std::nullopt, { + call("jubilife_to_rowan", "to the north street, Rowan's scene and the tag battle"), + }), + row("01.08", "Jubilife to the Ravaged Path", std::nullopt, { + call("jubilife_to_ravaged_path", "up Route 204's south half into the Ravaged Path"), + }), + row("01.09", "Ravaged Path", std::nullopt, { + call("ravaged_path", "through the cave with Rock Smash, out onto Route 204 North"), + }), + row("01.10", "Route 204 to Floaroma", std::nullopt, { + call("route204_to_floaroma", "the human's line north into Floaroma"), + save, + }), + row("01.11", "Floaroma's old couple", std::nullopt, { + call("floaroma_old_couple", "the old woman and the old man in the flower field"), + save, + }), + }; +} + +} + + +const std::vector& segment_00_table(){ + static const std::vector table = make_segment_00(); + return table; +} + +const std::vector& segment_01_table(){ + static const std::vector table = make_segment_01(); + return table; +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h new file mode 100644 index 0000000000..53a92dd035 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h @@ -0,0 +1,42 @@ +/* BDSP AutoStory - Story table + * + * From: https://github.com/PokemonAutomation/ + * + * Segments 00 and 01, new game to Floaroma, as rows of the shared story + * steps (Pokemon/Navigation/Pokemon_StorySteps.h): plain data, as FRLG's + * table is, so the tests can plan its routes and run it against a fake + * game. Each row saves at its start, which is where the row before it + * ends. + * + * Steps that have no declarative form yet call one of the old functions: + * a GAME step (or another kind with a variant) names it, and the segment + * registers it with its executor (segment_00_variants(), + * segment_01_variants()). Those are the next to split. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryTable_H + +#include +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// 00.00 to 00.05: the settings, the lake, the Pokedex, the catch, Sandgem +// and Route 202 (00.04b to 00.04d as story steps), Jubilife to the Poketch. +const std::vector& segment_00_table(); + +// 01.01 to 01.11: Jubilife to Oreburgh and the Coal Badge, back, and north +// to Floaroma's old couple. +const std::vector& segment_01_table(); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp index 4584f48280..82dcc701ed 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp @@ -31,7 +31,7 @@ class DescribeOnlyRunner : public StoryStepRunner{ virtual Pokemon::Outcome save_at_start(const std::string&) override{ return Pokemon::outcome_failed("not_run"); } - virtual Pokemon::Outcome run_step(const std::string&, StoryFailure, const StoryStep&) override{ + virtual Pokemon::Outcome run_step(const std::string&, StoryFailure, const StoryAction&) override{ return Pokemon::outcome_failed("not_run"); } }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp index 9c117daa5a..38766fd39b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Tests.cpp @@ -1416,12 +1416,14 @@ class Test_PokemonCenterInside : public UnitTest{ class FakeMartLady : public MartLadyActions{ public: enum class Where{ - DOORS_ROW, LAB_EAST, PATH_WEST, GRASS_EDGE, TOP_EDGE, LADY, + DOORS_ROW, SHORT_OF_LAB, LAB_EAST, PATH_WEST, GRASS_EDGE, TOP_EDGE, LADY, SIGNS_ROW, SHORT_OF_201, ROUTE_201_WEST, PATH_EAST, MART_TREES_LOW, MART_TREES, ROUTE_202, }; // Walks back east that a wild battle ends short of the sign. size_t short_walks = 0; + // Walks west along the doors' row that a townsperson ends short of the Lab's sign. + size_t short_lab_walks = 0; // Talks to the lady before she is there to answer. size_t lady_away = 0; Where where = Where::DOORS_ROW; @@ -1451,6 +1453,7 @@ class FakeMartLady : public MartLadyActions{ trace.push_back("go: " + label); const struct{ Where from; Direction direction; Where to; } walks[] = { {Where::DOORS_ROW, Direction::WEST, Where::LAB_EAST}, + {Where::SHORT_OF_LAB, Direction::WEST, Where::LAB_EAST}, {Where::PATH_WEST, Direction::WEST, Where::GRASS_EDGE}, {Where::GRASS_EDGE, Direction::NORTH, Where::TOP_EDGE}, {Where::LADY, Direction::WEST, Where::TOP_EDGE}, @@ -1467,6 +1470,10 @@ class FakeMartLady : public MartLadyActions{ short_walks--; where = Where::SHORT_OF_201; } + if (where == Where::LAB_EAST && short_lab_walks > 0 && walk.from == Where::DOORS_ROW){ + short_lab_walks--; + where = Where::SHORT_OF_LAB; + } break; } } @@ -1548,9 +1555,12 @@ class Test_MartLadyEvents : public UnitTest{ } return all; }; + const std::vector TO_LAB = {"go: West to the Lab's sign", "read: the Lab's sign"}; + const std::vector LAB_TO_LADY = {"step: South onto the path", "go: West along the path", "go: North to the trees"}; struct Case{ const char* name; size_t short_walks; + size_t short_lab_walks = 0; size_t lady_away; bool completed; size_t failed_event; @@ -1560,6 +1570,7 @@ class Test_MartLadyEvents : public UnitTest{ { .name = "Straight through", .short_walks = 0, + .short_lab_walks = 0, .lady_away = 0, .completed = true, .failed_event = 0, @@ -1568,14 +1579,25 @@ class Test_MartLadyEvents : public UnitTest{ { .name = "Back east, a tile short of the sign", .short_walks = 1, + .short_lab_walks = 0, .lady_away = 0, .completed = true, .failed_event = 0, .trace = join({TO_LADY, LADY, TO_SIGN, SIGN, {"go: On to the sign"}, SIGN, HOME}), }, + { + .name = "West, a tile short of the Lab's sign: re-anchored on it", + .short_walks = 0, + .short_lab_walks = 1, + .lady_away = 0, + .completed = true, + .failed_event = 0, + .trace = join({TO_LAB, {"go: On to the sign", "read: the Lab's sign"}, LAB_TO_LADY, LADY, TO_SIGN, SIGN, HOME}), + }, { .name = "The lady answers the second time", .short_walks = 0, + .short_lab_walks = 0, .lady_away = 1, .completed = true, .failed_event = 0, @@ -1584,6 +1606,7 @@ class Test_MartLadyEvents : public UnitTest{ { .name = "The lady never answers", .short_walks = 0, + .short_lab_walks = 0, .lady_away = 5, .completed = false, .failed_event = 6, @@ -1593,6 +1616,7 @@ class Test_MartLadyEvents : public UnitTest{ for (const Case& c : cases){ FakeMartLady game; game.short_walks = c.short_walks; + game.short_lab_walks = c.short_lab_walks; game.lady_away = c.lady_away; Pokemon::Checkpoint checkpoint = sandgem_mart_lady_checkpoint(game); const std::vector problems = Pokemon::lint_checkpoint(checkpoint); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp index 3ed2023a6b..fc21fa61ad 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.cpp @@ -16,6 +16,8 @@ #include "../Utils/PokemonBDSP_AutoStory_PokemonCenter.h" #include "../Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "../Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" +#include "../Utils/PokemonBDSP_AutoStory_StoryExecutor.h" +#include "../PokemonBDSP_AutoStory_StoryTable.h" #include "../Utils/PokemonBDSP_AutoStory_Walk.h" #include "../Utils/PokemonBDSP_OverworldMatchers.h" @@ -620,40 +622,6 @@ namespace PokemonAutomation { align_to_tutorial_landmarks(stream, context, match_sandgem_junction, "Sandgem junction"); } - // What the post-catch half of 00.04 does, logged up front and as each - // step starts. A Lv5 Piplup fainted to Route 202's trainers (r72), so: - // training, a heal at the Center, and Potions (they also saved the - // Barry fight in r85) before Route 202's trainers: the Poke Mart lady's - // 10 on Route 201 (she stands still, as in the decomp), the clerk's 5 after two misses. - static const char* const AFTER_CATCH_STEPS[] = { - "Train: fight 3 wild Pokemon in the catch grass, with the best move", - "Walk south into Sandgem Town and heal at its Pokemon Center", - "Get 10 Potions from the Poke Mart lady on Route 201 (the clerk's 5 after two misses), and check the Bag", - "Walk back to Route 202's save spot (00.04d saves there)", - "Walk north up Route 202 to the Jubilife exit, fighting trainers who see us; after each, use a Potion if HP is under half", - "Enter Jubilife and get through Dawn's scene", - }; - constexpr size_t AFTER_CATCH_STEP_COUNT = sizeof(AFTER_CATCH_STEPS) / sizeof(AFTER_CATCH_STEPS[0]); - - static void log_after_catch_steps(VideoStream& stream) { - std::string text = "[00.04] After the catch, the steps are:"; - for (size_t i = 0; i < AFTER_CATCH_STEP_COUNT; i++) { - text += "\n " + std::to_string(i + 1) + ". " + AFTER_CATCH_STEPS[i]; - } - stream.log(text, COLOR_BLUE); - } - - static void after_catch_step(VideoStream& stream, size_t number) { - const std::string count = std::to_string(number) + "/" + std::to_string(AFTER_CATCH_STEP_COUNT); - std::string text = "[00.04 step " + count + "] " + AFTER_CATCH_STEPS[number - 1]; - text += number < AFTER_CATCH_STEP_COUNT ? std::string(" -- next: ") + AFTER_CATCH_STEPS[number] : std::string(" -- last step"); - stream.log(text, COLOR_BLUE); - add_wrapped_log(stream.overlay(), "00.04 step " + count + ": " + AFTER_CATCH_STEPS[number - 1], COLOR_WHITE); - if (number < AFTER_CATCH_STEP_COUNT) { - add_wrapped_log(stream.overlay(), "Next: " + std::string(AFTER_CATCH_STEPS[number]), COLOR_GRAY); - } - } - // Fights a trainer whose dialogue has opened, with whatever starter we // have. Someone who only talks leaves no battle to fight. static void fight_route_trainer(VideoStream& stream, ProControllerContext& context, const std::string& name) { @@ -674,60 +642,6 @@ namespace PokemonAutomation { use_potion_first_pokemon(stream, context, 0.5); } - // The starter starts around level 5 with only a Normal move; the Route - // 202 trainers need a level or two more. Starts from either save: the - // catch spot (00.03's save, in the grass) or the save spot under the - // ledge. - static void train_in_catch_grass( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector& trainers, const RouteTrainerFight& fight - ) { - // From where the catch's save may stand (a save on the save spot - // has Potions and skips training) into the training column's grass. - const std::vector grass = story_after_training_area(); - walk_route(stream, context, maps, story_catch_save_area(), - [&](const TilePos& pos) { - return maps.world().at(pos) == Pokemon::TileType::TALL_GRASS && - std::find(grass.begin(), grass.end(), pos) != grass.end(); - }, - trainers, fight, "To the catch grass"); - for (size_t battle = 0; battle < 3; battle++) { - route_status(stream, "Training battle " + std::to_string(battle + 1) + "/3"); - battle_1_pokemon(stream, context); - fail_if_blacked_out(stream, context, "Training battle " + std::to_string(battle + 1)); - } - } - - // South into Sandgem Town in two walks (down the road to where it stops - // above the Mart, then the town plan into the Center's sign; one walk - // went round the houses and ended two tiles short, r252), three taps - // west below the door, and in by sight. Returns: below the door. - static std::vector heal_at_sandgem_center( - VideoStream& stream, ProControllerContext& context, const MapSet& maps, - std::vector& trainers, const RouteTrainerFight& fight - ) { - const TilePos entry = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::ROUTE_202_ENTRY_STOP); - const TilePos below_sign = maps.to_world(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_SIGN.next(Pokemon::Direction::SOUTH)); - const std::vector at = walk_route(stream, context, maps, story_after_training_area(), - [&](const TilePos& pos) { return pos == entry; }, trainers, fight, "Into Sandgem Town", {}, Pokemon::RobustPlanCosts()); - walk_route(stream, context, maps, at, - [&](const TilePos& pos) { return pos == below_sign; }, trainers, fight, "To the Pokemon Center", {}, town_plan_costs()); - // Below the door by D-pad taps before lining up by sight: from below - // the sign, the finder read the signs' white panels as doorways and - // stepped between them (r252, r255). We face north, and a tap the - // other way only turns (r256): turn first. One tile short is fine. - route_turn(stream, context, Pokemon::Direction::WEST); - const size_t to_door = (size_t)(SandgemTown::POKEMON_CENTER_SIGN.x - SandgemTown::POKEMON_CENTER_DOOR.x); - const WalkResult west = walk_tiles_dpad(stream, context, -1, 0, to_door, "west below the Center's door"); - stream.log("[00.04] Taps west below the Center's door moved " + std::to_string(west.moved_chunks) + " of " + std::to_string(to_door) + " tiles."); - if (west.moved_chunks + 1 < to_door) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Sandgem Town: the taps west below the Center's door moved " + std::to_string(west.moved_chunks) + " tiles.", stream); - } - heal_at_pokemon_center(stream, context, "Sandgem Town Pokemon Center"); - return story_door_front(MAP_SANDGEM_TOWN, SandgemTown::POKEMON_CENTER_DOOR); - } - // How many Potions to buy. A Potion is 300 and a new game starts with // 3000; the money on screen cannot be read yet. Five heal the starter // through Route 202's four trainers and leave half the money. @@ -915,13 +829,14 @@ namespace PokemonAutomation { walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); } - static void meet_dawn_in_jubilife(VideoStream& stream, ProControllerContext& context) { - // Jubilife's south edge is a wall with stairs in it, west of where the - // route may leave us (r78 stood at its east end). Entering starts a - // scene: Dawn walks over while we stand still. So walk north; if the - // wall stops us and no dialogue follows, the stairs are further west. - bool met_dawn = false; - for (size_t attempt = 0; attempt < 6 && !met_dawn; attempt++) { + // North into Jubilife until Dawn's scene starts (00.04d's + // into_jubilife). Jubilife's south edge is a wall with stairs in it, + // west of where the route may leave us (r78 stood at its east end). + // Entering starts a scene: Dawn walks over while we stand still. So + // walk north; if the wall stops us and no dialogue follows, the stairs + // are further west. The scene itself is the next step. + static void up_jubilife_stairs(VideoStream& stream, ProControllerContext& context) { + for (size_t attempt = 0; attempt < 6; attempt++) { const WalkResult walk = walk_until(stream, context, 0, 1, 4000ms, "to Dawn in Jubilife"); if (walk.stop == WalkStop::TRANSITION) { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, @@ -929,46 +844,28 @@ namespace PokemonAutomation { } ShortDialogWatcher dialog(COLOR_CYAN); if (walk.stop == WalkStop::DIALOG || wait_until(stream, context, 6s, {{dialog}}) == 0) { - met_dawn = true; - break; + return; } stream.log("[00.04] Jubilife: the wall stopped us; looking for the stairs one tile west."); route_turn(stream, context, Pokemon::Direction::WEST); pbf_move_left_joystick(context, {-1, 0}, route_open_hold(Pokemon::Direction::WEST, 1), 200ms); context.wait_for_all_requests(); } - if (!met_dawn) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - "Jubilife: no stairs found and no Dawn after six tiles west.", stream); - } - wait_for_dialogue(stream, context, "Phase 21 (dawn Jubilife dialog)"); - - pbf_mash_button(context, BUTTON_B, 5000ms); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + "Jubilife: no stairs found and no Dawn after six tiles west.", stream); } - // 00.04b, 00.04c and 00.04d: the post-catch steps as three rows, each - // starting from the save the one before ends on. Mart lady runs that - // failed this program run: the clerk after two. + // Mart lady runs that failed this program run: the clerk after two. static size_t g_mart_lady_misses = 0; - // 00.04b, from the catch's save: steps 1 and 2. - static void after_catch_train_and_heal(VideoStream& stream, ProControllerContext& context) { - log_after_catch_steps(stream); - const MapSet& maps = story_maps(); - std::vector trainers = story_trainer_sights(); - const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; - pbf_wait(context, 1500ms); - after_catch_step(stream, 1); - train_in_catch_grass(stream, context, maps, trainers, fight); - after_catch_step(stream, 2); - heal_at_sandgem_center(stream, context, maps, trainers, fight); - } - // 00.04c, from the save out of Sandgem's Center: steps 3 and 4, up to - // Route 202's save spot, where 00.04d's save goes. - static void after_catch_potions(SingleSwitchProgramEnvironment& env, VideoStream& stream, ProControllerContext& context) { - const MapSet& maps = story_maps(); - std::vector trainers = story_trainer_sights(); - const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; - after_catch_step(stream, 3); + // 00.04c's potions step, from the save out of Sandgem's Center: the + // Poke Mart lady's (her checkpoint without its own save), or the + // clerk's after two misses, on to Route 202's save spot, where + // 00.04d's save goes. The Bag must have Potions after. + static Pokemon::Outcome sandgem_potions( + ConsoleStoryExecutor& executor, StoryConsole& c, Pokemon::RunState&, const Pokemon::StoryStep& + ) { + VideoStream& stream = c.env.console; + ProControllerContext& context = c.context; if (g_mart_lady_misses < 2) { // Onto the doors' row first: out of the door we stand a tile to // three below it (r261 walked west into the trees from three). @@ -979,28 +876,15 @@ namespace PokemonAutomation { OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, std::string("Sandgem: north to the Center's front ended ") + walk_stop_name(up.stop) + ".", stream); } - try { mart_lady_without_save(env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } + try { mart_lady_without_save(c.env, context); } catch (OperationFailedException&) { g_mart_lady_misses++; throw; } } else { - const std::vector at = buy_potions_in_sandgem(stream, context, maps, trainers, fight); - after_catch_step(stream, 4); - walk_to_route_202_save_spot(stream, context, maps, at, trainers, fight); + const std::vector at = buy_potions_in_sandgem(stream, context, executor.maps(), executor.trainers(), executor.fight(c)); + walk_to_route_202_save_spot(stream, context, executor.maps(), at, executor.trainers(), executor.fight(c)); } if (!bag_has_item(stream, context, "potion", 1)) { - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after step 3.", stream); + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, "00.04: no Potions in the Bag after the shop.", stream); } - } - // 00.04d, from the save on Route 202's save spot: steps 5 and 6. From - // both save spots at once the planner gives up (no plan within 400000 - // sets; 1300 from the one). - static void after_catch_to_jubilife(VideoStream& stream, ProControllerContext& context) { - const MapSet& maps = story_maps(); - std::vector trainers = story_trainer_sights(); - const RouteTrainerFight fight = [&](const std::string& name) { fight_route_trainer(stream, context, name); }; - after_catch_step(stream, 5); - const TilePos save_spot = maps.to_world(MAP_ROUTE_202, Route202::SAVE_SPOT); - walk_route(stream, context, maps, {save_spot}, story_at_jubilife_exit, trainers, fight, "To Jubilife"); - after_catch_step(stream, 6); - meet_dawn_in_jubilife(stream, context); + return Pokemon::outcome_ok(); } // --------------------------------------------------------------------------- // End of Segment 00 - 04 @@ -1378,71 +1262,30 @@ namespace PokemonAutomation { } } - static StoryCheckpointSpec segment_00_row( - std::string id, std::string title, StoryFailure on_failure, std::vector steps - ) { - return {.id = std::move(id), .title = std::move(title), .on_failure = on_failure, .steps = std::move(steps)}; - } - - // 00.00 to 00.03: the settings, the lake and the Pokedex. - static std::vector segment_00_intro_rows() { - const StoryFailure retry = StoryFailure::RELOAD_AND_RETRY; - return { - segment_00_row("00.00", "Verify Settings", StoryFailure::STOP, { - {"verify_settings", "before any route movement or checkpoint save: fail visibly if options cannot be read, rather than navigate with unknown settings", verify_settings_step}, - }), - segment_00_row("00.01", "After TV", retry, { - {"tutorial_part_1_navigation", "intro and starter selection: Butters' recorded stick holds to the lake", - [](StoryConsole& c) { tutorial_part_1_navigation(c.env, c.env.console, c.context); }}, - }), - segment_00_row("00.02", "Outside Verity before starter", retry, { - {"starter_nav", "into the lake's scene", [](StoryConsole& c) { starter_nav(c.env.console, c.context); }}, - {"select_starter", "the starter from the program's option", select_starter_step}, - {"fight_starly", "the Starly battle", [](StoryConsole& c) { fight_starly(c.env.console, c.context); }}, - {"post_starly_cleanup", "the scene after the battle", [](StoryConsole& c) { post_starly_clenaup(c.env.console, c.context); }}, - }), - segment_00_row("00.03", "Outside Verity after starter", retry, { - {"tutorial_part_2_navigation", "Pokedex acquisition: back through Route 201 to Rowan's lab", - [](StoryConsole& c) { tutorial_part_2_navigation(c.env, c.env.console, c.context); }}, - }), - }; - } - - // 00.04 and 00.05: the catch, Sandgem, and Jubilife to the Poketch. - // 00.04 saves twice: a retry after the catch starts from the - // caught-Pokemon save, not the old route, so it is two rows. - static std::vector segment_00_town_rows() { - const StoryFailure retry = StoryFailure::RELOAD_AND_RETRY; + std::map segment_00_variants() { return { - segment_00_row("00.04", "After Pokedex, to the catch", retry, { - {"tutorial_part_3_navigation", "out of the lab, home to Mom, and the catch on Route 202", - [](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); }}, - }), - segment_00_row("00.04b", "After the catch: train and heal in Sandgem", retry, { - {"train_and_heal", "three wild battles in the catch grass, then Sandgem's Pokemon Center", - [](StoryConsole& c) { after_catch_train_and_heal(c.env.console, c.context); }}, - }), - segment_00_row("00.04c", "Potions, back to Route 202's save spot", retry, { - {"potions", "the Poke Mart lady's (the clerk's after two misses), then Route 202's save spot", - [](StoryConsole& c) { after_catch_potions(c.env, c.env.console, c.context); }}, - }), - segment_00_row("00.04d", "Route 202's trainers, Dawn in Jubilife", retry, { - {"to_jubilife", "north up Route 202 past its trainers, into Jubilife and Dawn's scene", - [](StoryConsole& c) { after_catch_to_jubilife(c.env.console, c.context); }}, - }), - segment_00_row("00.05", "Jubilife After Dawn", retry, { - {"tutorial_part_4_navigation", "the Town Map, the president, the coupons, the Poketch and the Center's PC", - [](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); }}, - }), + {"verify_settings", story_call(verify_settings_step)}, + {"tutorial_part_1_navigation", story_call([](StoryConsole& c) { tutorial_part_1_navigation(c.env, c.env.console, c.context); })}, + {"starter_nav", story_call([](StoryConsole& c) { starter_nav(c.env.console, c.context); })}, + {"select_starter", story_call(select_starter_step)}, + {"fight_starly", story_call([](StoryConsole& c) { fight_starly(c.env.console, c.context); })}, + {"post_starly_cleanup", story_call([](StoryConsole& c) { post_starly_clenaup(c.env.console, c.context); })}, + {"tutorial_part_2_navigation", story_call([](StoryConsole& c) { tutorial_part_2_navigation(c.env, c.env.console, c.context); })}, + {"tutorial_part_3_navigation", story_call([](StoryConsole& c) { tutorial_part_3_navigation(c.env, c.env.console, c.context); })}, + {"sandgem_potions", sandgem_potions}, + {"jubilife_stairs", story_call([](StoryConsole& c) { up_jubilife_stairs(c.env.console, c.context); })}, + {"tutorial_part_4_navigation", story_call([](StoryConsole& c) { tutorial_part_4_navigation(c.env, c.env.console, c.context); })}, }; } + // The table is segment_00_table() (PokemonBDSP_AutoStory_StoryTable.h). Pokemon::Segment make_segment_00(StoryStepRunner& runner) { - std::vector rows = segment_00_intro_rows(); - for (StoryCheckpointSpec& row : segment_00_town_rows()) { - rows.push_back(std::move(row)); - } - return story_segment(runner, "00", std::move(rows)); + const std::vector& table = segment_00_table(); + return story_table_segment(runner, "00", table, std::make_shared( + runner, rows_that_stop(table), story_maps(), story_trainer_sights(), + [](StoryConsole& c, const std::string& name) { fight_route_trainer(c.env.console, c.context, name); }, + segment_00_variants() + )); } // DIAGNOSTIC ONLY: Remove before pushing to production. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h index 4ff7d50b0a..a789a26969 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_00.h @@ -8,9 +8,12 @@ #ifndef PokemonAutomation_PokemonBDSP_AutoStory_Segment00_H #define PokemonAutomation_PokemonBDSP_AutoStory_Segment00_H +#include +#include #include #include "Pokemon/Navigation/Pokemon_Grid.h" #include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h" #include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h" namespace PokemonAutomation { @@ -18,10 +21,15 @@ namespace NintendoSwitch { namespace PokemonBDSP { -// Segment 00 as a table of checkpoints: 00.00 to 00.05, with 00.04 in two -// rows (00.04 to the catch, 00.04b after it), each saving at its start. +// Segment 00 as a table of checkpoints, segment_00_table(): 00.00 to 00.05, +// with 00.04 in four rows (to the catch, then 00.04b to 00.04d as story +// steps), each saving at its start. Pokemon::Segment make_segment_00(StoryStepRunner& runner); +// How segment_00_table()'s steps that call an old function run on the +// console, by their variants. +std::map segment_00_variants(); + // DIAGNOSTIC ONLY: Remove before pushing to production. // Starts at the post-catch save and runs only the rest of checkpoint 00.04. void diagnostic_checkpoint_0004_after_catch(SingleSwitchProgramEnvironment& env, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp index 418cd19abf..00cc3cf623 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.cpp @@ -19,6 +19,7 @@ #include "../Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "../Utils/PokemonBDSP_AutoStory_Segment_00_tools.h" #include "../Utils/PokemonBDSP_AutoStory_Walk.h" +#include "../PokemonBDSP_AutoStory_StoryTable.h" #include "Common/Cpp/PrettyPrint.h" #include "Common/Cpp/Filesystem/Filesystem.h" #include "CommonFramework/Globals.h" @@ -1458,75 +1459,30 @@ void floaroma_old_couple(VideoStream& stream, ProControllerContext& context){ } -namespace{ - -// Saves where a checkpoint ends, inside its attempt: a failed save is a -// failed attempt. -StoryStepSpec save_here(){ - return {"save_here", "the next checkpoint starts here", [](StoryConsole& c){ - checkpoint_save(c.env, c.context, c.options.notif_status_update, c.stats, c.options.checkpoint_save_mode); - }}; -} - -StoryCheckpointSpec segment_01_row(std::string id, std::string title, std::vector steps){ - return {.id = std::move(id), .title = std::move(title), .on_failure = StoryFailure::RELOAD_AND_RETRY, .steps = std::move(steps)}; -} - -// 01.01 to 01.05: Jubilife to Oreburgh, the Mine, the Gym and back out. -std::vector segment_01_oreburgh_rows(){ +std::map segment_01_variants(){ return { - segment_01_row("01.01", "Jubilife to Barry", { - {"jubilife_to_barry", "from the Center's PC out to Barry on Route 203", [](StoryConsole& c){ jubilife_to_barry(c.env.console, c.context); }}, - }), - segment_01_row("01.02", "Route 203 to Oreburgh", { - {"route203_to_oreburgh", "east through the Oreburgh Gate into Oreburgh City", [](StoryConsole& c){ route203_to_oreburgh(c.env.console, c.context); }}, - }), - segment_01_row("01.03", "Oreburgh Mine to Roark", { - {"oreburgh_to_roark", "into the Mine to Roark and back to the Center", [](StoryConsole& c){ oreburgh_to_roark(c.env.console, c.context); }}, - }), - segment_01_row("01.04", "Oreburgh Gym", { - {"oreburgh_gym", "the Gym's trainers and Roark for the Coal Badge", [](StoryConsole& c){ oreburgh_gym(c.env.console, c.context); }}, - }), - segment_01_row("01.05", "Oreburgh to Route 203", { - {"oreburgh_to_route203", "heal, then west out of the Oreburgh Gate", [](StoryConsole& c){ oreburgh_to_route203(c.env.console, c.context); }}, - }), + {"jubilife_to_barry", story_call([](StoryConsole& c){ jubilife_to_barry(c.env.console, c.context); })}, + {"route203_to_oreburgh", story_call([](StoryConsole& c){ route203_to_oreburgh(c.env.console, c.context); })}, + {"oreburgh_to_roark", story_call([](StoryConsole& c){ oreburgh_to_roark(c.env.console, c.context); })}, + {"oreburgh_gym", story_call([](StoryConsole& c){ oreburgh_gym(c.env.console, c.context); })}, + {"oreburgh_to_route203", story_call([](StoryConsole& c){ oreburgh_to_route203(c.env.console, c.context); })}, + {"route203_to_jubilife", story_call([](StoryConsole& c){ route203_to_jubilife(c.env.console, c.context); })}, + {"jubilife_to_rowan", story_call([](StoryConsole& c){ jubilife_to_rowan(c.env.console, c.context); })}, + {"jubilife_to_ravaged_path", story_call([](StoryConsole& c){ jubilife_to_ravaged_path(c.env.console, c.context); })}, + {"ravaged_path", story_call([](StoryConsole& c){ ravaged_path(c.env.console, c.context); })}, + {"route204_to_floaroma", story_call([](StoryConsole& c){ route204_to_floaroma(c.env.console, c.context); })}, + {"floaroma_old_couple", story_call([](StoryConsole& c){ floaroma_old_couple(c.env.console, c.context); })}, }; } -// 01.06 to 01.11: back through Jubilife, north to Floaroma. -std::vector segment_01_floaroma_rows(){ - return { - segment_01_row("01.06", "Route 203 to Jubilife", { - {"route203_to_jubilife", "west along Route 203 into Jubilife", [](StoryConsole& c){ route203_to_jubilife(c.env.console, c.context); }}, - }), - segment_01_row("01.07", "Jubilife through Rowan's scene", { - {"jubilife_to_rowan", "to the north street, Rowan's scene and the tag battle", [](StoryConsole& c){ jubilife_to_rowan(c.env.console, c.context); }}, - }), - segment_01_row("01.08", "Jubilife to the Ravaged Path", { - {"jubilife_to_ravaged_path", "up Route 204's south half into the Ravaged Path", [](StoryConsole& c){ jubilife_to_ravaged_path(c.env.console, c.context); }}, - }), - segment_01_row("01.09", "Ravaged Path", { - {"ravaged_path", "through the cave with Rock Smash, out onto Route 204 North", [](StoryConsole& c){ ravaged_path(c.env.console, c.context); }}, - }), - segment_01_row("01.10", "Route 204 to Floaroma", { - {"route204_to_floaroma", "the human's line north into Floaroma", [](StoryConsole& c){ route204_to_floaroma(c.env.console, c.context); }}, - save_here(), - }), - segment_01_row("01.11", "Floaroma's old couple", { - {"floaroma_old_couple", "the old woman and the old man in the flower field", [](StoryConsole& c){ floaroma_old_couple(c.env.console, c.context); }}, - save_here(), - }), - }; -} - -} - +// The table is segment_01_table() (PokemonBDSP_AutoStory_StoryTable.h). Its +// steps only call functions, so the executor plans nothing itself. Pokemon::Segment make_segment_01(StoryStepRunner& runner){ - std::vector rows = segment_01_oreburgh_rows(); - for (StoryCheckpointSpec& row : segment_01_floaroma_rows()){ - rows.push_back(std::move(row)); - } - return story_segment(runner, "01", std::move(rows)); + const std::vector& table = segment_01_table(); + return story_table_segment(runner, "01", table, std::make_shared( + runner, rows_that_stop(table), story_maps(), std::vector{}, + [](StoryConsole&, const std::string&){}, segment_01_variants() + )); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h index 1236c9d995..ccb65f4ca8 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StorySegments/PokemonBDSP_AutoStory_Segment_01.h @@ -9,6 +9,9 @@ #define PokemonAutomation_PokemonBDSP_AutoStory_Segment01_H #include "../PokemonBDSP_AutoStoryTools.h" +#include +#include +#include "../Utils/PokemonBDSP_AutoStory_StoryExecutor.h" #include "../Utils/PokemonBDSP_AutoStory_StoryTables.h" namespace PokemonAutomation{ @@ -16,9 +19,13 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ -// Segment 01 as a table of checkpoints: 01.01 to 01.11. +// Segment 01 as a table of checkpoints, segment_01_table(): 01.01 to 01.11. Pokemon::Segment make_segment_01(StoryStepRunner& runner); +// How segment_01_table()'s steps that call an old function run on the +// console, by their variants. +std::map segment_01_variants(); + class AutoStory_Segment_01 : public AutoStory_Segment{ public: virtual std::string name() const override; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp index f1ac20cb6e..a2381cb209 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.cpp @@ -850,6 +850,12 @@ bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room){ return watch.saw_dark && !room && brightness >= DOOR_OUTSIDE_BRIGHT; } +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room){ + // Both are asked: the fade watch keeps what it has seen. + const bool faded = door_fade_look(watch, brightness, room); + return faded || !room; +} + namespace{ // Three looks half a second apart, none a room: outside, whatever the fade @@ -882,7 +888,7 @@ void leave_door_south(VideoStream& stream, ProControllerContext& context, const auto out_of_door = [&](const ImageViewRGB32& screen){ const double brightness = image_stats(screen).average.sum(); const bool room = indoors(screen); - const bool out = door_fade_look(watch, brightness, room) || !room; + const bool out = door_walk_stops(watch, brightness, room); stream.log(std::format("[Route] {}: look: brightness {:.0f}, {}, fade {}.", label, brightness, room ? "a room" : "not a room", watch.saw_dark ? "seen" : "not seen")); return out; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h index 5dc613496a..8e98f0dfbc 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h @@ -211,6 +211,10 @@ struct DoorFadeWatch{ // returns whether we are out: a dark look seen, and now a bright one that // is not a room. bool door_fade_look(DoorFadeWatch& watch, double brightness, bool room); +// Whether leave_door_south()'s walk stops at this look: at the door's fade +// out (door_fade_look()), or at the first look that is not a room, so a +// held stick does not walk on outside (r261 three rows). +bool door_walk_stops(DoorFadeWatch& watch, double brightness, bool room); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp index 60b13ab45a..ee928ed8c0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -55,12 +55,12 @@ std::pair dpad_of(Direction direction){ ConsoleStoryExecutor::ConsoleStoryExecutor( - StoryStepRunner& runner, std::string checkpoint, + StoryStepRunner& runner, std::set stop_rows, const Pokemon::MapSet& maps, std::vector trainers, TrainerFight fight, std::map variants ) : m_runner(runner) - , m_checkpoint(std::move(checkpoint)) + , m_stop_rows(std::move(stop_rows)) , m_maps(maps) , m_all_trainers(std::move(trainers)) , m_trainers(m_all_trainers) @@ -68,7 +68,8 @@ ConsoleStoryExecutor::ConsoleStoryExecutor( , m_variants(std::move(variants)) {} -void ConsoleStoryExecutor::begin_attempt(){ +void ConsoleStoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ + m_checkpoint = data.id; m_trainers = m_all_trainers; } @@ -87,7 +88,8 @@ StoryConsole& ConsoleStoryExecutor::console(){ Outcome ConsoleStoryExecutor::run(RunState& state, const Pokemon::StoryStep& step){ Outcome result = Pokemon::outcome_ok(); - const Outcome ran = m_runner.run_step(m_checkpoint, StoryFailure::RELOAD_AND_RETRY, [&](StoryConsole& console){ + const StoryFailure on_failure = m_stop_rows.contains(m_checkpoint) ? StoryFailure::STOP : StoryFailure::RELOAD_AND_RETRY; + const Outcome ran = m_runner.run_step(m_checkpoint, on_failure, [&](StoryConsole& console){ m_console = &console; const auto variant = m_variants.find(step.variant); result = variant == m_variants.end() @@ -169,10 +171,9 @@ Outcome ConsoleStoryExecutor::heal(RunState&, const Pokemon::StoryStep& step){ return Pokemon::outcome_ok(); } -Outcome ConsoleStoryExecutor::save(RunState&, const Pokemon::StoryStep& step){ +Outcome ConsoleStoryExecutor::save(RunState&, const Pokemon::StoryStep&){ StoryConsole& c = console(); - c.env.console.log(std::format("[{}] Saving: {}.", m_checkpoint, step.why)); - save_game(c.env, c.context, step.target ? step.target->map : ""); + checkpoint_save(c.env, c.context, c.options.notif_status_update, c.stats, c.options.checkpoint_save_mode); return Pokemon::outcome_ok(); } @@ -185,24 +186,11 @@ Outcome ConsoleStoryExecutor::mash(RunState&, const Pokemon::StoryStep& step){ } -Pokemon::CheckpointSpec story_steps_checkpoint( - StoryStepRunner& runner, const Pokemon::StoryCheckpoint& data, - std::shared_ptr executor -){ - // The table's own row with no steps: its save at the start. - Pokemon::CheckpointSpec row = story_checkpoint(runner, StoryCheckpointSpec{ - .id = data.id, .title = data.title, .on_failure = StoryFailure::RELOAD_AND_RETRY, .steps = {}, - }); - row.build = [save = std::move(row.build), &data, executor = std::move(executor)]() mutable{ - executor->begin_attempt(); - Pokemon::Checkpoint checkpoint = save(); - Pokemon::Checkpoint steps = Pokemon::build_story_checkpoint(data, *executor); - for (Pokemon::Event& event : steps.events){ - checkpoint.events.push_back(std::move(event)); - } - return checkpoint; +ConsoleStoryExecutor::Variant story_call(StoryAction action){ + return [action = std::move(action)](ConsoleStoryExecutor&, StoryConsole& console, RunState&, const Pokemon::StoryStep&){ + action(console); + return Pokemon::outcome_ok(); }; - return row; } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h index c27c10f8e2..48a07aa476 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include "Pokemon/Navigation/Pokemon_MapSet.h" @@ -59,14 +60,15 @@ class ConsoleStoryExecutor : public Pokemon::StoryStepExecutor{ // Fights a trainer whose dialogue has opened. using TrainerFight = std::function; + // `stop_rows`: the rows whose failure ends the program (rows_that_stop()). ConsoleStoryExecutor( - StoryStepRunner& runner, std::string checkpoint, + StoryStepRunner& runner, std::set stop_rows, const Pokemon::MapSet& maps, std::vector trainers, TrainerFight fight, std::map variants ); - // A new attempt of the checkpoint: every trainer can see us again. - void begin_attempt(); + // A new attempt of a row: every trainer can see us again. + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override; const Pokemon::MapSet& maps() const{ return m_maps; @@ -88,7 +90,7 @@ class ConsoleStoryExecutor : public Pokemon::StoryStepExecutor{ // In front of a Pokemon Center, its lit doorway on screen: in, the // nurse, and out (heal_at_pokemon_center()). virtual Pokemon::Outcome heal(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; - // Saves; the save screen must read the target's map, if there is one. + // The checkpoint save (checkpoint_save(), real or fake as the options say). virtual Pokemon::Outcome save(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; // A dialogue opens (or has), then B through it. virtual Pokemon::Outcome mash(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; @@ -97,6 +99,7 @@ class ConsoleStoryExecutor : public Pokemon::StoryStepExecutor{ StoryConsole& console(); StoryStepRunner& m_runner; + std::set m_stop_rows; std::string m_checkpoint; const Pokemon::MapSet& m_maps; std::vector m_all_trainers; @@ -108,14 +111,9 @@ class ConsoleStoryExecutor : public Pokemon::StoryStepExecutor{ }; -// `data` as a row of a story table: it saves at its start like the -// table's other rows, then runs its steps with `executor`, which starts a -// new attempt each time the row is built. `data` must outlive the -// segment; the row keeps the executor. -Pokemon::CheckpointSpec story_steps_checkpoint( - StoryStepRunner& runner, const Pokemon::StoryCheckpoint& data, - std::shared_ptr executor -); +// A variant that calls an old console function, for a step that has no +// declarative form yet. +ConsoleStoryExecutor::Variant story_call(StoryAction action); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp index 71fa836117..4ff9530c50 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.cpp @@ -42,47 +42,46 @@ Event save_event(StoryStepRunner& runner, const std::string& id){ return event; } -Event step_event(StoryStepRunner& runner, const std::string& id, StoryFailure on_failure, StoryStepSpec step){ - Event event; - event.name = std::move(step.name); - event.why = std::move(step.why); - event.act = [&runner, id, on_failure, run = std::move(step.step)](RunState&){ - return runner.run_step(id, on_failure, run); - }; - return event; -} - -} - - -Pokemon::CheckpointSpec story_checkpoint(StoryStepRunner& runner, StoryCheckpointSpec spec){ - Pokemon::CheckpointSpec row; - row.id = spec.id; - row.title = spec.title; - row.build = [&runner, spec = std::move(spec)](){ - Checkpoint checkpoint; - checkpoint.id = spec.id; - if (spec.on_failure == StoryFailure::RELOAD_AND_RETRY){ - checkpoint.events.push_back(save_event(runner, spec.id)); - } - for (const StoryStepSpec& step : spec.steps){ - checkpoint.events.push_back(step_event(runner, spec.id, spec.on_failure, step)); - } - return checkpoint; - }; - return row; } -Pokemon::Segment story_segment(StoryStepRunner& runner, std::string id, std::vector rows){ +Pokemon::Segment story_table_segment( + StoryStepRunner& runner, std::string id, const std::vector& rows, + std::shared_ptr executor +){ Pokemon::Segment segment; segment.id = std::move(id); - for (StoryCheckpointSpec& row : rows){ - segment.checkpoints.push_back(story_checkpoint(runner, std::move(row))); + for (const StoryRow& row : rows){ + segment.checkpoints.push_back(Pokemon::CheckpointSpec{ + .id = row.id, .title = row.title, .saves = false, + .build = [&runner, &row, executor]{ + Checkpoint steps = Pokemon::build_story_checkpoint(row, *executor); + if (row.on_failure != StoryFailure::RELOAD_AND_RETRY){ + return steps; + } + Checkpoint checkpoint; + checkpoint.id = row.id; + checkpoint.events.push_back(save_event(runner, row.id)); + for (Event& event : steps.events){ + checkpoint.events.push_back(std::move(event)); + } + return checkpoint; + }, + }); } return segment; } +std::set rows_that_stop(const std::vector& rows){ + std::set ids; + for (const StoryRow& row : rows){ + if (row.on_failure == StoryFailure::STOP){ + ids.insert(row.id); + } + } + return ids; +} + ConsoleStoryRunner::ConsoleStoryRunner(StoryConsole console) : m_console(console) @@ -102,7 +101,7 @@ Outcome ConsoleStoryRunner::save_at_start(const std::string& id){ }); } -Outcome ConsoleStoryRunner::run_step(const std::string& id, StoryFailure on_failure, const StoryStep& step){ +Outcome ConsoleStoryRunner::run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step){ if (on_failure == StoryFailure::STOP){ step(m_console); return Pokemon::outcome_ok(); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h index fbd9e03b9c..165cc3cbd9 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryTables.h @@ -18,10 +18,12 @@ #include #include +#include #include #include #include #include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" #include "PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h" namespace PokemonAutomation{ @@ -29,20 +31,15 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ -// What a step runs on. +// What a step runs on on the console. struct StoryConsole{ SingleSwitchProgramEnvironment& env; ProControllerContext& context; const AutoStoryOptions& options; AutoStoryStats& stats; }; -using StoryStep = std::function; - -struct StoryStepSpec{ - std::string name; - std::string why; - StoryStep step; -}; +// A console action: what a step that calls an old function runs. +using StoryAction = std::function; // What a checkpoint does when a step fails. enum class StoryFailure{ @@ -53,15 +50,13 @@ enum class StoryFailure{ STOP, }; -struct StoryCheckpointSpec{ - std::string id; - std::string title; +// A row of a story table: the shared checkpoint, and what a failure does. +struct StoryRow : public Pokemon::StoryCheckpoint{ StoryFailure on_failure = StoryFailure::RELOAD_AND_RETRY; - std::vector steps; }; -// How a table's events reach the game: the console runner below, or a -// fake in the tests, which only describe the tables. +// How a table saves at a row's start and runs a console action: the +// console runner below, or a fake in the tests. class StoryStepRunner{ public: virtual ~StoryStepRunner() = default; @@ -69,14 +64,20 @@ class StoryStepRunner{ // starts from that save and does not save again. virtual bool saved_at_start(const std::string& id) const = 0; virtual Pokemon::Outcome save_at_start(const std::string& id) = 0; - virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryStep& step) = 0; + virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step) = 0; }; -// A row of a segment table: builds the checkpoint's events from `spec`. -Pokemon::CheckpointSpec story_checkpoint(StoryStepRunner& runner, StoryCheckpointSpec spec); +// The table `rows` (kept alive by the caller) as segment `id`: each row +// that retries saves first, through `runner`, unless this run has saved it +// already (a retry starts from that save); then its steps run with +// `executor`, which the segment keeps. +Pokemon::Segment story_table_segment( + StoryStepRunner& runner, std::string id, const std::vector& rows, + std::shared_ptr executor +); -// A segment table of `rows`, in order. -Pokemon::Segment story_segment(StoryStepRunner& runner, std::string id, std::vector rows); +// The ids of the rows that stop on a failure instead of retrying. +std::set rows_that_stop(const std::vector& rows); using StorySegmentFactory = std::function; @@ -88,7 +89,7 @@ class ConsoleStoryRunner : public StoryStepRunner{ virtual bool saved_at_start(const std::string& id) const override; virtual Pokemon::Outcome save_at_start(const std::string& id) override; - virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryStep& step) override; + virtual Pokemon::Outcome run_step(const std::string& id, StoryFailure on_failure, const StoryAction& step) override; // Runs the rows of `segment` in `range`; throws as the checkpoint // loop did once a checkpoint has used up its attempts. diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index b86734c1a1..141c16a04a 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1576,8 +1576,6 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_OreburghGym.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Lines.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Segment00Steps.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Route204.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_SandgemTown.h @@ -1591,10 +1589,12 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Indoors_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.cpp - Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00Steps_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTables_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_NewGame.cpp From 79cc16157cd82aaa33a9fe257c17a04342d91605 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 10:17:39 -0800 Subject: [PATCH 06/13] BDSP AutoStory: initialize AutoStoryOptions::policy for its positional initializer Windows clang: missing field 'policy' initializer (-Wmissing-field-initializers). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h index 143e2b6316..5567e8ea9b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h @@ -141,7 +141,7 @@ struct AutoStoryOptions { CheckpointSaveMode checkpoint_save_mode = CheckpointSaveMode::REAL; CheckpointRetryScope checkpoint_retry_scope = CheckpointRetryScope::CHECKPOINT; // How the story tables retry a checkpoint (StoryRunOptions). - Pokemon::SegmentPolicy policy; + Pokemon::SegmentPolicy policy = {}; }; From 03298de07a55ff9b6e14c433de99c0649bd229b6 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 10:32:43 -0800 Subject: [PATCH 07/13] Pokemon navigation: a shared checks level for story steps (Thorough or Fast) StoryChecks: THOROUGH steps read the screen to confirm what they did (purchases, the Bag, status after battles, position); FAST trusts counts and timings. Each game decides what its steps check at each level. The executor carries it (checks(), set_checks(); THOROUGH by default), and StoryRunOptions gains a "Checks" dropdown, Thorough by default, read with checks(). Tests: the level reaches a step; the option group's defaults. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Pokemon/Navigation/Pokemon_StorySteps.cpp | 11 +++++ .../Pokemon/Navigation/Pokemon_StorySteps.h | 22 ++++++++++ .../Navigation/Pokemon_StorySteps_Tests.cpp | 40 +++++++++++++++++++ .../Options/Pokemon_StoryRunOptions.cpp | 19 +++++++++ .../Pokemon/Options/Pokemon_StoryRunOptions.h | 12 ++++-- 5 files changed, 101 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp index 75422b02cc..c9825391b0 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.cpp @@ -51,6 +51,17 @@ const char* story_step_kind_name(StoryStepKind kind){ } +const char* story_checks_name(StoryChecks checks){ + switch (checks){ + case StoryChecks::THOROUGH: + return "thorough"; + case StoryChecks::FAST: + return "fast"; + } + return "?"; +} + + Outcome unsupported_step(const StoryStep& step){ return outcome_failed("unsupported_step", std::format("{} {}", story_step_kind_name(step.kind), step.name)); } diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h index 0c877739ad..99b9b74583 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h @@ -55,6 +55,16 @@ enum class StoryStepKind{ }; const char* story_step_kind_name(StoryStepKind kind); +// How much a step reads the screen to confirm what it did. Each game +// decides what its steps check at each level. +enum class StoryChecks{ + // Confirm on screen: purchases, the Bag, status after battles, position. + THOROUGH, + // In a rush: trust counts and timings. + FAST, +}; +const char* story_checks_name(StoryChecks checks); + // A straight leg of a fixed walk. struct StoryLeg{ @@ -124,6 +134,15 @@ class StoryStepExecutor{ // who can see us again, the row's own settings). Does nothing by default. virtual void begin_attempt(const StoryCheckpoint& data); + // The checks level the program's options chose (StoryRunOptions); + // THOROUGH unless set. Steps read it to decide what to confirm. + StoryChecks checks() const{ + return m_checks; + } + void set_checks(StoryChecks checks){ + m_checks = checks; + } + // Runs `step`: dispatch_story_step() by default. A game overrides it // to wrap every step, e.g. to turn a failed console step into a failed // Outcome. @@ -148,6 +167,9 @@ class StoryStepExecutor{ virtual Outcome save(RunState& state, const StoryStep& step); virtual Outcome ready(RunState& state, const StoryStep& step); virtual Outcome game(RunState& state, const StoryStep& step); + +private: + StoryChecks m_checks = StoryChecks::THOROUGH; }; // Calls the executor's method for the step's kind. diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp index bb73f89676..9886671245 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp @@ -17,6 +17,7 @@ #include #include #include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Options/Pokemon_StoryRunOptions.h" #include "Pokemon_StoryEvents.h" #include "Pokemon_StorySegments.h" #include "Pokemon_StorySteps.h" @@ -456,6 +457,43 @@ UnitTestResult test_policy_recovers_in_place(){ return true; } +// The checks level: THOROUGH by default, and what the program sets is +// what a step reads. +UnitTestResult test_checks_level(){ + class CheckingExecutor : public FakeExecutor{ + public: + std::vector seen; + virtual Outcome talk(RunState&, const StoryStep& step) override{ + seen.push_back(step.name + " " + story_checks_name(checks())); + return outcome_ok(); + } + }; + StoryCheckpoint data = lab_checkpoint(); + CheckingExecutor executor; + run(data, executor); + executor.set_checks(StoryChecks::FAST); + run(data, executor); + if (executor.seen != std::vector{"talk_oak thorough", "talk_oak fast"}){ + return "Expected the talk to read thorough, then fast:" + joined(executor.seen); + } + return true; +} + +// The program's option group: 3 attempts, no stop, Thorough, by default; +// Fast when chosen. +UnitTestResult test_run_options(){ + StoryRunOptions options; + const SegmentPolicy policy = options.policy(); + if (policy.attempts_per_checkpoint != 3 || policy.stop_at_first_failure || options.checks() != StoryChecks::THOROUGH){ + return "Expected 3 attempts, no stop at the first failure, and Thorough checks by default."; + } + options.CHECKS.set(StoryChecks::FAST); + if (options.checks() != StoryChecks::FAST){ + return "Expected Fast checks once chosen."; + } + return true; +} + UnitTestResult test_lint(){ StoryStep no_target = make(StoryStepKind::ROUTE, "walk"); StoryStep again = make(StoryStepKind::TALK, "walk"); @@ -507,6 +545,8 @@ void add_tests_StorySteps(UnitTestDatabase& database){ database.add("unsupported kinds and lost battles", test_unsupported_and_lost_battles); database.add("tables as a segment, with a reload", test_segment_of_tables); database.add("lint", test_lint); + database.add("the checks level reaches the steps", test_checks_level); + database.add("the run options' defaults", test_run_options); database.add("policy: attempts per checkpoint, then fail", test_policy_attempts_then_fail); database.add("policy: stop at the first failure", test_policy_stop_at_first_failure); database.add("policy: recover in place before reloading", test_policy_recovers_in_place); diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp index ceccb40638..6f90320428 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp @@ -10,6 +10,14 @@ namespace PokemonAutomation{ namespace Pokemon{ +const EnumDropdownDatabase& StoryChecks_Database(){ + static const EnumDropdownDatabase database({ + {StoryChecks::THOROUGH, "thorough", "Thorough: confirm purchases, the Bag, status and position on screen"}, + {StoryChecks::FAST, "fast", "Fast: trust counts and timings"}, + }); + return database; +} + StoryRunOptions::StoryRunOptions() : GroupOption("Checkpoint Retries", LockMode::LOCK_WHILE_RUNNING) , ATTEMPTS_PER_CHECKPOINT( @@ -22,9 +30,16 @@ StoryRunOptions::StoryRunOptions() LockMode::LOCK_WHILE_RUNNING, false ) + , CHECKS( + "Checks:
Thorough reads the screen to confirm what each step did; Fast trusts counts and timings, for a run in a rush.", + StoryChecks_Database(), + LockMode::LOCK_WHILE_RUNNING, + StoryChecks::THOROUGH + ) { PA_ADD_OPTION(ATTEMPTS_PER_CHECKPOINT); PA_ADD_OPTION(STOP_AT_FIRST_FAILURE); + PA_ADD_OPTION(CHECKS); } SegmentPolicy StoryRunOptions::policy() const{ @@ -34,6 +49,10 @@ SegmentPolicy StoryRunOptions::policy() const{ return policy; } +StoryChecks StoryRunOptions::checks() const{ + return CHECKS; +} + } diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h index 652ddd44ab..f32fdbad1a 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -2,9 +2,10 @@ * * From: https://github.com/PokemonAutomation/ * - * The options every AutoStory program shows for how hard to try: the - * same group in each game, read into the shared SegmentPolicy - * (Pokemon/Navigation/Pokemon_StorySegments.h). + * The options every AutoStory program shows for how hard to try and how + * much to check: the same group in each game, read into the shared + * SegmentPolicy (Pokemon/Navigation/Pokemon_StorySegments.h) and the + * executor's checks level (Pokemon_StorySteps.h). * */ @@ -13,9 +14,11 @@ #include #include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/EnumDropdownOption.h" #include "Common/Cpp/Options/GroupOption.h" #include "Common/Cpp/Options/SimpleIntegerOption.h" #include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -28,10 +31,13 @@ class StoryRunOptions : public GroupOption{ // The policy these options set. In-place recoveries are the game's to // set, not the user's. SegmentPolicy policy() const; + // The checks level for the game's story step executor (set_checks()). + StoryChecks checks() const; public: SimpleIntegerOption ATTEMPTS_PER_CHECKPOINT; BooleanCheckBoxOption STOP_AT_FIRST_FAILURE; + EnumDropdownOption CHECKS; }; From c1e9a37eae6d735a80746a1891d928be63da78c6 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 10:33:17 -0800 Subject: [PATCH 08/13] BDSP AutoStory: hand the shared checks option to the story executor AutoStoryOptions carries it; ConsoleStoryExecutor sets it before each step. Nothing reads it yet. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp | 1 + .../Programs/AutoStory/PokemonBDSP_AutoStoryTools.h | 3 +++ .../AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp | 1 + 3 files changed, 5 insertions(+) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp index fd8c024af3..a17464ce34 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory.cpp @@ -550,6 +550,7 @@ void BDSPAutoStory::program( DIAGNOSTIC_FAKE_CHECKPOINT_SAVE ? CheckpointSaveMode::FAKE : CheckpointSaveMode::REAL }; options.policy = RETRIES.policy(); + options.checks = RETRIES.checks(); const bool fake_saves = options.checkpoint_save_mode == CheckpointSaveMode::FAKE; if (fake_saves) options.checkpoint_retry_scope = CheckpointRetryScope::DEFER_TO_SELECTED_RANGE; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h index 5567e8ea9b..9435548d14 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStoryTools.h @@ -15,6 +15,7 @@ #include "Common/Cpp/Options/EnumDropdownOption.h" #include "Pokemon/Navigation/Pokemon_StorySegments.h" +#include "Pokemon/Navigation/Pokemon_StorySteps.h" #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTools/ImageBoxes.h" @@ -142,6 +143,8 @@ struct AutoStoryOptions { CheckpointRetryScope checkpoint_retry_scope = CheckpointRetryScope::CHECKPOINT; // How the story tables retry a checkpoint (StoryRunOptions). Pokemon::SegmentPolicy policy = {}; + // What the story steps confirm on screen (StoryRunOptions). + Pokemon::StoryChecks checks = Pokemon::StoryChecks::THOROUGH; }; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp index ee928ed8c0..3b3e738b9e 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -91,6 +91,7 @@ Outcome ConsoleStoryExecutor::run(RunState& state, const Pokemon::StoryStep& ste const StoryFailure on_failure = m_stop_rows.contains(m_checkpoint) ? StoryFailure::STOP : StoryFailure::RELOAD_AND_RETRY; const Outcome ran = m_runner.run_step(m_checkpoint, on_failure, [&](StoryConsole& console){ m_console = &console; + set_checks(console.options.checks); const auto variant = m_variants.find(step.variant); result = variant == m_variants.end() ? Pokemon::StoryStepExecutor::run(state, step) From daf7b8faf9d7da7fcefeb58277fee4a10cd85ee2 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 11:07:04 -0800 Subject: [PATCH 09/13] Pokemon navigation: story steps take the game's language, and never need it StoryRunOptions gains "Game language" (any language the games have, English by default, never refused for lacking a reader), and the executor carries it (language(), set_language()) for the few reads that need text: item names in a shop or the Bag. The header states the rule: steps never match dialogue or menu text, and a game with no reader for the language falls back to language-free checks. Tests: the option's choices and the executor's language. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Pokemon/Navigation/Pokemon_StorySteps.h | 18 ++++++++++++ .../Navigation/Pokemon_StorySteps_Tests.cpp | 12 ++++++++ .../Options/Pokemon_StoryRunOptions.cpp | 28 +++++++++++++++++++ .../Pokemon/Options/Pokemon_StoryRunOptions.h | 5 ++++ 4 files changed, 63 insertions(+) diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h index 99b9b74583..84972ead25 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps.h @@ -14,6 +14,13 @@ * implements the kinds it uses, and a step the game has no way of doing * fails by name instead of doing something else. * + * Steps never match dialogue or menu text: they look for screen state (a + * dialogue box, an arrow, a fade, a battle menu, colours, positions). The + * few reads that need text (item names in a shop or the Bag) take the + * game's language from the executor, and with no reader for it fall back + * to checks that need none (a row's position, digits, a changed screen), + * so a run never fails because its language is not supported. + * * Poses are the game's own (Pose, with a TilePose for these games): a * checkpoint's start, a scene's end and a route's target are poses, and * an executor that knows where a step left the player says so in its @@ -28,6 +35,7 @@ #include #include #include +#include "CommonFramework/Language.h" #include "Pokemon_Grid.h" #include "Pokemon_StoryEvents.h" #include "Pokemon_TilePose.h" @@ -142,6 +150,15 @@ class StoryStepExecutor{ void set_checks(StoryChecks checks){ m_checks = checks; } + // The game's language (StoryRunOptions), for the reads that need text; + // English unless set. A game with no reader for it uses language-free + // checks instead. + Language language() const{ + return m_language; + } + void set_language(Language language){ + m_language = language; + } // Runs `step`: dispatch_story_step() by default. A game overrides it // to wrap every step, e.g. to turn a failed console step into a failed @@ -170,6 +187,7 @@ class StoryStepExecutor{ private: StoryChecks m_checks = StoryChecks::THOROUGH; + Language m_language = Language::English; }; // Calls the executor's method for the step's kind. diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp index 9886671245..4e44b6e3b3 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_StorySteps_Tests.cpp @@ -472,7 +472,11 @@ UnitTestResult test_checks_level(){ CheckingExecutor executor; run(data, executor); executor.set_checks(StoryChecks::FAST); + executor.set_language(Language::Korean); run(data, executor); + if (executor.language() != Language::Korean){ + return "Expected the language set on the executor."; + } if (executor.seen != std::vector{"talk_oak thorough", "talk_oak fast"}){ return "Expected the talk to read thorough, then fast:" + joined(executor.seen); } @@ -491,6 +495,14 @@ UnitTestResult test_run_options(){ if (options.checks() != StoryChecks::FAST){ return "Expected Fast checks once chosen."; } + // Every game language can be chosen, read or not. + if (options.language() != Language::English){ + return "Expected English by default."; + } + options.LANGUAGE.set(Language::Japanese); + if (options.language() != Language::Japanese || !options.check_validity().empty()){ + return "Expected Japanese to be chosen and valid, reader or not."; + } return true; } diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp index 6f90320428..c68a783002 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp @@ -18,6 +18,22 @@ const EnumDropdownDatabase& StoryChecks_Database(){ return database; } +const EnumDropdownDatabase& StoryLanguage_Database(){ + static const EnumDropdownDatabase database({ + {Language::English, "en", "English"}, + {Language::Japanese, "ja", "Japanese"}, + {Language::Spanish, "es", "Spanish"}, + {Language::French, "fr", "French"}, + {Language::German, "de", "German"}, + {Language::Italian, "it", "Italian"}, + {Language::Korean, "ko", "Korean"}, + {Language::ChineseSimplified, "zh-Hans", "Chinese (Simplified)"}, + {Language::ChineseTraditional, "zh-Hant", "Chinese (Traditional)"}, + }); + return database; +} + + StoryRunOptions::StoryRunOptions() : GroupOption("Checkpoint Retries", LockMode::LOCK_WHILE_RUNNING) , ATTEMPTS_PER_CHECKPOINT( @@ -36,10 +52,18 @@ StoryRunOptions::StoryRunOptions() LockMode::LOCK_WHILE_RUNNING, StoryChecks::THOROUGH ) + , LANGUAGE( + "Game language:
For the few reads that need text (item names in a shop or the Bag). " + "Story steps never match dialogue text; with no reader for this language they check positions, digits and the screen instead.", + StoryLanguage_Database(), + LockMode::LOCK_WHILE_RUNNING, + Language::English + ) { PA_ADD_OPTION(ATTEMPTS_PER_CHECKPOINT); PA_ADD_OPTION(STOP_AT_FIRST_FAILURE); PA_ADD_OPTION(CHECKS); + PA_ADD_OPTION(LANGUAGE); } SegmentPolicy StoryRunOptions::policy() const{ @@ -53,6 +77,10 @@ StoryChecks StoryRunOptions::checks() const{ return CHECKS; } +Language StoryRunOptions::language() const{ + return LANGUAGE; +} + } diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h index f32fdbad1a..4f9a3a506b 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -33,11 +33,16 @@ class StoryRunOptions : public GroupOption{ SegmentPolicy policy() const; // The checks level for the game's story step executor (set_checks()). StoryChecks checks() const; + // The game's language, for the executor (set_language()). + Language language() const; public: SimpleIntegerOption ATTEMPTS_PER_CHECKPOINT; BooleanCheckBoxOption STOP_AT_FIRST_FAILURE; EnumDropdownOption CHECKS; + // Any language the game has, whether or not a reader supports it: + // steps without one use language-free checks, so none is refused. + EnumDropdownOption LANGUAGE; }; From c7a2136f1472d94eb7f73b32ea75b49827ec9be0 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 1 Nov 2026 11:08:11 -0800 Subject: [PATCH 10/13] Pokemon navigation: the route log lines, shared by the games' walkers BDSP AutoStory's route lines (the plan, each leg as it starts, a replan where the player was seen, a trainer stopping us, the arrival), lifted from PokemonBDSP_AutoStory_RouteWalk.cpp so that every game's walker logs the same lines, with what other games need: tiles of named maps rather than BDSP's world tiles, one tile or a set, legs onto a warp, Surf and turns, and two new lines: where a leg ended and how that is known (walked, assumed after an interrupt, unknown), and where the player was found again after a stop. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../ComputerPrograms_UnitTestRunner.cpp | 1 + .../Navigation/Pokemon_Navigation_Tests.h | 1 + .../Pokemon/Navigation/Pokemon_RouteLog.cpp | 98 +++++++++++ .../Pokemon/Navigation/Pokemon_RouteLog.h | 120 +++++++++++++ .../Navigation/Pokemon_RouteLog_Tests.cpp | 162 ++++++++++++++++++ SerialPrograms/cmake/SourceFiles.cmake | 3 + 6 files changed, 385 insertions(+) create mode 100644 SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.cpp create mode 100644 SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.h create mode 100644 SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog_Tests.cpp diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index 2e17398c46..a3a4cc2fcd 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -63,6 +63,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ Kernels::add_tests(ret); Pokemon::add_tests_LegPlanner(ret); Pokemon::add_tests_MapData(ret); + Pokemon::add_tests_RouteLog(ret); Pokemon::add_tests_StoryEvents(ret); Pokemon::add_tests_StorySegments(ret); NintendoSwitch::add_tests_CheckOnlineDetector(ret); diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h index 73eac9841a..3d51443174 100644 --- a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Navigation_Tests.h @@ -18,6 +18,7 @@ namespace Pokemon{ void add_tests_LegPlanner(UnitTestDatabase& database); void add_tests_MapData(UnitTestDatabase& database); +void add_tests_RouteLog(UnitTestDatabase& database); void add_tests_StoryEvents(UnitTestDatabase& database); void add_tests_StorySegments(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.cpp b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.cpp new file mode 100644 index 0000000000..3fbf5bb16f --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.cpp @@ -0,0 +1,98 @@ +/* Route Log + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Pokemon_RouteLog.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +std::string to_string(const RouteTile& tile){ + return tile.map + " " + to_string(tile.pos); +} + +std::string describe(const std::vector& tiles){ + if (tiles.empty()){ + return "an unknown tile"; + } + std::string out; + for (const RouteTile& tile : tiles){ + out += (out.empty() ? "" : ", ") + to_string(tile); + } + return out; +} + +std::string to_string(const RouteLogLeg& leg){ + const std::string direction = direction_name(leg.direction); + switch (leg.action){ + case RouteLegAction::SURF: + return "surf " + direction; + case RouteLegAction::FACE: + return "face " + direction; + case RouteLegAction::WALK: + break; + } + const char* end = leg.end == RouteLegEnd::WALL ? " (to wall)" : leg.end == RouteLegEnd::WARP ? " (warp)" : " (open)"; + return direction + " " + std::to_string(leg.tiles) + end; +} + +const char* basis_name(EndBasis basis){ + switch (basis){ + case EndBasis::WALKED: + return "walked"; + case EndBasis::ASSUMED: + return "assumed"; + case EndBasis::UNKNOWN: + return "unknown"; + } + return "?"; +} + + +std::string route_plan_line(const std::string& label, const std::vector& from, const std::vector& legs){ + std::string text = label + ": plan from " + describe(from) + ":"; + for (const PlannedRouteLeg& leg : legs){ + text += "\n " + to_string(leg.leg) + " to " + describe(leg.after); + } + return text; +} + +std::string route_leg_line(size_t number, size_t count, const PlannedRouteLeg& leg){ + return std::format("Leg {}/{}: {} to {}", number, count, to_string(leg.leg), describe(leg.after)); +} + +std::string route_leg_end_line( + const std::string& label, const RouteLogLeg& leg, + const std::vector& at, EndBasis basis, const std::string& why +){ + const std::string how = why.empty() ? basis_name(basis) : std::string(basis_name(basis)) + ": " + why; + return std::format("{}: {}: ended at {} ({}).", label, to_string(leg), describe(at), how); +} + +std::string route_replan_line( + const std::string& label, const RouteLogLeg& leg, + const std::string& what, const std::vector& at +){ + return std::format("{}: {}: {}, at {}; planning again.", label, to_string(leg), what, describe(at)); +} + +std::string route_found_line(const std::string& label, const std::vector& at, size_t holds){ + return std::format("{}: found again at {}, after {} hold(s).", label, describe(at), holds); +} + +std::string route_trainer_line(const std::string& label, const std::string& who, const std::vector& at){ + return label + ": " + who + " stopped us about " + describe(at) + "."; +} + +std::string route_arrived_line(const std::string& label, const std::vector& at){ + return label + ": arrived at " + describe(at) + "."; +} + + + +} +} diff --git a/SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.h b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.h new file mode 100644 index 0000000000..5f5d4f09a4 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Navigation/Pokemon_RouteLog.h @@ -0,0 +1,120 @@ +/* Route Log + * + * From: https://github.com/PokemonAutomation/ + * + * The log lines a route walk writes, shared by the games' walkers so that + * every game's log reads the same and devtools/maps can draw any run's + * routes from it. These are BDSP AutoStory's route lines, lifted from + * PokemonBDSP_AutoStory_RouteWalk.cpp, with what the other games need: + * + * [Route] To the grass: plan from Route 202 (15, 25), Route 202 (16, 25): + * east 15 (to wall) to Route 202 (23, 25), Route 202 (30, 26) + * north 5 (to wall) to Route 202 (23, 24) + * [Route] Leg 1/2: east 15 (to wall) to Route 202 (23, 25), Route 202 (30, 26) + * [Route] To the grass: east 15 (to wall): ended at Route 202 (23, 25) (walked). + * [Route] To the grass: north 5 (to wall): stopped early after about 2 tiles, at Route 202 (23, 23); planning again. + * [Route] To the grass: found again at Route 202 (22, 24), after 2 holds. + * [Route] To the grass: arrived at Route 202 (23, 24). + * + * A position is a set of tiles: BDSP plans from every tile the player may + * be on, FRLG from the one it is on. A leg's end says how it is known: + * walked (as planned, or seen), assumed (taken to have ended as planned + * after something interrupted it), or unknown. + * + * The functions return a line without the "[Route] " tag, which + * ROUTE_LOG_TAG holds; the caller logs it. + * + */ + +#ifndef PokemonAutomation_Pokemon_RouteLog_H +#define PokemonAutomation_Pokemon_RouteLog_H + +#include +#include +#include +#include "Pokemon_Grid.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +inline constexpr const char* ROUTE_LOG_TAG = "[Route] "; + +// A tile of a named map. +struct RouteTile{ + std::string map; + TilePos pos; +}; +// "Route 202 (23, 25)". +std::string to_string(const RouteTile& tile); +// The tiles, comma-separated; "an unknown tile" for none. +std::string describe(const std::vector& tiles); + + +enum class RouteLegAction{ + WALK, + SURF, + FACE, +}; + +// How a leg ends: at a wall, on a warp, or where it is timed to. +enum class RouteLegEnd{ + OPEN, + WALL, + WARP, +}; + +struct RouteLogLeg{ + RouteLegAction action = RouteLegAction::WALK; + Direction direction = Direction::NORTH; + size_t tiles = 0; + RouteLegEnd end = RouteLegEnd::OPEN; +}; +// "east 15 (to wall)", "south 4 (open)", "north 1 (warp)", "surf east", +// "face west". +std::string to_string(const RouteLogLeg& leg); + +// A planned leg and the tiles it may end on. +struct PlannedRouteLeg{ + RouteLogLeg leg; + std::vector after; +}; + +// How the end of a walked leg is known. +enum class EndBasis{ + WALKED, // walked as planned, or seen + ASSUMED, // taken to have ended as planned after an interrupt + UNKNOWN, // stopped somewhere on the way +}; +const char* basis_name(EndBasis basis); + + +// "