From 10f05830b315765e44b58087d68c3a5015e31b6d Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 18:19:02 -0800 Subject: [PATCH 1/8] AutoStory: story types and engine, split out of #122 The story step vocabulary, the checkpoint builder and executor interface, the variant parser, the shared story-step JSON codec and authored checkpoint envelope, the story run options, and the StoryEvents/ StorySegments additions (changed_nothing, game_not_running), taken as they are at nav/story-steps fd603cac2. Screen state and interrupt tracking follow in their own layer. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .github/workflows/story-data.yml | 34 + .../ComputerPrograms_UnitTestRunner.cpp | 3 + .../Pokemon_AuthoredCheckpointJson.cpp | 67 ++ .../Pokemon_AuthoredCheckpointJson.h | 53 ++ .../Pokemon_AuthoredCheckpointJson_Tests.cpp | 139 +++ .../AutoStory/Pokemon_AutoStory_Tests.h | 8 + .../Pokemon/AutoStory/Pokemon_StoryEvents.cpp | 16 + .../Pokemon/AutoStory/Pokemon_StoryEvents.h | 15 + .../AutoStory/Pokemon_StorySegments.cpp | 70 +- .../Pokemon/AutoStory/Pokemon_StorySegments.h | 35 +- .../AutoStory/Pokemon_StoryStepJson.cpp | 355 ++++++++ .../Pokemon/AutoStory/Pokemon_StoryStepJson.h | 26 + .../AutoStory/Pokemon_StoryStepJson_Tests.cpp | 150 +++ .../Pokemon/AutoStory/Pokemon_StorySteps.cpp | 325 +++++++ .../Pokemon/AutoStory/Pokemon_StorySteps.h | 261 ++++++ .../AutoStory/Pokemon_StorySteps_Tests.cpp | 854 ++++++++++++++++++ .../AutoStory/Pokemon_StoryVariant.cpp | 72 ++ .../Pokemon/AutoStory/Pokemon_StoryVariant.h | 51 ++ .../Options/Pokemon_StoryRunOptions.cpp | 161 ++++ .../Pokemon/Options/Pokemon_StoryRunOptions.h | 119 +++ SerialPrograms/cmake/SourceFiles.cmake | 13 + devtools/story-data-tests/CMakeLists.txt | 43 + devtools/story-data-tests/README.md | 37 + 23 files changed, 2902 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/story-data.yml create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h create mode 100644 devtools/story-data-tests/CMakeLists.txt create mode 100644 devtools/story-data-tests/README.md diff --git a/.github/workflows/story-data.yml b/.github/workflows/story-data.yml new file mode 100644 index 0000000000..545299ac51 --- /dev/null +++ b/.github/workflows/story-data.yml @@ -0,0 +1,34 @@ +name: Story data codec +on: + pull_request: + workflow_dispatch: +jobs: + native-codec: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + - uses: actions/checkout@v7 + with: + repository: dayfine/PokemonSimulation + # Synthetic contract cases, not an approved map/resource pin. + ref: 866613fa03e5c4a5eb7e57c91b59fafb44fc7ee3 + path: publisher + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + - name: Check the Python reader against the same cases + working-directory: publisher + env: + PYTHONPATH: src + run: python -m unittest pokesim.story.test_checkpoint_wire + - run: cmake -S devtools/story-data-tests -B build-story-data -DCMAKE_BUILD_TYPE=Debug + - run: cmake --build build-story-data --config Debug -j4 + - run: ctest --test-dir build-story-data -C Debug --output-on-failure + - if: runner.os != 'Windows' + run: ./build-story-data/StoryDataTests publisher/data/story/wire-conformance.json + - if: runner.os == 'Windows' + run: ./build-story-data/Debug/StoryDataTests.exe publisher/data/story/wire-conformance.json diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index 71e86b9f48..df98fc4df2 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -80,6 +80,9 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ Pokemon::add_tests_RouteLog(ret); Pokemon::add_tests_StoryEvents(ret); Pokemon::add_tests_StorySegments(ret); + Pokemon::add_tests_StorySteps(ret); + Pokemon::add_tests_StoryStepJson(ret); + Pokemon::add_tests_AuthoredCheckpointJson(ret); NintendoSwitch::add_tests_CheckOnlineDetector(ret); NintendoSwitch::add_tests_FailedToConnectDetector(ret); NintendoSwitch::add_tests_UpdatePopupDetector(ret); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp new file mode 100644 index 0000000000..699ece6df1 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp @@ -0,0 +1,67 @@ +/* Shared Authored Checkpoint JSON + * From: https://github.com/PokemonAutomation/ + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Pokemon_StoryStepJson.h" +#include "Pokemon_AuthoredCheckpointJson.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +void require_story_json_fields( + const JsonObject& object, std::initializer_list fields, std::string_view source +){ + if (object.size() != fields.size()){ + throw ParseException(std::format("{}: missing or extra authored checkpoint fields.", source)); + } + for (std::string_view field : fields){ + object.get_value_throw(std::string(field), std::string(source)); + } +} + +AuthoredCheckpoint read_authored_checkpoint( + const JsonValue& value, std::string_view expected_game, std::string_view source +){ + const JsonObject& root = value.to_object_throw(std::string(source)); + require_story_json_fields(root, {"format", "game", "checkpoint", "execution"}, source); + if (expected_game.empty() || root.get_string_throw("format") != "pokesim-authored-checkpoint/1" + || root.get_string_throw("game") != expected_game + ){ + throw ParseException(std::format("{}: unsupported authored checkpoint format/game.", source)); + } + return { + .checkpoint = read_story_checkpoint(root.get_value_throw("checkpoint"), source), + .execution = root.get_object_throw("execution", std::string(source)).clone(), + }; +} + +JsonValue authored_checkpoint_json(const StoryCheckpoint& checkpoint, std::string_view game, JsonObject execution){ + if (game.empty()){ + throw ParseException("Authored checkpoint game must not be empty."); + } + JsonObject result; + result["format"] = "pokesim-authored-checkpoint/1"; + result["game"] = std::string(game); + result["checkpoint"] = story_checkpoint_json(checkpoint); + result["execution"] = std::move(execution); + return result; +} + +AuthoredCheckpoint load_authored_checkpoint( + std::span> files, std::string_view file, std::string_view expected_game +){ + for (const auto& [name, text] : files){ + if (name == file){ + return read_authored_checkpoint(parse_json(text), expected_game, file); + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Authored checkpoint is not embedded: " + std::string(file)); +} + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h new file mode 100644 index 0000000000..1b1be213dd --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h @@ -0,0 +1,53 @@ +/* Shared Authored Checkpoint JSON + * From: https://github.com/PokemonAutomation/ + */ + +#ifndef PokemonAutomation_AuthoredCheckpointJson_H +#define PokemonAutomation_AuthoredCheckpointJson_H + +#include +#include +#include +#include +#include +#include "Common/Cpp/Json/JsonObject.h" +#include "Pokemon_StorySteps.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// Common story data and an owned execution object for the game adapter to validate. +// Parsing this document does not validate game policies or bind/execute actions. +struct AuthoredCheckpoint{ + StoryCheckpoint checkpoint; + JsonObject execution; +}; + +// Require exactly these fields, including fields whose value is null/zero/false. +// Missing/unknown fields throw ParseException with the document source. +void require_story_json_fields( + const JsonObject& object, std::initializer_list fields, std::string_view source +); + +// Read the strict authored envelope and common checkpoint for expected_game. +// The game adapter must still validate execution fields and values before use. +AuthoredCheckpoint read_authored_checkpoint( + const JsonValue& value, std::string_view expected_game, std::string_view source +); + +// Encode the authored envelope; execution is supplied by the game adapter. +// This does not encode game bookkeeping or the legacy native-export envelope. +JsonValue authored_checkpoint_json(const StoryCheckpoint& checkpoint, std::string_view game, JsonObject execution); + +// Find and parse an embedded (filename without .json, text) resource for this game. +// Missing resources throw InternalProgramError; malformed documents throw ParseException. +AuthoredCheckpoint load_authored_checkpoint( + std::span> files, std::string_view file, std::string_view expected_game +); + + +} // namespace Pokemon +} // namespace PokemonAutomation + +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp new file mode 100644 index 0000000000..7aba0e12f4 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp @@ -0,0 +1,139 @@ +/* Shared Authored Checkpoint JSON Tests + * From: https://github.com/PokemonAutomation/ + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Pokemon_AuthoredCheckpointJson.h" +#include "Pokemon_StoryStepJson.h" +#ifndef PA_STORY_DATA_STANDALONE +#include "Pokemon_AutoStory_Tests.h" +#endif + +namespace PokemonAutomation{ +namespace Pokemon{ +namespace{ + +// Documents intentionally contain no game headers or native bookkeeping. +const std::string FRLG = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{"id":"00.05","title":"Parcel","events":[{"id":"first","kind":"route"},{"id":"second","kind":"scene"}]},"execution":{"menu":"before_pokedex"}})"; +const std::string BDSP = R"({"format":"pokesim-authored-checkpoint/1","game":"bdsp","checkpoint":{"id":"01.03","title":"Mine","events":[{"id":"roark","kind":"talk"}]},"execution":{"on_failure":"reload_and_retry","saves_in":"Oreburgh City"}})"; + +enum class Failure{ NONE, PARSE, MISSING_RESOURCE }; + +struct AuthoredInput{ + std::string name; + std::string json; + std::string game = "frlg"; + bool resource = false; + std::string file = "plan"; +}; + +struct AuthoredExpectation{ + std::string checkpoint = "00.05"; + std::string execution = R"({"menu":"before_pokedex"})"; + std::vector events = {"first", "second"}; + Failure failure = Failure::NONE; +}; + +struct AuthoredCase{ + AuthoredInput input; + AuthoredExpectation expectation; +}; + +const AuthoredCase CASES[] = { + {.input = {.name = "FRLG envelope", .json = FRLG}, .expectation = {}}, + {.input = {.name = "BDSP embedded resource", .json = BDSP, .game = "bdsp", .resource = true}, .expectation = { + .checkpoint = "01.03", .execution = R"({"on_failure":"reload_and_retry","saves_in":"Oreburgh City"})", .events = {"roark"}}}, + {.input = {.name = "FRLG embedded resource", .json = FRLG, .resource = true}, .expectation = {}}, + {.input = {.name = "wrong game", .json = BDSP}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "empty expected game", .json = FRLG, .game = ""}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "missing resource", .json = FRLG, .resource = true, .file = "absent"}, .expectation = {.failure = Failure::MISSING_RESOURCE}}, + {.input = {.name = "invalid embedded JSON", .json = "{", .resource = true}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "wrong format", .json = R"({"format":"future","game":"frlg","checkpoint":{},"execution":{}})"}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "missing execution", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{}})"}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "same count wrong field", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{},"extra":{}})"}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "extra envelope field", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{},"execution":{},"extra":0})"}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "execution must be object", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{"id":"x","title":"X","events":[]},"execution":false})"}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "invalid common action", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{"id":"x","title":"X","events":[{"id":"bad","kind":"magic"}]},"execution":{}})"}, .expectation = {.failure = Failure::PARSE}}, + {.input = {.name = "execution stays game owned", .json = R"({"format":"pokesim-authored-checkpoint/1","game":"future","checkpoint":{"id":"x","title":"X","events":[]},"execution":{"zero":0,"false":false,"null":null}})", .game = "future"}, .expectation = { + .checkpoint = "x", .execution = R"({"zero":0,"false":false,"null":null})", .events = {}}}, +}; + +std::string check_loaded(const AuthoredCheckpoint& actual, const AuthoredCase& test){ + if (test.expectation.failure != Failure::NONE){ + return "Expected rejection, read accepted authored checkpoint."; + } + if (actual.checkpoint.id != test.expectation.checkpoint){ + return "Expected checkpoint " + test.expectation.checkpoint + ", read " + actual.checkpoint.id; + } + if (!(JsonValue(actual.execution.clone()) == parse_json(test.expectation.execution))){ + return "Expected execution " + test.expectation.execution + ", read " + actual.execution.dump(); + } + std::vector events; + for (const StoryStep& step : actual.checkpoint.steps){ + events.push_back(step.name); + } + if (events != test.expectation.events){ + return "Expected authored event IDs in their original order."; + } + const JsonValue encoded = authored_checkpoint_json(actual.checkpoint, test.input.game, actual.execution.clone()); + const AuthoredCheckpoint reread = read_authored_checkpoint(encoded, test.input.game, test.input.name); + if (!(story_checkpoint_json(actual.checkpoint) == story_checkpoint_json(reread.checkpoint)) || actual.execution != reread.execution){ + return "Expected checkpoint and execution to survive the shared writer/reader round trip."; + } + return ""; +} + +std::string check_case(const AuthoredCase& test){ + try{ + const std::vector> files = {{"plan", test.input.json}}; + const AuthoredCheckpoint actual = test.input.resource + ? load_authored_checkpoint(files, test.input.file, test.input.game) + : read_authored_checkpoint(parse_json(test.input.json), test.input.game, test.input.name); + return check_loaded(actual, test); + }catch (const ParseException& error){ + return test.expectation.failure == Failure::PARSE ? "" : "Unexpected parse failure: " + error.message(); + }catch (const InternalProgramError& error){ + return test.expectation.failure == Failure::MISSING_RESOURCE ? "" : "Unexpected resource failure: " + error.message(); + } +} + +#ifndef PA_STORY_DATA_STANDALONE +class AuthoredJsonTest : public UnitTest{ +public: + explicit AuthoredJsonTest(AuthoredCase test) + : UnitTest("Pokemon::AuthoredCheckpointJson - " + test.input.name), m_case(std::move(test)){} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string error = check_case(m_case); + return error.empty() ? UnitTestResult(true) : UnitTestResult(error); + } +private: + AuthoredCase m_case; +}; +#endif + +} // namespace + +#ifndef PA_STORY_DATA_STANDALONE +void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database){ + for (const AuthoredCase& test : CASES){ + database.add(test); + } +} +#endif + +} // namespace Pokemon +} // namespace PokemonAutomation + +#ifdef PA_STORY_DATA_STANDALONE +int main(){ + int failed = 0; + for (const auto& test : PokemonAutomation::Pokemon::CASES){ + const std::string error = PokemonAutomation::Pokemon::check_case(test); + std::cout << test.input.name << ": " << (error.empty() ? "PASS" : error) << '\n'; + failed += !error.empty(); + } + return failed != 0; +} +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h index 0d139a39f4..5d0bbe1172 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -19,9 +19,17 @@ namespace Pokemon{ void add_tests_StoryEvents(UnitTestDatabase& database); // Registers unit tests for story segment running, verification, and branching. void add_tests_StorySegments(UnitTestDatabase& database); +// Registers unit tests for high-level story step dispatching and table conversion. +void add_tests_StorySteps(UnitTestDatabase& database); +// Lossless shared story-data wire contract. +void add_tests_StoryStepJson(UnitTestDatabase& database); + +// Shared authored envelope, game identity and embedded-resource loading. +void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database); + } // namespace Pokemon } // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp index d355460da3..ad22d7a6a7 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp @@ -68,6 +68,11 @@ Outcome outcome_ok(Pose pose){ outcome.pose = std::move(pose); return outcome; } +Outcome outcome_unchanged(){ + Outcome outcome; + outcome.changed_nothing = true; + return outcome; +} Outcome outcome_failed(std::string failure, std::string detail){ Outcome outcome; outcome.failure = std::move(failure); @@ -78,6 +83,10 @@ std::string to_string(const Outcome& outcome){ return outcome.detail.empty() ? outcome.failure : outcome.failure + ": " + outcome.detail; } +Outcome outcome_game_not_running(){ + return outcome_failed(GAME_NOT_RUNNING, "the game is not running: the Switch HOME menu is showing"); +} + Condition condition(std::string name, Check check){ return Condition{std::move(name), std::move(check)}; } @@ -156,6 +165,8 @@ Outcome attempt_event(Event& event, RunState& state){ return check_expects(event, state); } +} // namespace + void apply_interrupt_pose(Pose& pose, InterruptPose effect){ switch (effect){ case InterruptPose::UNCHANGED: @@ -172,6 +183,8 @@ void apply_interrupt_pose(Pose& pose, InterruptPose effect){ } } +namespace{ + // One event being run, with what it has used up. struct EventRun{ @@ -257,6 +270,9 @@ Recovery* next_recovery(EventRun& run, const std::string& failure){ // is answered in turn, so escalation goes on from there. std::optional recover(EventRun& run, Outcome failure){ while (true){ + if (failure.failure == GAME_NOT_RUNNING){ + return failure; + } Recovery* recovery = next_recovery(run, failure.failure); if (recovery == nullptr){ return failure; diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h index 0a2673ff38..388ce48b03 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h @@ -97,6 +97,10 @@ struct Outcome{ std::string failure; std::string detail; std::optional pose; + // A succeeded action that left the game as it found it: a read, a + // check, a TM the lead already knows. A reload of the same save undoes + // nothing it did. + bool changed_nothing = false; bool succeeded() const; }; @@ -104,11 +108,20 @@ struct Outcome{ Outcome outcome_ok(); // Constructs a successful event outcome updating the run pose. Outcome outcome_ok(Pose pose); +// outcome_ok() with `changed_nothing`. +Outcome outcome_unchanged(); // Constructs a failed event outcome with reason and optional diagnostic detail. Outcome outcome_failed(std::string failure, std::string detail = ""); // "probe_exhausted: 4 rows": the failure and its detail, if any. std::string to_string(const Outcome& outcome); +// The failure of a step that found the Switch's HOME menu instead of the +// game. Nothing in the story can answer it, as every press goes to the +// console's menus: no recovery runs and no reload, the run stops. +constexpr const char* GAME_NOT_RUNNING = "game_not_running"; +// GAME_NOT_RUNNING, saying so for the run log. +Outcome outcome_game_not_running(); + // Callable action executed for a story event. using Act = MoveOnlyFunction; @@ -189,6 +202,8 @@ struct Interrupt{ InterruptPose pose = InterruptPose::UNCHANGED; ResumePolicy resume = ResumePolicy::RETRY_EVENT; }; +// Writes what an interrupt did (`effect`) into `pose`. +void apply_interrupt_pose(Pose& pose, InterruptPose effect); // Story milestone containing pre/post conditions and a series of events. diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp index 0d112b0170..5653725467 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp @@ -102,22 +102,69 @@ 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); EventLogger log = [&hooks](const std::string& line){ say(hooks, line); }; CheckpointReport result = run_checkpoint(state, checkpoint, log); - if (result.completed || attempt >= policy.attempts_per_checkpoint){ + if (result.completed){ + run.passed = true; + say(hooks, std::format( + "[Checkpoint] {}: passed on attempt {} ({}){}", + spec.id, attempt, resets_text(attempt), + run.recoveries == 0 ? "" : std::format(", {} in place", run.recoveries) + )); + return result; + } + const bool not_running = result.failure.failure == GAME_NOT_RUNNING; + if (not_running || policy.stop_at_first_failure){ + say(hooks, std::format("[Segment] {}: {} failed; {}", segment.id, spec.id, not_running + ? "the game is not running, so no reload" + : "stopping at the first failure, the game stays where it failed" + )); + report.stopped_at_first_failure = !not_running; + return result; + } + if (attempt >= policy.attempts_per_checkpoint){ return result; } say(hooks, std::format( @@ -148,6 +195,19 @@ SegmentReport stop_segment(SegmentReport& report, SegmentHooks& hooks, const std } // namespace +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 @@ -161,7 +221,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; @@ -169,6 +230,7 @@ SegmentReport run_segment( } } report.completed = true; + say(hooks, std::format("[Segment] {}: {} of {} checkpoints one-shot", segment.id, report.one_shot_passes(), report.checkpoints_run)); say(hooks, std::format("[Segment] {}: done, {} checkpoints", segment.id, report.checkpoints_run)); return report; } diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h index a63b8b4be9..8e2426aee8 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h @@ -47,10 +47,14 @@ 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; // Optional: each [Segment], [Checkpoint] and [Event] line again, // without its tag ("00.03/2 below_oaks_trigger: start"), for the video // overlay, so whoever watches the stream sees the step under way; and, @@ -60,8 +64,16 @@ struct SegmentHooks{ // Retry and timeout configuration for story segment execution. 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 @@ -72,9 +84,22 @@ struct SegmentRange{ }; +// Summary of checkpoint attempts, recoveries, and pass status. +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; +}; + // Execution results and failure diagnostics for a story segment run. 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; @@ -83,13 +108,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/AutoStory/Pokemon_StoryStepJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp new file mode 100644 index 0000000000..3ee2f51f6d --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp @@ -0,0 +1,355 @@ +/* Story Step JSON + * From: https://github.com/PokemonAutomation/ + */ +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Pokemon_StoryStepJson.h" +namespace PokemonAutomation{ +namespace Pokemon{ +namespace{ +constexpr std::array, 15> KINDS{{ + {StoryStepKind::NEW_GAME, "new_game"}, + {StoryStepKind::OPTIONS, "options"}, + {StoryStepKind::ROUTE, "route"}, + {StoryStepKind::LEGS, "legs"}, + {StoryStepKind::STEP, "step"}, + {StoryStepKind::TALK, "talk"}, + {StoryStepKind::ANSWER, "answer"}, + {StoryStepKind::MASH, "scene"}, + {StoryStepKind::BATTLE, "battle"}, + {StoryStepKind::TRAIN, "train"}, + {StoryStepKind::HEAL, "heal"}, + {StoryStepKind::SHOP, "shop"}, + {StoryStepKind::SAVE, "save"}, + {StoryStepKind::READY, "ready"}, + {StoryStepKind::GAME, "game_action"}, +}}; +constexpr std::array, 5> CERTAINTIES{{ + {PoseCertainty::TILE, "tile"}, + {PoseCertainty::COLUMN, "column"}, + {PoseCertainty::ROW, "row"}, + {PoseCertainty::SET, "set"}, + {PoseCertainty::UNKNOWN, "unknown"}, +}}; +constexpr std::array, 4> DIRECTIONS{{ + {Direction::NORTH, "north"}, + {Direction::EAST, "east"}, + {Direction::SOUTH, "south"}, + {Direction::WEST, "west"}, +}}; + +const JsonObject& fields(const JsonValue& value, std::initializer_list keys, std::string_view source){ + const JsonObject& object = value.to_object_throw(std::string(source)); + for (const auto& [key, unused] : object){ + bool known = false; + for (std::string_view allowed : keys){ + known |= key == allowed; + } + if (!known){ + throw ParseException(std::format("{}: unsupported field {}", source, key)); + } + } + return object; +} + +template +Enum read_enum(const std::array, N>& names, const std::string& name, std::string_view source){ + for (const auto& [value, label] : names){ + if (label == name){ + return value; + } + } + throw ParseException(std::format("{}: unsupported value {}", source, name)); +} + +template +std::string enum_name(const std::array, N>& names, Enum value){ + for (const auto& [candidate, label] : names){ + if (candidate == value){ + return std::string(label); + } + } + throw ParseException("Unsupported story enum value."); +} + +int integer(const JsonValue& value, std::string_view source){ + const int64_t number = value.to_integer_throw(std::string(source)); + if (number < std::numeric_limits::min() || number > std::numeric_limits::max()){ + throw ParseException(std::format("{}: coordinate exceeds native int", source)); + } + return (int)number; +} + +size_t count(const JsonValue& value, std::string_view source){ + const int64_t number = value.to_integer_throw(std::string(source)); + if (number < 0 || (uint64_t)number > std::numeric_limits::max()){ + throw ParseException(std::format("{}: count is out of range", source)); + } + return (size_t)number; +} + +JsonValue count_json(size_t value){ + if ((uint64_t)value > (uint64_t)std::numeric_limits::max()){ + throw ParseException("Story count exceeds the wire integer range."); + } + return (uint64_t)value; +} + +JsonArray strings_json(const std::vector& strings){ + JsonArray result; + for (const std::string& value : strings){ + result.push_back(value); + } + return result; +} + +std::vector read_strings(const JsonValue& value, std::string_view source){ + std::vector result; + for (const JsonValue& item : value.to_array_throw(std::string(source))){ + result.push_back(item.to_string_throw(std::string(source))); + } + return result; +} + +JsonArray legs_json(const std::vector& legs){ + JsonArray result; + for (const StoryLeg& leg : legs){ + JsonObject item; + item["direction"] = enum_name(DIRECTIONS, leg.direction); + item["tiles"] = leg.tiles; + result.push_back(std::move(item)); + } + return result; +} + +std::vector read_legs(const JsonValue& value, std::string_view source){ + std::vector result; + for (const JsonValue& item : value.to_array_throw(std::string(source))){ + const JsonObject& row = fields(item, {"direction", "tiles"}, source); + result.push_back({.direction = read_enum(DIRECTIONS, row.get_string_throw("direction"), source), + .tiles = integer(row.get_value_throw("tiles"), source)}); + } + return result; +} + +JsonArray items_json(const std::vector& items){ + JsonArray result; + for (const StoryItem& item : items){ + JsonObject row; + row["item"] = item.item; + row["quantity"] = count_json(item.quantity); + result.push_back(std::move(row)); + } + return result; +} + +std::vector read_items(const JsonValue& value, std::string_view source){ + std::vector result; + for (const JsonValue& item : value.to_array_throw(std::string(source))){ + const JsonObject& row = fields(item, {"item", "quantity"}, source); + result.push_back({.item = row.get_string_throw("item"), .quantity = count(row.get_value_throw("quantity"), source)}); + } + return result; +} +StoryStep read_step_options(StoryStep result, const JsonObject& root, std::string_view source){ + if (const JsonValue* field = root.get_value("fight_wild")){ + result.fight_wild = field->to_boolean_throw(); + } + if (const JsonValue* field = root.get_value("yes")){ + result.yes = field->to_boolean_throw(); + } + if (const JsonValue* field = root.get_value("may_lose")){ + result.may_lose = field->to_boolean_throw(); + } + if (const JsonValue* field = root.get_value("battles")){ + result.battles = count(*field, source); + } + return result; +} + +} // namespace + +JsonValue story_pose_json(const Pose& pose){ + JsonObject result; + result["map"] = pose.map; + result["certainty"] = enum_name(CERTAINTIES, pose.certainty); + JsonObject where; + where["kind"] = "unknown"; + if (pose.where){ + const auto* tiles = dynamic_cast(pose.where.get()); + if (!tiles){ + throw ParseException("Unsupported story Position subtype."); + } + where["kind"] = "tiles"; + JsonArray positions; + for (const TilePos& tile : tiles->tiles){ + JsonArray pair; + pair.push_back(tile.x); pair.push_back(tile.y); + positions.push_back(std::move(pair)); + } + where["tiles"] = std::move(positions); + if (tiles->facing){ + where["facing"] = enum_name(DIRECTIONS, *tiles->facing); + } + } + result["where"] = std::move(where); + return result; +} + +Pose read_story_pose(const JsonValue& value, std::string_view source){ + const JsonObject& root = fields(value, {"map", "certainty", "where"}, source); + Pose result; + result.map = root.get_string_throw("map"); + result.certainty = read_enum(CERTAINTIES, root.get_string_throw("certainty"), source); + const JsonObject& where = fields(root.get_value_throw("where"), {"kind", "tiles", "facing"}, source); + const std::string& kind = where.get_string_throw("kind"); + if (kind == "unknown"){ + if (where.size() != 1){ + throw ParseException("Unknown story position has extra fields."); + } + return result; + } + if (kind != "tiles"){ + throw ParseException("Unsupported story position kind."); + } + std::vector tiles; + for (const JsonValue& item : where.get_array_throw("tiles")){ + const JsonArray& pair = item.to_array_throw(std::string(source)); + if (pair.size() != 2){ + throw ParseException("Story tile needs two coordinates."); + } + tiles.push_back({integer(pair[0], source), integer(pair[1], source)}); + } + std::optional facing; + if (const JsonValue* field = where.get_value("facing")){ + facing = read_enum(DIRECTIONS, field->to_string_throw(), source); + } + result.where = std::make_shared(std::move(tiles), facing); + return result; +} + +JsonValue story_step_json(const StoryStep& step){ + JsonObject result; + result["id"] = step.name; + result["kind"] = enum_name(KINDS, step.kind); + if (!step.why.empty()){ + result["why"] = step.why; + } + if (step.target){ + result["target"] = story_pose_json(*step.target); + } + if (step.direction != Direction::NORTH){ + result["direction"] = enum_name(DIRECTIONS, step.direction); + } + if (!step.legs.empty()){ + result["legs"] = legs_json(step.legs); + } + if (step.fight_wild){ + result["fight_wild"] = true; + } + if (!step.yes){ + result["yes"] = false; + } + if (step.may_lose){ + result["may_lose"] = true; + } + if (step.battles){ + result["battles"] = count_json(step.battles); + } + if (!step.items.empty()){ + result["items"] = items_json(step.items); + } + if (!step.variant.empty()){ + result["variant"] = step.variant; + } + if (!step.skip_if.empty()){ + result["skip_if"] = strings_json(step.skip_if); + } + if (step.pose_after){ + result["declared_after"] = story_pose_json(*step.pose_after); + } + return result; +} + +StoryStep read_story_step(const JsonValue& value, std::string_view source){ + const JsonObject& root = fields(value, {"id", "kind", "why", "target", "direction", "legs", "fight_wild", "yes", + "may_lose", "battles", "items", "variant", "skip_if", "declared_after"}, source); + StoryStep result; + result.name = root.get_string_throw("id"); + if (result.name.empty()){ + throw ParseException("Empty story step id."); + } + result.kind = read_enum(KINDS, root.get_string_throw("kind"), source); + if (const JsonValue* field = root.get_value("why")){ + result.why = field->to_string_throw(); + } + if (const JsonValue* field = root.get_value("target")){ + result.target = read_story_pose(*field, source); + } + if (const JsonValue* field = root.get_value("direction")){ + result.direction = read_enum(DIRECTIONS, field->to_string_throw(), source); + } + if (const JsonValue* field = root.get_value("legs")){ + result.legs = read_legs(*field, source); + } + result = read_step_options(std::move(result), root, source); + if (const JsonValue* field = root.get_value("items")){ + result.items = read_items(*field, source); + } + if (const JsonValue* field = root.get_value("variant")){ + result.variant = field->to_string_throw(); + } + if (const JsonValue* field = root.get_value("skip_if")){ + result.skip_if = read_strings(*field, source); + } + if (const JsonValue* field = root.get_value("declared_after")){ + result.pose_after = read_story_pose(*field, source); + } + return result; +} + +JsonValue story_checkpoint_json(const StoryCheckpoint& checkpoint){ + JsonObject result; + result["id"] = checkpoint.id; + result["title"] = checkpoint.title; + if (checkpoint.start){ + result["start"] = story_pose_json(*checkpoint.start); + } + JsonArray events; + for (const StoryStep& step : checkpoint.steps){ + events.push_back(story_step_json(step)); + } + result["events"] = std::move(events); + return result; +} + +StoryCheckpoint read_story_checkpoint(const JsonValue& value, std::string_view source){ + const JsonObject& root = fields(value, {"id", "title", "start", "events"}, source); + StoryCheckpoint result; + result.id = root.get_string_throw("id"); + if (result.id.empty()){ + throw ParseException("Empty story checkpoint id."); + } + result.title = root.get_string_throw("title"); + if (const JsonValue* start = root.get_value("start")){ + result.start = read_story_pose(*start, source); + } + std::set names; + for (const JsonValue& event : root.get_array_throw("events")){ + StoryStep step = read_story_step(event, source); + if (step.name.empty() || !names.insert(step.name).second){ + throw ParseException("Empty or duplicate story event id."); + } + result.steps.push_back(std::move(step)); + } + return result; +} +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h new file mode 100644 index 0000000000..d58fd9828c --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h @@ -0,0 +1,26 @@ +/* Story Step JSON + * From: https://github.com/PokemonAutomation/ + */ +#ifndef PokemonAutomation_StoryStepJson_H +#define PokemonAutomation_StoryStepJson_H +#include +#include "Common/Cpp/Json/JsonValue.h" +#include "Pokemon_StorySteps.h" +namespace PokemonAutomation{ +namespace Pokemon{ +// Lossless tile-game pose data, including uncertainty and absent position/facing. +// An unsupported Position subtype fails instead of being converted to unknown. +JsonValue story_pose_json(const Pose& pose); +// Read a tile-game pose; malformed fields and unsupported position tags fail. +Pose read_story_pose(const JsonValue& value, std::string_view source); +// Encode every shared step field. This does not bind or execute an action. +JsonValue story_step_json(const StoryStep& step); +// Read the shared step record; unknown fields/kinds and invalid values fail. +StoryStep read_story_step(const JsonValue& value, std::string_view source); +// Encode the common checkpoint; game adapters supply the shared policy/state envelope. +JsonValue story_checkpoint_json(const StoryCheckpoint& checkpoint); +// Read only the common checkpoint record, retaining ordered steps and start belief. +StoryCheckpoint read_story_checkpoint(const JsonValue& value, std::string_view source); +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp new file mode 100644 index 0000000000..021b18f508 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp @@ -0,0 +1,150 @@ +/* Story Step JSON Tests + * From: https://github.com/PokemonAutomation/ + */ +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Pokemon_StoryStepJson.h" +#ifndef PA_STORY_DATA_STANDALONE +#include "Pokemon_AutoStory_Tests.h" +#endif +namespace PokemonAutomation{ +namespace Pokemon{ +namespace{ +enum class Record{ POSE, STEP, CHECKPOINT }; +struct WireInput{ + std::string_view name; + Record record = Record::STEP; + std::string_view json; +}; +struct WireExpectation{ + std::string_view canonical = {}; + bool rejected = false; +}; +struct WireCase{ + WireInput input; + WireExpectation expectation; +}; +const WireCase CASES[] = { + {.input = {.name = "default fields", .json = R"({"id":"talk","kind":"talk"})"}, .expectation = {.canonical = R"({"id":"talk","kind":"talk"})"}}, + {.input = {.name = "all shared fields", .json = R"({"id":"all","kind":"game_action","why":"source reason","direction":"west","legs":[{"direction":"south","tiles":0}],"fight_wild":true,"yes":false,"may_lose":true,"battles":3,"items":[{"item":"potion","quantity":0}],"variant":"profile","skip_if":["flag"],"target":{"map":"A","certainty":"unknown","where":{"kind":"unknown"}},"declared_after":{"map":"B","certainty":"set","where":{"kind":"tiles","tiles":[[0,0],[1,0]],"facing":"east"}}})"}, .expectation = {.canonical = R"({"id":"all","kind":"game_action","why":"source reason","direction":"west","legs":[{"direction":"south","tiles":0}],"fight_wild":true,"yes":false,"may_lose":true,"battles":3,"items":[{"item":"potion","quantity":0}],"variant":"profile","skip_if":["flag"],"target":{"map":"A","certainty":"unknown","where":{"kind":"unknown"}},"declared_after":{"map":"B","certainty":"set","where":{"kind":"tiles","tiles":[[0,0],[1,0]],"facing":"east"}}})"}}, + {.input = {.name = "unknown facing stays absent", .record = Record::POSE, .json = R"({"map":"M","certainty":"tile","where":{"kind":"tiles","tiles":[[0,0]]}})"}, .expectation = {.canonical = R"({"map":"M","certainty":"tile","where":{"kind":"tiles","tiles":[[0,0]]}})"}}, + {.input = {.name = "empty position differs from no position", .record = Record::POSE, .json = R"({"map":"M","certainty":"unknown","where":{"kind":"tiles","tiles":[]}})"}, .expectation = {.canonical = R"({"map":"M","certainty":"unknown","where":{"kind":"tiles","tiles":[]}})"}}, + {.input = {.name = "ordered checkpoint", .record = Record::CHECKPOINT, .json = R"({"id":"00.05","title":"Parcel","events":[{"id":"first","kind":"route"},{"id":"second","kind":"scene"}]})"}, .expectation = {.canonical = R"({"id":"00.05","title":"Parcel","events":[{"id":"first","kind":"route"},{"id":"second","kind":"scene"}]})"}}, + {.input = {.name = "unknown field", .json = R"({"id":"a","kind":"talk","mystery":0})"}, .expectation = {.rejected = true}}, + {.input = {.name = "unknown action", .json = R"({"id":"a","kind":"magic"})"}, .expectation = {.rejected = true}}, + {.input = {.name = "negative count", .json = R"({"id":"a","kind":"train","battles":-1})"}, .expectation = {.rejected = true}}, + {.input = {.name = "boolean count", .json = R"({"id":"a","kind":"train","battles":false})"}, .expectation = {.rejected = true}}, + {.input = {.name = "wrong bool", .json = R"({"id":"a","kind":"answer","yes":0})"}, .expectation = {.rejected = true}}, + {.input = {.name = "unknown position", .record = Record::POSE, .json = R"({"map":"M","certainty":"unknown","where":{"kind":"gps"}})"}, .expectation = {.rejected = true}}, + {.input = {.name = "bad coordinate", .record = Record::POSE, .json = R"({"map":"M","certainty":"tile","where":{"kind":"tiles","tiles":[[0,true]]}})"}, .expectation = {.rejected = true}}, + {.input = {.name = "duplicate events", .record = Record::CHECKPOINT, .json = R"({"id":"x","title":"X","events":[{"id":"a","kind":"talk"},{"id":"a","kind":"talk"}]})"}, .expectation = {.rejected = true}}, +}; + +JsonValue round_trip(const WireInput& input){ + const JsonValue value = parse_json(std::string(input.json)); + switch (input.record){ + case Record::POSE: return story_pose_json(read_story_pose(value, input.name)); + case Record::STEP: return story_step_json(read_story_step(value, input.name)); + case Record::CHECKPOINT: return story_checkpoint_json(read_story_checkpoint(value, input.name)); + } + throw ParseException("Unknown test record."); +} + +std::string check_case(const WireCase& test){ + try{ + const JsonValue actual = round_trip(test.input); + if (test.expectation.rejected){ + return "Expected rejection, read accepted record."; + } + const JsonValue expected = parse_json(std::string(test.expectation.canonical)); + if (!(actual == expected)){ + return "Expected " + expected.dump() + ", read " + actual.dump(); + } + }catch (const ParseException& error){ + return test.expectation.rejected ? "" : "Unexpected parse failure: " + error.message(); + } + return ""; +} +#ifdef PA_STORY_DATA_STANDALONE +Record record_kind(const std::string& name){ + if (name == "pose"){ + return Record::POSE; + } + if (name == "step"){ + return Record::STEP; + } + if (name == "checkpoint"){ + return Record::CHECKPOINT; + } + throw ParseException("Unsupported shared case record kind."); +} + +int check_shared_file(const std::string& filename){ + const JsonValue document = load_json_file(filename); + const JsonObject& root = document.to_object_throw(filename); + if (root.get_string_throw("format") != "pokesim-story-wire-cases/1"){ + throw ParseException("Unsupported story wire case format."); + } + int failed = 0; + for (const JsonValue& value : root.get_array_throw("cases")){ + const JsonObject& row = value.to_object_throw(filename); + const JsonObject& input = row.get_object_throw("input"); + const JsonObject& expected = row.get_object_throw("expectation"); + const std::string source = input.get_value_throw("value").dump(); + const JsonValue* normalized = expected.get_value("canonical"); + const std::string canonical = normalized ? normalized->dump() : ""; + const JsonValue* rejected = expected.get_value("rejected"); + const WireCase test{ + .input = {.name = input.get_string_throw("name"), .record = record_kind(input.get_string_throw("record")), .json = source}, + .expectation = {.canonical = canonical, .rejected = rejected && rejected->to_boolean_throw()}, + }; + const std::string error = check_case(test); + if (!error.empty()){ + std::cerr << test.input.name << ": " << error << '\n'; + failed++; + } + } + std::cout << "Checked " << root.get_array_throw("cases").size() << " shared story records.\n"; + return failed != 0; +} +#endif + +#ifndef PA_STORY_DATA_STANDALONE +class StoryJsonTest : public UnitTest{ +public: + explicit StoryJsonTest(WireCase test) + : UnitTest("Pokemon::StoryStepJson - " + std::string(test.input.name)), m_case(test){} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string error = check_case(m_case); + return error.empty() ? UnitTestResult(true) : UnitTestResult(error); + } +private: + WireCase m_case; +}; +#endif +} // namespace +#ifndef PA_STORY_DATA_STANDALONE +void add_tests_StoryStepJson(UnitTestDatabase& database){ + for (const WireCase& test : CASES){ + database.add(test); + } +} +#endif +} // namespace Pokemon +} // namespace PokemonAutomation +#ifdef PA_STORY_DATA_STANDALONE +int main(int argc, char** argv){ + if (argc == 2){ + return PokemonAutomation::Pokemon::check_shared_file(argv[1]); + } + int failed = 0; + for (const auto& test : PokemonAutomation::Pokemon::CASES){ + const std::string error = PokemonAutomation::Pokemon::check_case(test); + std::cout << test.input.name << ": " << (error.empty() ? "PASS" : error) << '\n'; + failed += !error.empty(); + } + return failed != 0; +} +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp new file mode 100644 index 0000000000..3004ba6013 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp @@ -0,0 +1,325 @@ +/* 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 "?"; +} + + +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)); +} + +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); +} + +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; +} + +} // namespace + +Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor){ + executor.begin_attempt(data); + 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{ + +// `event`, for a once_per_save step known in `prepared` as `key`: skipped +// while `prepared` has it, and added to it when it succeeds changing +// nothing. +void run_once_per_save(Event& event, std::string key, PreparedSteps& prepared){ + event.skip_if.push_back(condition("done_from_this_save", [&prepared, key](RunState&){ + return prepared.contains(key); + })); + event.act = [act = std::move(event.act), &prepared, key](RunState& state) mutable{ + Outcome outcome = act(state); + if (outcome.succeeded() && outcome.changed_nothing){ + prepared.insert(key); + } + return outcome; + }; +} + +} // namespace + +Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor, PreparedSteps& prepared){ + Checkpoint checkpoint = build_story_checkpoint(data, executor); + // After "from_save", the events are the steps in order. + const size_t first = data.start ? 1 : 0; + for (size_t c = 0; c < data.steps.size(); c++){ + if (data.steps[c].once_per_save){ + run_once_per_save(checkpoint.events[first + c], data.id + "/" + data.steps[c].name, prepared); + } + } + 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 ""; + } +} + +} // namespace + +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)); + } + // skip_if's conditions must all hold; once per save is one more. + if (step.once_per_save && !step.skip_if.empty()){ + problems.emplace_back(std::format("{} {}: once per save with skip_if", where, step.name)); + } + } + return problems; +} + + +void record_story_stats(AutoStoryStats& stats, const SegmentReport& report){ + stats.m_segment++; + for (const CheckpointRun& run : report.runs){ + if (run.passed){ + stats.m_checkpoint++; + } + } + stats.m_reset += report.reloads; +} + + +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; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h new file mode 100644 index 0000000000..27aaabad8a --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h @@ -0,0 +1,261 @@ +/* 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. + * + * 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 + * Outcome, which the engine writes into the run. + * + */ + +#ifndef PokemonAutomation_Pokemon_StorySteps_H +#define PokemonAutomation_Pokemon_StorySteps_H + +#include +#include +#include +#include +#include +#include "CommonFramework/Language.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "Pokemon_AutoStoryStats.h" +#include "Pokemon_StoryEvents.h" +#include "Pokemon_StorySegments.h" +#include "Pokemon_TilePose.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// Action category classification for individual story steps. +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` +}; +// Returns the readable string representation of a story step kind. +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, +}; +// Returns the readable string representation of a story check level. +const char* story_checks_name(StoryChecks checks); + + +// 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; +}; + +// Specification of an atomic story task with actions and postconditions. +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()), run flags + // unless the game reads them off the screen. + std::vector skip_if; + // Preparation for what follows, done once per save: a TEACH, a check, + // a read. Once it has succeeded and changed nothing + // (Outcome::changed_nothing), the attempts after a reload of the same + // save skip it, so a retry goes from the reload straight to the + // battle. One that changed the game (a TM taught) runs again, as the + // reload undid it. Only with build_story_checkpoint()'s PreparedSteps. + bool once_per_save = false; + // 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; +}; + +// Milestone row in the story table grouping related steps and starting pose. +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; + + // 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); + + // 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; + } + // 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 + // Outcome. + virtual Outcome run(RunState& state, const StoryStep& step); + + // 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); + +protected: + // One per step kind, called by dispatch_story_step(); each fails as + // unsupported unless the game overrides it. + friend Outcome dispatch_story_step(StoryStepExecutor& executor, RunState& state, const StoryStep& step); + 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); + +private: + StoryChecks m_checks = StoryChecks::THOROUGH; + Language m_language = Language::English; +}; + +// 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); + +// The once_per_save steps done from the save the game is on that changed +// nothing, as "/". The game keeps it across reloads and +// empties it when it saves or loads another save. +using PreparedSteps = std::set; + +// As above, and each once_per_save step is skipped while `prepared` has +// it, and added to it when it succeeds changing nothing. `prepared` must +// outlive the checkpoint. +Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor, PreparedSteps& prepared); + +// 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); + + +// Adds a segment's run to the program's stats: the segment, each checkpoint +// that passed, each reload. +void record_story_stats(AutoStoryStats& stats, const SegmentReport& report); + + +// 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); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp new file mode 100644 index 0000000000..e61a1eeb95 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp @@ -0,0 +1,854 @@ +/* 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 +#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" +#include "Pokemon_TilePose.h" +#include "Pokemon_AutoStory_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 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; + // The checkpoints begun, one per attempt. + std::vector attempts; + // Game steps by name that change nothing in the game. + std::set unchanged; + + 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)); + 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"); + } + 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& state) override{ + return conditions.contains(condition) || StoryStepExecutor::holds(condition, state); + } + +protected: + 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(); + } + // 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 unchanged.contains(step.name) ? outcome_unchanged() : 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); + } + + // 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; +} + +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; +} + +// Runs `tables` as one segment whose saves and reloads go into the +// executor's trace and whose log lines go into `lines`. Whatever blocked a +// step has gone by the reload. +SegmentReport run_tables(const std::vector& tables, FakeExecutor& executor, std::vector& lines){ + 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); }, + }); + } + SegmentHooks hooks; + hooks.save = [&executor](RunState&){ + executor.trace.push_back("save"); + return outcome_ok(); + }; + 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); }; + RunState state; + return run_segment(state, segment, SegmentRange{}, SegmentPolicy{}, hooks); +} + +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; + executor.failing = {"give_parcel"}; + std::vector lines; + const SegmentReport report = run_tables(tables, executor, lines); + 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); + } + // 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 + && !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; +} + +// A boss's checkpoint: a TEACH of a move the lead knows (changes +// nothing), a TEACH that teaches, both once per save, then the battle, +// lost on the first attempt. The segment's saves empty `prepared`. +struct BossLab{ + StoryCheckpoint table{ + .id = "00.15b", + .title = "the rival", + .start = tile_pose("Cerulean", {{23, 7}}, Direction::NORTH), + .steps = {once("teach_known"), once("teach_new"), make(StoryStepKind::BATTLE, "rival")}, + }; + FakeExecutor executor; + PreparedSteps prepared; + std::vector lines; + + static StoryStep once(std::string name){ + StoryStep step = make(StoryStepKind::GAME, name); + step.variant = name; + step.once_per_save = true; + return step; + } + BossLab(){ + executor.unchanged = {"teach_known"}; + } + Segment segment(){ + Segment segment; + segment.id = "00"; + segment.checkpoints.push_back(CheckpointSpec{ + .id = table.id, .title = table.title, .saves = true, + .build = [this]{ return build_story_checkpoint(table, executor, prepared); }, + }); + return segment; + } + SegmentHooks hooks(){ + SegmentHooks hooks; + hooks.save = [this](RunState&){ + executor.trace.push_back("save"); + prepared.clear(); + return outcome_ok(); + }; + hooks.reload = [this](RunState&){ + executor.trace.push_back("reload"); + return outcome_ok(); + }; + hooks.log = [this](const std::string& line){ lines.push_back(line); }; + return hooks; + } + size_t count(const std::string& item) const{ + return (size_t)std::count(executor.trace.begin(), executor.trace.end(), item); + } +}; + +// A retry after a reload of the same save skips the preparation that +// changed nothing and goes to the battle; the TEACH that taught runs +// again, as the reload undid it. The save at the end empties the memory. +UnitTestResult test_once_per_save_retry(){ + BossLab lab; + StoryStep battle = make(StoryStepKind::BATTLE, "rival"); + battle.may_lose = true; + lab.table.steps.back() = battle; + lab.executor.failures_left = {{"rival", 1}}; + Segment segment = lab.segment(); + SegmentHooks hooks = lab.hooks(); + RunState state; + const SegmentReport report = run_segment(state, segment, SegmentRange{}, SegmentPolicy{}, hooks); + if (!report.completed || report.reloads != 1 || lab.count("GAME teach_known") != 1 + || lab.count("GAME teach_new") != 2 || lab.count("BATTLE rival") != 2 + ){ + return "Expected the known TEACH once, the new one twice, the battle twice:" + joined(lab.executor.trace) + joined(lab.lines); + } + if (!lab.prepared.empty()){ + return "Expected the save to empty the prepared steps."; + } + return true; +} + +// Verify mode: three runs from one save; the preparation that changed +// nothing runs in the first only. +UnitTestResult test_once_per_save_verify(){ + BossLab lab; + StoryStep battle = make(StoryStepKind::BATTLE, "rival"); + battle.may_lose = true; + lab.table.steps.back() = battle; + Segment segment = lab.segment(); + SegmentHooks hooks = lab.hooks(); + RunState state; + const VerifyReport report = verify_checkpoint(state, segment, "00.15b", VerifyPolicy{}, hooks); + if (!report.verified || report.runs != 3 || lab.count("GAME teach_known") != 1 + || lab.count("GAME teach_new") != 3 || lab.count("BATTLE rival") != 3 + ){ + return "Expected the known TEACH once in 3 runs, the new one each run:" + joined(lab.executor.trace) + joined(lab.lines); + } + // Without the memory, every run prepares. + FakeExecutor plain; + plain.unchanged = {"teach_known"}; + Segment without; + without.id = "00"; + without.checkpoints.push_back(CheckpointSpec{ + .id = lab.table.id, .title = "", .saves = true, + .build = [&]{ return build_story_checkpoint(lab.table, plain); }, + }); + RunState again; + verify_checkpoint(again, without, "00.15b", VerifyPolicy{}, hooks); + if (std::count(plain.trace.begin(), plain.trace.end(), "GAME teach_known") != 3){ + return "Expected every run to prepare without PreparedSteps:" + joined(plain.trace); + } + 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); + 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); + } + return true; +} + +// Two sections of a story, as BDSP has segments 00 and 01. +const std::vector& two_sections(){ + static const std::vector rows{ + StoryCheckpoint{.id = "00.01", .title = "Leave the house", .start = std::nullopt, .steps = {}}, + StoryCheckpoint{.id = "00.02", .title = "Walk to Sandgem Town", .start = std::nullopt, .steps = {}}, + StoryCheckpoint{.id = "01.01", .title = "Jubilife to Barry", .start = std::nullopt, .steps = {}}, + }; + return rows; +} +const StringSelectDatabase& two_sections_database(){ + static const StringSelectDatabase database = story_checkpoint_database(two_sections()); + return database; +} + +// The dropdowns list the checkpoints as "id title", slugged by their index +// in the whole story, which is story_index() of a section's own index. +UnitTestResult test_checkpoint_dropdowns(){ + const StringSelectDatabase& database = two_sections_database(); + const std::vector expected{"00.01 Leave the house", "00.02 Walk to Sandgem Town", "01.01 Jubilife to Barry"}; + if (database.size() != expected.size()){ + return "Expected 3 entries, got " + std::to_string(database.size()); + } + for (size_t c = 0; c < expected.size(); c++){ + if (database[c].display_name != expected[c] || database[c].slug != std::to_string(c)){ + return "Unexpected entry " + std::to_string(c) + ": " + database[c].slug + " " + database[c].display_name; + } + } + if (database[story_index(true, 0, 2)].display_name != "01.01 Jubilife to Barry"){ + return "Expected the second section's first entry at story_index(true, 0, 2)."; + } + if (story_checkpoint_label(StoryCheckpoint{.id = "00.09", .title = "Pewter Gym: Camper Liam, then Brock", .start = std::nullopt, .steps = {}}) + != "00.09 Pewter Gym: Camper Liam, then Brock" + ){ + return "Unexpected label."; + } + StoryRunOptions options(database); + if (options.start_index() != 0 || options.end_index() != 2 || !options.check_validity().empty()){ + return "Expected the whole story by default."; + } + options.START_CHECKPOINT.set_by_index(2); + options.END_CHECKPOINT.set_by_index(1); + if (options.check_validity().empty()){ + return "Expected a start after the end to be refused."; + } + return true; +} + +// A segment's report in the shared AutoStory stats. +UnitTestResult test_story_stats(){ + SegmentReport report; + report.reloads = 2; + report.runs = { + CheckpointRun{.id = "00.03", .passed = true, .attempts = 1, .recoveries = 0}, + CheckpointRun{.id = "00.04", .passed = true, .attempts = 3, .recoveries = 0}, + CheckpointRun{.id = "00.05", .passed = false, .attempts = 1, .recoveries = 0}, + }; + AutoStoryStats stats; + record_story_stats(stats, report); + if (stats.m_segment != 1 || stats.m_checkpoint != 2 || stats.m_reset != 2){ + return "Expected 1 segment, 2 checkpoints and 2 resets."; + } + return true; +} + +// The program's option group: 3 attempts, no stop, Thorough, by default; +// Fast when chosen. +UnitTestResult test_run_options(){ + StoryRunOptions options(two_sections_database()); + 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."; + } + // 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; +} + +// Verify mode: off by default; 3 runs and 2 passes when on, for one +// checkpoint only, never needing more passes than runs. +UnitTestResult test_verify_options(){ + StoryRunOptions options(two_sections_database()); + if (options.VERIFY.policy()){ + return "Expected verify mode off by default."; + } + options.VERIFY.set_enabled(true); + if (options.check_validity().empty()){ + return "Expected verify mode refused over more than one checkpoint."; + } + options.END_CHECKPOINT.set_by_index(options.start_index()); + const std::optional policy = options.VERIFY.policy(); + if (!options.check_validity().empty() || !policy || policy->runs != 3 || policy->passes_needed != 2){ + return "Expected 3 runs and 2 passes on one checkpoint: " + options.check_validity(); + } + options.VERIFY.RUNS.set(2); + options.VERIFY.PASSES.set(3); + if (options.check_validity().empty()){ + return "Expected more passes than runs refused."; + } + 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; +} + + +struct StoryStepInput{ + std::string name; + UnitTestResult (*test)(); +}; + +struct StoryStepExpectation{ + const char* description; +}; + +struct StoryStepTestCase{ + StoryStepInput input; + StoryStepExpectation expectation; +}; + +class Test_StorySteps : public UnitTest{ +public: + explicit Test_StorySteps(StoryStepTestCase scenario) + : UnitTest("Pokemon::StorySteps - " + scenario.input.name) + , m_scenario(std::move(scenario)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + return m_scenario.input.test(); + } + +private: + StoryStepTestCase m_scenario; +}; + +const StoryStepTestCase STORY_STEP_CASES[] = { + { + .input = {.name = "steps run in table order", .test = test_events_in_table_order}, + .expectation = {.description = "executes story steps sequentially as defined in table"}, + }, + { + .input = {.name = "skip_if asks the executor", .test = test_skip_if}, + .expectation = {.description = "delegates skip condition check to executor"}, + }, + { + .input = {.name = "each step starts where the last left the player", .test = test_pose_propagation}, + .expectation = {.description = "propagates player pose across sequential step executions"}, + }, + { + .input = {.name = "a failure names its step", .test = test_failure_names_the_step}, + .expectation = {.description = "records failing step identifier on error"}, + }, + { + .input = {.name = "unsupported kinds and lost battles", .test = test_unsupported_and_lost_battles}, + .expectation = {.description = "flags unsupported step types and handles battle defeat"}, + }, + { + .input = {.name = "tables as a segment, with a reload", .test = test_segment_of_tables}, + .expectation = {.description = "converts step tables into segment with checkpoint reload support"}, + }, + { + .input = {.name = "lint", .test = test_lint}, + .expectation = {.description = "verifies lint validation rules on story checkpoints"}, + }, + { + .input = {.name = "the checks level reaches the steps", .test = test_checks_level}, + .expectation = {.description = "transfers checks granularity option to step execution"}, + }, + { + .input = {.name = "the run options' defaults", .test = test_run_options}, + .expectation = {.description = "validates initial default story run option values"}, + }, + { + .input = {.name = "the run options' verify mode", .test = test_verify_options}, + .expectation = {.description = "configures verification mode options and passes count"}, + }, + { + .input = {.name = "start and end checkpoint dropdowns", .test = test_checkpoint_dropdowns}, + .expectation = {.description = "populates start and end dropdown selections correctly"}, + }, + { + .input = {.name = "a segment's run in the AutoStory stats", .test = test_story_stats}, + .expectation = {.description = "updates story run statistics following segment completion"}, + }, + { + .input = {.name = "policy: attempts per checkpoint, then fail", .test = test_policy_attempts_then_fail}, + .expectation = {.description = "enforces max checkpoint attempt policy before terminal failure"}, + }, + { + .input = {.name = "policy: stop at the first failure", .test = test_policy_stop_at_first_failure}, + .expectation = {.description = "halts execution immediately when stop_at_first_failure is enabled"}, + }, + { + .input = {.name = "policy: recover in place before reloading", .test = test_policy_recovers_in_place}, + .expectation = {.description = "applies in-place recovery handler prior to reloading save"}, + }, + { + .input = {.name = "once per save: a retry skips what changed nothing", .test = test_once_per_save_retry}, + .expectation = {.description = "skips steps that changed nothing during once per save retry"}, + }, + { + .input = {.name = "once per save: verify runs prepare once", .test = test_once_per_save_verify}, + .expectation = {.description = "executes once per save prepare step only once during verification"}, + }, +}; + +} // namespace + + +void add_tests_StorySteps(UnitTestDatabase& database){ + for (const auto& scenario : STORY_STEP_CASES){ + database.add(scenario); + } +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp new file mode 100644 index 0000000000..6df0e9df31 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp @@ -0,0 +1,72 @@ +/* Story Variant + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Pokemon_StoryVariant.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +std::vector variant_words(std::string_view text){ + std::vector words; + while (!text.empty()){ + const size_t space = text.find(' '); + const std::string_view word = text.substr(0, space); + if (!word.empty()){ + words.push_back(word); + } + if (space == std::string_view::npos){ + break; + } + text.remove_prefix(space + 1); + } + return words; +} + +std::optional variant_pair(std::string_view word){ + const size_t equals = word.find('='); + if (equals == std::string_view::npos || equals == 0){ + return std::nullopt; + } + return VariantPair{word.substr(0, equals), word.substr(equals + 1)}; +} + +std::string_view variant_name(std::string_view variant){ + const std::vector words = variant_words(variant); + if (words.empty() || variant_pair(words[0])){ + return {}; + } + return words[0]; +} + +std::optional variant_value(std::string_view variant, std::string_view key){ + for (std::string_view word : variant_words(variant)){ + const std::optional pair = variant_pair(word); + if (pair && pair->key == key){ + return pair->value; + } + } + return std::nullopt; +} + +std::optional variant_number(std::string_view text, size_t max_digits){ + if (text.empty() || text.size() > max_digits){ + return std::nullopt; + } + size_t value = 0; + for (char c : text){ + if (c < '0' || c > '9'){ + return std::nullopt; + } + value = value * 10 + size_t(c - '0'); + } + return value; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h new file mode 100644 index 0000000000..f136fa147b --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h @@ -0,0 +1,51 @@ +/* Story Variant + * + * From: https://github.com/PokemonAutomation/ + * + * Reading a step's variant (StoryStep::variant): a name, then words split + * on spaces, some of them "key=value" pairs and small numbers. A table + * writes "mart PewterCity_Mart budget=2000 potion=5"; the name ("mart") + * picks the step's action (Pokemon_StoryActions.h) and the action reads + * the rest. + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryVariant_H +#define PokemonAutomation_Pokemon_StoryVariant_H + +#include +#include +#include +#include + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// `text`'s words, split on spaces, empty words dropped. +std::vector variant_words(std::string_view text); + +// A word "key=value" as its two halves. +struct VariantPair{ + std::string_view key; + std::string_view value; +}; +// `word` split at its first '='; nothing without one or with an empty key. +std::optional variant_pair(std::string_view word); + +// The variant's name: its first word, unless that is a "key=value" pair. +// Empty for an empty variant or one of pairs only. +std::string_view variant_name(std::string_view variant); + +// The value of the first "key=value" word with `key`; nothing if none. +std::optional variant_value(std::string_view variant, std::string_view key); + +// `text` as a number of up to `max_digits` decimal digits; nothing if it +// isn't one. +std::optional variant_number(std::string_view text, size_t max_digits = 6); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp new file mode 100644 index 0000000000..5ce0fb7ab0 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp @@ -0,0 +1,161 @@ +/* Story Run Options + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Pokemon_StoryRunOptions.h" + +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; +} + +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; +} + + +std::string story_checkpoint_label(const StoryCheckpoint& checkpoint){ + return checkpoint.title.empty() ? checkpoint.id : checkpoint.id + " " + checkpoint.title; +} + + +StoryVerifyOptions::StoryVerifyOptions() + : GroupOption( + "Verify Mode: run the start checkpoint (also the end) from its save several times, never saving at its end", + LockMode::LOCK_WHILE_RUNNING, + EnableMode::DEFAULT_DISABLED + ) + , RUNS( + "Runs:
Runs of the checkpoint, each from its save.", + LockMode::LOCK_WHILE_RUNNING, + 3, 1, 20 + ) + , PASSES( + "Passes needed:
Runs that must pass for the checkpoint to be verified.", + LockMode::LOCK_WHILE_RUNNING, + 2, 1, 20 + ) +{ + PA_ADD_OPTION(RUNS); + PA_ADD_OPTION(PASSES); +} + +std::optional StoryVerifyOptions::policy() const{ + if (!enabled()){ + return std::nullopt; + } + return VerifyPolicy{.runs = RUNS, .passes_needed = PASSES}; +} + +std::string StoryVerifyOptions::check_validity() const{ + if (enabled() && PASSES > RUNS){ + return "Verify mode can't need more passes than it has runs."; + } + return GroupOption::check_validity(); +} + + +StoryRunOptions::StoryRunOptions(const StringSelectDatabase& checkpoints) + : GroupOption("Story Run", LockMode::LOCK_WHILE_RUNNING) + , START_CHECKPOINT( + "Start checkpoint:
The program starts from this checkpoint's save.", + checkpoints, + LockMode::LOCK_WHILE_RUNNING, + 0 + ) + , END_CHECKPOINT( + "End checkpoint:
The program stops after this checkpoint.", + checkpoints, + LockMode::LOCK_WHILE_RUNNING, + checkpoints.size() == 0 ? 0 : checkpoints.size() - 1 + ) + , 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 + ) + , 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 + ) + , 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(START_CHECKPOINT); + PA_ADD_OPTION(END_CHECKPOINT); + PA_ADD_OPTION(ATTEMPTS_PER_CHECKPOINT); + PA_ADD_OPTION(STOP_AT_FIRST_FAILURE); + PA_ADD_OPTION(CHECKS); + PA_ADD_OPTION(LANGUAGE); + PA_ADD_OPTION(VERIFY); +} + +size_t StoryRunOptions::start_index() const{ + return START_CHECKPOINT.index(); +} + +size_t StoryRunOptions::end_index() const{ + return END_CHECKPOINT.index(); +} + +std::string StoryRunOptions::check_validity() const{ + if (start_index() > end_index()){ + return "The start checkpoint comes after the end checkpoint."; + } + if (VERIFY.enabled() && start_index() != end_index()){ + return "Verify mode runs one checkpoint: set the end checkpoint to the start checkpoint."; + } + return GroupOption::check_validity(); +} + +SegmentPolicy StoryRunOptions::policy() const{ + SegmentPolicy policy; + policy.attempts_per_checkpoint = ATTEMPTS_PER_CHECKPOINT; + policy.stop_at_first_failure = STOP_AT_FIRST_FAILURE; + return policy; +} + +StoryChecks StoryRunOptions::checks() const{ + return CHECKS; +} + +Language StoryRunOptions::language() const{ + return LANGUAGE; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h new file mode 100644 index 0000000000..c54482cc52 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -0,0 +1,119 @@ +/* Story Run Options + * + * From: https://github.com/PokemonAutomation/ + * + * The options every AutoStory program shows for where to start and end, how + * hard to try and how much to check: the same group in each game, read into the shared + * SegmentPolicy (Pokemon/AutoStory/Pokemon_StorySegments.h) and the + * executor's checks level (Pokemon_StorySteps.h). + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryRunOptions_H +#define PokemonAutomation_Pokemon_StoryRunOptions_H + +#include +#include +#include +#include +#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 "CommonTools/Options/StringSelectOption.h" +#include "Pokemon/AutoStory/Pokemon_AutoStorySelect.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// A checkpoint's entry in the start and end dropdowns, as SV labels its +// segments: "00.09 Pewter Gym: Camper Liam, then Brock". +struct StoryCheckpointEntry{ + std::string label; + const std::string& name() const{ + return label; + } +}; +// Formats a checkpoint identifier and description into a dropdown label. +std::string story_checkpoint_label(const StoryCheckpoint& checkpoint); + +// The start/end dropdown database of a story's checkpoints, `rows` in +// order, slugged by their index in it (make_story_select_database()). A +// game whose story is in sections (BDSP's segments 00 and 01) passes them +// joined: a section's entry i is then story_index(true, i, first of the +// section). The program keeps it in a function-local static: the options +// hold a reference. +template +StringSelectDatabase story_checkpoint_database(const std::vector& rows); + +template +StringSelectDatabase story_checkpoint_database(const std::vector& rows){ + std::vector> entries; + for (const StoryCheckpoint& row : rows){ + entries.emplace_back(std::make_unique(StoryCheckpointEntry{.label = story_checkpoint_label(row)})); + } + return make_story_select_database(entries, 0, entries.size()); +} + + +// Verify mode, off by default (verify_checkpoint()): the start checkpoint +// RUNS times from its save, never saving at its end; verified when at +// least PASSES pass. For a battle that has lost twice or more, given its +// own checkpoint saved right before it. +class StoryVerifyOptions : public GroupOption{ +public: + StoryVerifyOptions(); + + // The runs and passes, unset when verify mode is off. + std::optional policy() const; + virtual std::string check_validity() const override; + +public: + SimpleIntegerOption RUNS; + SimpleIntegerOption PASSES; +}; + + +// Configuration options panel for selecting story checkpoints and execution mode. +class StoryRunOptions : public GroupOption{ +public: + // `checkpoints`: story_checkpoint_database() of the game's story, kept + // alive by the caller; the dropdowns start on its first and last. + explicit StoryRunOptions(const StringSelectDatabase& checkpoints); + + // The checkpoints to run, by index in the database, both inclusive. + size_t start_index() const; + size_t end_index() const; + // "The start checkpoint comes after the end checkpoint." if it does. + virtual std::string check_validity() const override; + + // 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; + // The game's language, for the executor (set_language()). + Language language() const; + +public: + StringSelectOption START_CHECKPOINT; + StringSelectOption END_CHECKPOINT; + 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; + // On, it runs one checkpoint: the start, which must also be the end. + StoryVerifyOptions VERIFY; +}; + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index ba4906e546..69e99c18a7 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1491,6 +1491,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/AutoStory/Pokemon_AutoStorySelect.h Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.cpp Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h @@ -1502,6 +1504,17 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp Source/Pokemon/AutoStory/Pokemon_StorySegments.h Source/Pokemon/AutoStory/Pokemon_StorySegments_Tests.cpp + Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp + Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h + Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp + Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp + Source/Pokemon/AutoStory/Pokemon_StoryVariant.h + Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp + Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h + Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp + Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp + Source/Pokemon/AutoStory/Pokemon_StorySteps.h + Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.h Source/Pokemon/Pokemon_AdvRng.cpp diff --git a/devtools/story-data-tests/CMakeLists.txt b/devtools/story-data-tests/CMakeLists.txt new file mode 100644 index 0000000000..978db4f6d0 --- /dev/null +++ b/devtools/story-data-tests/CMakeLists.txt @@ -0,0 +1,43 @@ +# Offline codec checks, no Qt application or controller initialization: +# cmake -S devtools/story-data-tests -B build/story-data-tests +# cmake --build build/story-data-tests -j4 +# ctest --test-dir build/story-data-tests --output-on-failure +cmake_minimum_required(VERSION 3.24) +project(StoryDataTests LANGUAGES CXX) +enable_testing() +get_filename_component(ROOT "../.." ABSOLUTE) +set(STORY "${ROOT}/SerialPrograms/Source/Pokemon/AutoStory") +add_executable(StoryDataTests + "${STORY}/Pokemon_StoryStepJson.cpp" + "${STORY}/Pokemon_StoryStepJson_Tests.cpp" + "${STORY}/Pokemon_TilePose.cpp" + "${ROOT}/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Grid.cpp" + "${ROOT}/Common/Cpp/Exceptions.cpp" + "${ROOT}/Common/Cpp/Json/JsonValue.cpp" + "${ROOT}/Common/Cpp/Json/JsonObject.cpp" + "${ROOT}/Common/Cpp/Json/JsonArray.cpp" + "${ROOT}/Common/Cpp/Json/JsonTools.cpp" + "${ROOT}/Common/Cpp/Filesystem/FileIO.cpp" + "${ROOT}/Common/Cpp/Filesystem/FilePath.cpp" + "${ROOT}/Common/Cpp/Strings/Unicode.cpp") +# Reuse the Qt/controller-free support sources; each codec has its own test main. +get_target_property(STORY_DATA_SOURCES StoryDataTests SOURCES) +list(REMOVE_ITEM STORY_DATA_SOURCES "${STORY}/Pokemon_StoryStepJson_Tests.cpp") +add_executable(AuthoredCheckpointTests ${STORY_DATA_SOURCES} + "${STORY}/Pokemon_AuthoredCheckpointJson.cpp" + "${STORY}/Pokemon_AuthoredCheckpointJson_Tests.cpp") + +foreach(TEST_TARGET IN ITEMS StoryDataTests AuthoredCheckpointTests) +target_include_directories(${TEST_TARGET} PRIVATE "${ROOT}" "${ROOT}/SerialPrograms/Source") +target_compile_features(${TEST_TARGET} PRIVATE cxx_std_23) +target_compile_definitions(${TEST_TARGET} PRIVATE PA_STORY_DATA_STANDALONE) +if(MSVC) + target_compile_definitions(${TEST_TARGET} PRIVATE _CRT_SECURE_NO_WARNINGS) + target_compile_options(${TEST_TARGET} PRIVATE /W4 /WX) + set_property(SOURCE "${ROOT}/Common/Cpp/Filesystem/FileIO.cpp" APPEND PROPERTY COMPILE_OPTIONS /wd4505) +else() + target_compile_options(${TEST_TARGET} PRIVATE -Wall -Wextra -Werror) +endif() +endforeach() +add_test(NAME StoryData COMMAND StoryDataTests) +add_test(NAME AuthoredCheckpoint COMMAND AuthoredCheckpointTests) diff --git a/devtools/story-data-tests/README.md b/devtools/story-data-tests/README.md new file mode 100644 index 0000000000..310cea3ea3 --- /dev/null +++ b/devtools/story-data-tests/README.md @@ -0,0 +1,37 @@ +# Shared story-record codec tests + +Run without Qt, controllers, profiles or game saves: + +```sh +cmake -S devtools/story-data-tests -B build/story-data-tests +cmake --build build/story-data-tests -j4 +ctest --test-dir build/story-data-tests --output-on-failure +build/story-data-tests/StoryDataTests /path/to/PokemonSimulation/data/story/wire-conformance.json +``` + +For a multi-configuration generator, add `--config Debug` to the build, +`-C Debug` to CTest and use `build/story-data-tests/Debug/StoryDataTests.exe`. +The same native cases are registered in the application's unit-test runner. + +`Pokemon_StoryStepJson` encodes and reads the common `StoryCheckpoint`, +`StoryStep` and tile-based `Pose` records. It retains action order, alternative +tiles, certainty, absent facing and explicit false/zero values. Unknown fields, +action/position tags and invalid primitive types fail. Native default values +are omitted canonically; encoding does not establish semantic route validity. + +The portable workflow also reads the 31 synthetic conformance cases at +[PokemonSimulation revision 866613f](https://github.com/dayfine/PokemonSimulation/tree/866613fa03e5c4a5eb7e57c91b59fafb44fc7ee3/data/story). +Python reads the same input/expectation records. This fixture revision is a +contract-test dependency, not an approved map-resource revision. + +See the [shared checkpoint contract](https://github.com/dayfine/PokemonSimulation/blob/866613fa03e5c4a5eb7e57c91b59fafb44fc7ee3/docs/story/CHECKPOINT_CONTRACT.md) +for the field boundary and proposed common requirements, state, save/retry and +handoff envelope. Both games can use those policies; their menu/detector/action +bindings remain game specific. + +These are synthetic codec tests, not exports of either game's authored story +tables or captured controller replay. Evaluated FRLG/BDSP table exporters, +policy/state envelope binding and execution parity remain subsequent work. +No gameplay behavior changes here. + +The same CTest command also runs `AuthoredCheckpointTests`: shared authored-envelope parsing/writing and embedded-resource lookup for FRLG, BDSP, and opaque future game settings. Game adapters must validate their own execution object before running anything. The shared codec does not interpret FRLG menus, BDSP save-card policy, native bookkeeping, or controller evidence. From 5b974f0c750680a314cc8e2ed536d9293bc3be08 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 20:22:58 -0800 Subject: [PATCH 2/8] AutoStory: story data types in Types/, executor as a data-free interface, spacing Review of #312: - The plain story data types move out of Pokemon_StorySteps.h into Pokemon/AutoStory/Types/ (StoryStepKind, StoryChecks, StoryStep with StoryLeg and StoryItem, StoryCheckpoint). Pokemon_StorySteps.h still includes them, so its users build unchanged; the JSON codec and the run options now include only the types. - StoryStepExecutor is an interface with no data and no protected or private members: begin_attempt(), run() (pure) and holds(). The 15 per-kind virtuals and dispatch_story_step() go: a game dispatches by kind in run(), as the StoryActionTable of the next layer does. The executor's checks level and language go too; nothing read them, and the program passes StoryRunOptions' values to the game's own steps. - Read-only sequence parameters take std::span. - Spacing: header comment blocks, blank lines after includes, around namespaces and between definitions, one statement per line. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Pokemon_AuthoredCheckpointJson.cpp | 3 + .../Pokemon_AuthoredCheckpointJson.h | 5 +- .../Pokemon_AuthoredCheckpointJson_Tests.cpp | 36 +++- .../AutoStory/Pokemon_AutoStory_Tests.h | 10 +- .../Pokemon/AutoStory/Pokemon_StoryEvents.cpp | 2 + .../AutoStory/Pokemon_StoryStepJson.cpp | 34 +++- .../Pokemon/AutoStory/Pokemon_StoryStepJson.h | 24 ++- .../AutoStory/Pokemon_StoryStepJson_Tests.cpp | 67 ++++++- .../Pokemon/AutoStory/Pokemon_StorySteps.cpp | 133 ------------- .../Pokemon/AutoStory/Pokemon_StorySteps.h | 179 ++---------------- .../AutoStory/Pokemon_StorySteps_Tests.cpp | 82 ++++---- .../Pokemon/AutoStory/Pokemon_StoryVariant.h | 1 + .../AutoStory/Types/Pokemon_StoryCheckpoint.h | 37 ++++ .../AutoStory/Types/Pokemon_StoryChecks.cpp | 26 +++ .../AutoStory/Types/Pokemon_StoryChecks.h | 33 ++++ .../AutoStory/Types/Pokemon_StoryStep.h | 87 +++++++++ .../AutoStory/Types/Pokemon_StoryStepKind.cpp | 52 +++++ .../AutoStory/Types/Pokemon_StoryStepKind.h | 43 +++++ .../Pokemon/Options/Pokemon_StoryRunOptions.h | 15 +- SerialPrograms/cmake/SourceFiles.cmake | 6 + 20 files changed, 509 insertions(+), 366 deletions(-) create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp index 699ece6df1..63cce48b7a 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp @@ -1,5 +1,7 @@ /* Shared Authored Checkpoint JSON + * * From: https://github.com/PokemonAutomation/ + * */ #include @@ -63,5 +65,6 @@ AuthoredCheckpoint load_authored_checkpoint( } + } // namespace Pokemon } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h index 1b1be213dd..5bcbc181a2 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h @@ -1,5 +1,7 @@ /* Shared Authored Checkpoint JSON + * * From: https://github.com/PokemonAutomation/ + * */ #ifndef PokemonAutomation_AuthoredCheckpointJson_H @@ -11,7 +13,7 @@ #include #include #include "Common/Cpp/Json/JsonObject.h" -#include "Pokemon_StorySteps.h" +#include "Types/Pokemon_StoryCheckpoint.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -47,6 +49,7 @@ AuthoredCheckpoint load_authored_checkpoint( ); + } // namespace Pokemon } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp index 7aba0e12f4..d01a4fe633 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp @@ -1,8 +1,12 @@ /* Shared Authored Checkpoint JSON Tests + * * From: https://github.com/PokemonAutomation/ + * */ #include +#include +#include #include #include "Common/Cpp/Exceptions.h" #include "Pokemon_AuthoredCheckpointJson.h" @@ -13,13 +17,21 @@ namespace PokemonAutomation{ namespace Pokemon{ + + namespace{ + // Documents intentionally contain no game headers or native bookkeeping. const std::string FRLG = R"({"format":"pokesim-authored-checkpoint/1","game":"frlg","checkpoint":{"id":"00.05","title":"Parcel","events":[{"id":"first","kind":"route"},{"id":"second","kind":"scene"}]},"execution":{"menu":"before_pokedex"}})"; const std::string BDSP = R"({"format":"pokesim-authored-checkpoint/1","game":"bdsp","checkpoint":{"id":"01.03","title":"Mine","events":[{"id":"roark","kind":"talk"}]},"execution":{"on_failure":"reload_and_retry","saves_in":"Oreburgh City"}})"; -enum class Failure{ NONE, PARSE, MISSING_RESOURCE }; +// How a case fails, if it does. +enum class Failure{ + NONE, + PARSE, + MISSING_RESOURCE, +}; struct AuthoredInput{ std::string name; @@ -60,6 +72,9 @@ const AuthoredCase CASES[] = { .checkpoint = "x", .execution = R"({"zero":0,"false":false,"null":null})", .events = {}}}, }; + +// "" when `actual` is what `test` expects and survives a write and a +// read, else what was expected and what was read. std::string check_loaded(const AuthoredCheckpoint& actual, const AuthoredCase& test){ if (test.expectation.failure != Failure::NONE){ return "Expected rejection, read accepted authored checkpoint."; @@ -85,6 +100,7 @@ std::string check_loaded(const AuthoredCheckpoint& actual, const AuthoredCase& t return ""; } +// "" when `test` reads or fails as expected. std::string check_case(const AuthoredCase& test){ try{ const std::vector> files = {{"plan", test.input.json}}; @@ -99,34 +115,49 @@ std::string check_case(const AuthoredCase& test){ } } + #ifndef PA_STORY_DATA_STANDALONE + class AuthoredJsonTest : public UnitTest{ public: explicit AuthoredJsonTest(AuthoredCase test) - : UnitTest("Pokemon::AuthoredCheckpointJson - " + test.input.name), m_case(std::move(test)){} + : UnitTest("Pokemon::AuthoredCheckpointJson - " + test.input.name) + , m_case(std::move(test)) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ const std::string error = check_case(m_case); return error.empty() ? UnitTestResult(true) : UnitTestResult(error); } + private: AuthoredCase m_case; }; + #endif + } // namespace + #ifndef PA_STORY_DATA_STANDALONE + void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database){ for (const AuthoredCase& test : CASES){ database.add(test); } } + #endif + + } // namespace Pokemon } // namespace PokemonAutomation + #ifdef PA_STORY_DATA_STANDALONE + int main(){ int failed = 0; for (const auto& test : PokemonAutomation::Pokemon::CASES){ @@ -136,4 +167,5 @@ int main(){ } return failed != 0; } + #endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h index 5d0bbe1172..0efa649a7c 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -21,15 +21,13 @@ void add_tests_StoryEvents(UnitTestDatabase& database); void add_tests_StorySegments(UnitTestDatabase& database); // Registers unit tests for high-level story step dispatching and table conversion. void add_tests_StorySteps(UnitTestDatabase& database); - - - -// Lossless shared story-data wire contract. +// Registers unit tests for the lossless shared story-data wire contract. void add_tests_StoryStepJson(UnitTestDatabase& database); - -// Shared authored envelope, game identity and embedded-resource loading. +// Registers unit tests for the shared authored envelope, game identity and embedded-resource loading. void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database); + + } // namespace Pokemon } // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp index ad22d7a6a7..35c61a688c 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp @@ -68,11 +68,13 @@ Outcome outcome_ok(Pose pose){ outcome.pose = std::move(pose); return outcome; } + Outcome outcome_unchanged(){ Outcome outcome; outcome.changed_nothing = true; return outcome; } + Outcome outcome_failed(std::string failure, std::string detail){ Outcome outcome; outcome.failure = std::move(failure); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp index 3ee2f51f6d..78427f9667 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp @@ -1,19 +1,31 @@ /* Story Step JSON + * * From: https://github.com/PokemonAutomation/ + * */ + #include #include #include #include +#include #include +#include #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonArray.h" #include "Common/Cpp/Json/JsonObject.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" #include "Pokemon_StoryStepJson.h" + namespace PokemonAutomation{ namespace Pokemon{ + + namespace{ + + +// The wire names of the step kinds. constexpr std::array, 15> KINDS{{ {StoryStepKind::NEW_GAME, "new_game"}, {StoryStepKind::OPTIONS, "options"}, @@ -31,6 +43,8 @@ constexpr std::array, 15> KINDS{{ {StoryStepKind::READY, "ready"}, {StoryStepKind::GAME, "game_action"}, }}; + +// The wire names of the pose certainties. constexpr std::array, 5> CERTAINTIES{{ {PoseCertainty::TILE, "tile"}, {PoseCertainty::COLUMN, "column"}, @@ -38,6 +52,8 @@ constexpr std::array, 5> CERTAINTIES{ {PoseCertainty::SET, "set"}, {PoseCertainty::UNKNOWN, "unknown"}, }}; + +// The wire names of the directions. constexpr std::array, 4> DIRECTIONS{{ {Direction::NORTH, "north"}, {Direction::EAST, "east"}, @@ -45,6 +61,8 @@ constexpr std::array, 4> DIRECTIONS{{ {Direction::WEST, "west"}, }}; + +// `value` as an object with no field outside `keys`. const JsonObject& fields(const JsonValue& value, std::initializer_list keys, std::string_view source){ const JsonObject& object = value.to_object_throw(std::string(source)); for (const auto& [key, unused] : object){ @@ -102,7 +120,7 @@ JsonValue count_json(size_t value){ return (uint64_t)value; } -JsonArray strings_json(const std::vector& strings){ +JsonArray strings_json(std::span strings){ JsonArray result; for (const std::string& value : strings){ result.push_back(value); @@ -118,7 +136,7 @@ std::vector read_strings(const JsonValue& value, std::string_view s return result; } -JsonArray legs_json(const std::vector& legs){ +JsonArray legs_json(std::span legs){ JsonArray result; for (const StoryLeg& leg : legs){ JsonObject item; @@ -139,7 +157,7 @@ std::vector read_legs(const JsonValue& value, std::string_view source) return result; } -JsonArray items_json(const std::vector& items){ +JsonArray items_json(std::span items){ JsonArray result; for (const StoryItem& item : items){ JsonObject row; @@ -158,6 +176,8 @@ std::vector read_items(const JsonValue& value, std::string_view sourc } return result; } + +// `result` with the step's yes/no and count fields read from `root`. StoryStep read_step_options(StoryStep result, const JsonObject& root, std::string_view source){ if (const JsonValue* field = root.get_value("fight_wild")){ result.fight_wild = field->to_boolean_throw(); @@ -174,8 +194,10 @@ StoryStep read_step_options(StoryStep result, const JsonObject& root, std::strin return result; } + } // namespace + JsonValue story_pose_json(const Pose& pose){ JsonObject result; result["map"] = pose.map; @@ -191,7 +213,8 @@ JsonValue story_pose_json(const Pose& pose){ JsonArray positions; for (const TilePos& tile : tiles->tiles){ JsonArray pair; - pair.push_back(tile.x); pair.push_back(tile.y); + pair.push_back(tile.x); + pair.push_back(tile.y); positions.push_back(std::move(pair)); } where["tiles"] = std::move(positions); @@ -351,5 +374,8 @@ StoryCheckpoint read_story_checkpoint(const JsonValue& value, std::string_view s } return result; } + + + } // namespace Pokemon } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h index d58fd9828c..b50caadc58 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h @@ -1,26 +1,48 @@ /* Story Step JSON + * * From: https://github.com/PokemonAutomation/ + * + * The shared JSON wire form of the story data types (Types/): poses, + * steps and checkpoints, read strictly and written losslessly. + * */ + #ifndef PokemonAutomation_StoryStepJson_H #define PokemonAutomation_StoryStepJson_H + #include #include "Common/Cpp/Json/JsonValue.h" -#include "Pokemon_StorySteps.h" +#include "Pokemon/AutoStory/Pokemon_StoryEvents.h" +#include "Types/Pokemon_StoryCheckpoint.h" +#include "Types/Pokemon_StoryStep.h" + namespace PokemonAutomation{ namespace Pokemon{ + + // Lossless tile-game pose data, including uncertainty and absent position/facing. // An unsupported Position subtype fails instead of being converted to unknown. JsonValue story_pose_json(const Pose& pose); + // Read a tile-game pose; malformed fields and unsupported position tags fail. Pose read_story_pose(const JsonValue& value, std::string_view source); + + // Encode every shared step field. This does not bind or execute an action. JsonValue story_step_json(const StoryStep& step); + // Read the shared step record; unknown fields/kinds and invalid values fail. StoryStep read_story_step(const JsonValue& value, std::string_view source); + + // Encode the common checkpoint; game adapters supply the shared policy/state envelope. JsonValue story_checkpoint_json(const StoryCheckpoint& checkpoint); + // Read only the common checkpoint record, retaining ordered steps and start belief. StoryCheckpoint read_story_checkpoint(const JsonValue& value, std::string_view source); + + + } // namespace Pokemon } // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp index 021b18f508..269b9f4412 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp @@ -1,7 +1,16 @@ /* Story Step JSON Tests + * * From: https://github.com/PokemonAutomation/ + * + * Each case reads a record from JSON and writes it back: the canonical + * form, or a rejection. Built standalone (PA_STORY_DATA_STANDALONE), it + * also checks the shared case file the Python reader checks. + * */ + #include +#include +#include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonObject.h" #include "Common/Cpp/Json/JsonArray.h" @@ -9,23 +18,39 @@ #ifndef PA_STORY_DATA_STANDALONE #include "Pokemon_AutoStory_Tests.h" #endif + namespace PokemonAutomation{ namespace Pokemon{ + + namespace{ -enum class Record{ POSE, STEP, CHECKPOINT }; + + +// Which record a case reads. +enum class Record{ + POSE, + STEP, + CHECKPOINT, +}; + +// A record to read, and which kind it is. struct WireInput{ std::string_view name; Record record = Record::STEP; std::string_view json; }; + +// What reading it gives: the canonical record, or a rejection. struct WireExpectation{ std::string_view canonical = {}; bool rejected = false; }; + struct WireCase{ WireInput input; WireExpectation expectation; }; + const WireCase CASES[] = { {.input = {.name = "default fields", .json = R"({"id":"talk","kind":"talk"})"}, .expectation = {.canonical = R"({"id":"talk","kind":"talk"})"}}, {.input = {.name = "all shared fields", .json = R"({"id":"all","kind":"game_action","why":"source reason","direction":"west","legs":[{"direction":"south","tiles":0}],"fight_wild":true,"yes":false,"may_lose":true,"battles":3,"items":[{"item":"potion","quantity":0}],"variant":"profile","skip_if":["flag"],"target":{"map":"A","certainty":"unknown","where":{"kind":"unknown"}},"declared_after":{"map":"B","certainty":"set","where":{"kind":"tiles","tiles":[[0,0],[1,0]],"facing":"east"}}})"}, .expectation = {.canonical = R"({"id":"all","kind":"game_action","why":"source reason","direction":"west","legs":[{"direction":"south","tiles":0}],"fight_wild":true,"yes":false,"may_lose":true,"battles":3,"items":[{"item":"potion","quantity":0}],"variant":"profile","skip_if":["flag"],"target":{"map":"A","certainty":"unknown","where":{"kind":"unknown"}},"declared_after":{"map":"B","certainty":"set","where":{"kind":"tiles","tiles":[[0,0],[1,0]],"facing":"east"}}})"}}, @@ -42,16 +67,22 @@ const WireCase CASES[] = { {.input = {.name = "duplicate events", .record = Record::CHECKPOINT, .json = R"({"id":"x","title":"X","events":[{"id":"a","kind":"talk"},{"id":"a","kind":"talk"}]})"}, .expectation = {.rejected = true}}, }; + +// `input` read and written back. JsonValue round_trip(const WireInput& input){ const JsonValue value = parse_json(std::string(input.json)); switch (input.record){ - case Record::POSE: return story_pose_json(read_story_pose(value, input.name)); - case Record::STEP: return story_step_json(read_story_step(value, input.name)); - case Record::CHECKPOINT: return story_checkpoint_json(read_story_checkpoint(value, input.name)); + case Record::POSE: + return story_pose_json(read_story_pose(value, input.name)); + case Record::STEP: + return story_step_json(read_story_step(value, input.name)); + case Record::CHECKPOINT: + return story_checkpoint_json(read_story_checkpoint(value, input.name)); } throw ParseException("Unknown test record."); } +// "" when `test` reads as expected, else what was expected and what was read. std::string check_case(const WireCase& test){ try{ const JsonValue actual = round_trip(test.input); @@ -67,7 +98,11 @@ std::string check_case(const WireCase& test){ } return ""; } + + #ifdef PA_STORY_DATA_STANDALONE + +// A shared case file's record kind. Record record_kind(const std::string& name){ if (name == "pose"){ return Record::POSE; @@ -81,6 +116,7 @@ Record record_kind(const std::string& name){ throw ParseException("Unsupported shared case record kind."); } +// Checks every case of the shared file `filename`; nonzero if any failed. int check_shared_file(const std::string& filename){ const JsonValue document = load_json_file(filename); const JsonObject& root = document.to_object_throw(filename); @@ -109,32 +145,52 @@ int check_shared_file(const std::string& filename){ std::cout << "Checked " << root.get_array_throw("cases").size() << " shared story records.\n"; return failed != 0; } + #endif + #ifndef PA_STORY_DATA_STANDALONE + class StoryJsonTest : public UnitTest{ public: explicit StoryJsonTest(WireCase test) - : UnitTest("Pokemon::StoryStepJson - " + std::string(test.input.name)), m_case(test){} + : UnitTest("Pokemon::StoryStepJson - " + std::string(test.input.name)) + , m_case(test) + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ const std::string error = check_case(m_case); return error.empty() ? UnitTestResult(true) : UnitTestResult(error); } + private: WireCase m_case; }; + #endif + + } // namespace + + #ifndef PA_STORY_DATA_STANDALONE + void add_tests_StoryStepJson(UnitTestDatabase& database){ for (const WireCase& test : CASES){ database.add(test); } } + #endif + + + } // namespace Pokemon } // namespace PokemonAutomation + + #ifdef PA_STORY_DATA_STANDALONE + int main(int argc, char** argv){ if (argc == 2){ return PokemonAutomation::Pokemon::check_shared_file(argv[1]); @@ -147,4 +203,5 @@ int main(int argc, char** argv){ } return failed != 0; } + #endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp index 3004ba6013..5309b3e496 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp @@ -14,61 +14,10 @@ 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 "?"; -} - - -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)); } -Outcome StoryStepExecutor::run(RunState& state, const StoryStep& step){ - return dispatch_story_step(*this, state, step); -} void StoryStepExecutor::begin_attempt(const StoryCheckpoint&){} @@ -76,88 +25,6 @@ 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); -} -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; diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h index 27aaabad8a..7b76155d63 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h @@ -5,21 +5,22 @@ * 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. + * such steps, plain data (Types/), 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. + * StoryStepExecutor to do its step; a game implements the kinds it uses, + * and a step it has no way of doing fails by name (unsupported_step()) + * 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. + * game's language from the program's options (StoryRunOptions), 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 @@ -31,123 +32,28 @@ #ifndef PokemonAutomation_Pokemon_StorySteps_H #define PokemonAutomation_Pokemon_StorySteps_H -#include #include #include #include #include -#include "CommonFramework/Language.h" #include "Pokemon/Navigation/Pokemon_Grid.h" #include "Pokemon_AutoStoryStats.h" #include "Pokemon_StoryEvents.h" #include "Pokemon_StorySegments.h" #include "Pokemon_TilePose.h" +#include "Types/Pokemon_StoryChecks.h" +#include "Types/Pokemon_StoryCheckpoint.h" +#include "Types/Pokemon_StoryStep.h" +#include "Types/Pokemon_StoryStepKind.h" namespace PokemonAutomation{ namespace Pokemon{ -// Action category classification for individual story steps. -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` -}; -// Returns the readable string representation of a story step kind. -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, -}; -// Returns the readable string representation of a story check level. -const char* story_checks_name(StoryChecks checks); - - -// 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; -}; - -// Specification of an atomic story task with actions and postconditions. -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()), run flags - // unless the game reads them off the screen. - std::vector skip_if; - // Preparation for what follows, done once per save: a TEACH, a check, - // a read. Once it has succeeded and changed nothing - // (Outcome::changed_nothing), the attempts after a reload of the same - // save skip it, so a retry goes from the reload straight to the - // battle. One that changed the game (a TM taught) runs again, as the - // reload undid it. Only with build_story_checkpoint()'s PreparedSteps. - bool once_per_save = false; - // 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; -}; - -// Milestone row in the story table grouping related steps and starting pose. -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. +// How a game does its story steps: an interface with no data. run() is the +// game's: it does the step's kind its own way (FRLG and BDSP bind kinds and +// variants to actions in a table), and returns unsupported_step() for a +// kind it has no way of doing. class StoryStepExecutor{ public: virtual ~StoryStepExecutor() = default; @@ -157,61 +63,14 @@ 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; - } - // 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 - // Outcome. - virtual Outcome run(RunState& state, const StoryStep& step); + // Does `step`, from the run so far. + virtual Outcome run(RunState& state, const StoryStep& step) = 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); - -protected: - // One per step kind, called by dispatch_story_step(); each fails as - // unsupported unless the game overrides it. - friend Outcome dispatch_story_step(StoryStepExecutor& executor, RunState& state, const StoryStep& step); - 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); - -private: - StoryChecks m_checks = StoryChecks::THOROUGH; - Language m_language = Language::English; }; -// 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); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp index e61a1eeb95..a61a345ee2 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -64,41 +65,53 @@ class FakeExecutor : public StoryStepExecutor{ left--; return outcome_failed("blocked", step.name + " was blocked"); } - return StoryStepExecutor::run(state, step); + return run_kind(state, step); } + virtual bool holds(const std::string& condition, RunState& state) override{ return conditions.contains(condition) || StoryStepExecutor::holds(condition, state); } -protected: - virtual Outcome route(RunState&, const StoryStep& step) override{ - return outcome_ok(*step.target); +private: + // The kinds this world has; any other is unsupported. + Outcome run_kind(RunState& state, const StoryStep& step){ + switch (step.kind){ + case StoryStepKind::ROUTE: + return outcome_ok(*step.target); + case StoryStepKind::STEP: + return step_one_tile(state, step); + case StoryStepKind::TALK: + case StoryStepKind::MASH: + return outcome_ok(); + case StoryStepKind::BATTLE: + // The first battle is lost; a step that may lose goes on. + return step.may_lose ? outcome_ok() : outcome_failed("blackout"); + case StoryStepKind::SHOP: + return shop(step); + case StoryStepKind::GAME: + return game(state, step); + default: + return unsupported_step(step); + } } - virtual Outcome step(RunState& state, const StoryStep& step) override{ + + Outcome step_one_tile(RunState& state, const StoryStep& step){ 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{ + + Outcome shop(const StoryStep& step){ for (const StoryItem& item : step.items){ trace.push_back(" bought " + std::to_string(item.quantity) + " " + item.item); } return outcome_ok(); } + // A game step sets a run flag named for its variant. - virtual Outcome game(RunState& state, const StoryStep& step) override{ + Outcome game(RunState& state, const StoryStep& step){ trace.push_back(" game " + step.variant); state.flags.insert(step.variant); return unchanged.contains(step.name) ? outcome_unchanged() : outcome_ok(); @@ -166,7 +179,7 @@ Run run(const StoryCheckpoint& data, StoryStepExecutor& executor){ return ret; } -std::string joined(const std::vector& lines){ +std::string joined(std::span lines){ std::string text; for (const std::string& line : lines){ text += "\n " + line; @@ -300,7 +313,7 @@ UnitTestResult test_unsupported_and_lost_battles(){ // Runs `tables` as one segment whose saves and reloads go into the // executor's trace and whose log lines go into `lines`. Whatever blocked a // step has gone by the reload. -SegmentReport run_tables(const std::vector& tables, FakeExecutor& executor, std::vector& lines){ +SegmentReport run_tables(std::span tables, FakeExecutor& executor, std::vector& lines){ Segment segment; segment.id = "00"; for (const StoryCheckpoint& data : tables){ @@ -577,32 +590,6 @@ UnitTestResult test_once_per_save_verify(){ } -// 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); - 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); - } - return true; -} - // Two sections of a story, as BDSP has segments 00 and 01. const std::vector& two_sections(){ static const std::vector rows{ @@ -612,6 +599,7 @@ const std::vector& two_sections(){ }; return rows; } + const StringSelectDatabase& two_sections_database(){ static const StringSelectDatabase database = story_checkpoint_database(two_sections()); return database; @@ -797,10 +785,6 @@ const StoryStepTestCase STORY_STEP_CASES[] = { .input = {.name = "lint", .test = test_lint}, .expectation = {.description = "verifies lint validation rules on story checkpoints"}, }, - { - .input = {.name = "the checks level reaches the steps", .test = test_checks_level}, - .expectation = {.description = "transfers checks granularity option to step execution"}, - }, { .input = {.name = "the run options' defaults", .test = test_run_options}, .expectation = {.description = "validates initial default story run option values"}, diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h index f136fa147b..79f854bdd6 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h @@ -30,6 +30,7 @@ struct VariantPair{ std::string_view key; std::string_view value; }; + // `word` split at its first '='; nothing without one or with an empty key. std::optional variant_pair(std::string_view word); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h new file mode 100644 index 0000000000..7071035402 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h @@ -0,0 +1,37 @@ +/* Story Checkpoint + * + * From: https://github.com/PokemonAutomation/ + * + * A checkpoint of a tile game's story table as plain data: where its save + * leaves the player and its steps in order. build_story_checkpoint() + * (Pokemon_StorySteps.h) turns it into engine events. + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryCheckpoint_H +#define PokemonAutomation_Pokemon_StoryCheckpoint_H + +#include +#include +#include +#include "Pokemon/AutoStory/Pokemon_StoryEvents.h" +#include "Pokemon_StoryStep.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// Milestone row in the story table grouping related steps and starting pose. +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; +}; + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp new file mode 100644 index 0000000000..1ea9bbb92a --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp @@ -0,0 +1,26 @@ +/* Story Checks + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Pokemon_StoryChecks.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +const char* story_checks_name(StoryChecks checks){ + switch (checks){ + case StoryChecks::THOROUGH: + return "thorough"; + case StoryChecks::FAST: + return "fast"; + } + return "?"; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h new file mode 100644 index 0000000000..3dba180603 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h @@ -0,0 +1,33 @@ +/* Story Checks + * + * From: https://github.com/PokemonAutomation/ + * + * How much a story step reads the screen to confirm what it did. The + * program's options choose it (StoryRunOptions); each game decides what + * its steps check at each level. + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryChecks_H +#define PokemonAutomation_Pokemon_StoryChecks_H + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// How much a step reads the screen to confirm what it did. +enum class StoryChecks{ + // Confirm on screen: purchases, the Bag, status after battles, position. + THOROUGH, + // In a rush: trust counts and timings. + FAST, +}; + +// Returns the readable string representation of a story check level. +const char* story_checks_name(StoryChecks checks); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h new file mode 100644 index 0000000000..457725dc05 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h @@ -0,0 +1,87 @@ +/* Story Step + * + * From: https://github.com/PokemonAutomation/ + * + * One step of a tile game's story (FRLG, BDSP) as plain data: its kind + * and the fields that kind reads. A game's executor does it + * (Pokemon_StorySteps.h); the shared JSON reads and writes it + * (Pokemon_StoryStepJson.h). + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryStep_H +#define PokemonAutomation_Pokemon_StoryStep_H + +#include +#include +#include +#include +#include "Pokemon/AutoStory/Pokemon_StoryEvents.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "Pokemon_StoryStepKind.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// 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; +}; + +// Specification of an atomic story task with actions and postconditions. +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()), run flags + // unless the game reads them off the screen. + std::vector skip_if; + // Preparation for what follows, done once per save: a TEACH, a check, + // a read. Once it has succeeded and changed nothing + // (Outcome::changed_nothing), the attempts after a reload of the same + // save skip it, so a retry goes from the reload straight to the + // battle. One that changed the game (a TM taught) runs again, as the + // reload undid it. Only with build_story_checkpoint()'s PreparedSteps. + bool once_per_save = false; + // 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; +}; + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp new file mode 100644 index 0000000000..8491db4f90 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp @@ -0,0 +1,52 @@ +/* Story Step Kind + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Pokemon_StoryStepKind.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 "?"; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h new file mode 100644 index 0000000000..a62d40e7d6 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h @@ -0,0 +1,43 @@ +/* Story Step Kind + * + * From: https://github.com/PokemonAutomation/ + * + * The kinds of story step the tile games (FRLG, BDSP) share. A game's + * executor does each kind its own way (Pokemon_StorySteps.h). + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryStepKind_H +#define PokemonAutomation_Pokemon_StoryStepKind_H + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// Action category classification for individual story steps. +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` +}; + +// Returns the readable string representation of a story step kind. +const char* story_step_kind_name(StoryStepKind kind); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h index c54482cc52..f362ce875f 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -4,8 +4,9 @@ * * The options every AutoStory program shows for where to start and end, how * hard to try and how much to check: the same group in each game, read into the shared - * SegmentPolicy (Pokemon/AutoStory/Pokemon_StorySegments.h) and the - * executor's checks level (Pokemon_StorySteps.h). + * SegmentPolicy (Pokemon/AutoStory/Pokemon_StorySegments.h), the checks + * level (Types/Pokemon_StoryChecks.h) and the language the game's steps + * read text in. * */ @@ -21,10 +22,12 @@ #include "Common/Cpp/Options/EnumDropdownOption.h" #include "Common/Cpp/Options/GroupOption.h" #include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "CommonFramework/Language.h" #include "CommonTools/Options/StringSelectOption.h" #include "Pokemon/AutoStory/Pokemon_AutoStorySelect.h" #include "Pokemon/AutoStory/Pokemon_StorySegments.h" -#include "Pokemon/AutoStory/Pokemon_StorySteps.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryChecks.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -34,10 +37,12 @@ namespace Pokemon{ // segments: "00.09 Pewter Gym: Camper Liam, then Brock". struct StoryCheckpointEntry{ std::string label; + const std::string& name() const{ return label; } }; + // Formats a checkpoint identifier and description into a dropdown label. std::string story_checkpoint_label(const StoryCheckpoint& checkpoint); @@ -94,9 +99,9 @@ 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()). + // The checks level, for the game's steps to read. StoryChecks checks() const; - // The game's language, for the executor (set_language()). + // The game's language, for the steps that read text. Language language() const; public: diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 69e99c18a7..4630157b77 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1517,6 +1517,12 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.h + Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp + Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h + Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h + Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h + Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp + Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h Source/Pokemon/Pokemon_AdvRng.cpp Source/Pokemon/Pokemon_AdvRng.h Source/Pokemon/Pokemon_BdspRng.cpp From 1b74c8256d2352f60e34375be67c84d93db306fc Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 20:42:44 -0800 Subject: [PATCH 3/8] AutoStory: structured step args next to a plain variant name A step's variant carried its action's arguments as free text ("mart CeruleanCity_Mart budget=1500 ..."), parsed by each action. Steps now carry them in `args` (flags, whole numbers, words), which the shared JSON codec reads and writes; actions declare StoryArgSpecs and a game checks args against them at load. split_legacy_variant() reads an old variant into a name and args during the migration; the variant word helpers are deprecated. Design: docs/design/AutoStory.md section 10. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../ComputerPrograms_UnitTestRunner.cpp | 1 + .../AutoStory/Pokemon_AutoStory_Tests.h | 2 + .../AutoStory/Pokemon_StoryArgs_Tests.cpp | 144 ++++++++++++++++++ .../AutoStory/Pokemon_StoryStepJson.cpp | 34 ++++- .../AutoStory/Pokemon_StoryStepJson_Tests.cpp | 5 + .../AutoStory/Pokemon_StoryVariant.cpp | 26 ++++ .../Pokemon/AutoStory/Pokemon_StoryVariant.h | 23 +++ .../AutoStory/Types/Pokemon_StoryArgs.cpp | 84 ++++++++++ .../AutoStory/Types/Pokemon_StoryArgs.h | 68 +++++++++ .../AutoStory/Types/Pokemon_StoryStep.h | 7 +- SerialPrograms/cmake/SourceFiles.cmake | 3 + devtools/story-data-tests/CMakeLists.txt | 8 +- docs/design/AutoStory.md | 45 ++++++ 13 files changed, 447 insertions(+), 3 deletions(-) create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index df98fc4df2..a47faa19ff 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -82,6 +82,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ Pokemon::add_tests_StorySegments(ret); Pokemon::add_tests_StorySteps(ret); Pokemon::add_tests_StoryStepJson(ret); + Pokemon::add_tests_StoryArgs(ret); Pokemon::add_tests_AuthoredCheckpointJson(ret); NintendoSwitch::add_tests_CheckOnlineDetector(ret); NintendoSwitch::add_tests_FailedToConnectDetector(ret); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h index 0efa649a7c..d48ec292d1 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -23,6 +23,8 @@ void add_tests_StorySegments(UnitTestDatabase& database); void add_tests_StorySteps(UnitTestDatabase& database); // Registers unit tests for the lossless shared story-data wire contract. void add_tests_StoryStepJson(UnitTestDatabase& database); +// Registers unit tests for step args: action specs and reading old variant strings. +void add_tests_StoryArgs(UnitTestDatabase& database); // Registers unit tests for the shared authored envelope, game identity and embedded-resource loading. void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp new file mode 100644 index 0000000000..889639d650 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp @@ -0,0 +1,144 @@ +/* Story Args Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Checking a step's args against an action's specs, and reading an old + * "name key=value" variant into a name and args. Built standalone + * (PA_STORY_DATA_STANDALONE), it runs without the application. + * + */ + +#include +#include +#include +#include +#include "Pokemon_StoryVariant.h" +#include "Types/Pokemon_StoryArgs.h" +#ifndef PA_STORY_DATA_STANDALONE +#include "Pokemon_AutoStory_Tests.h" +#endif + +namespace PokemonAutomation{ +namespace Pokemon{ + + +namespace{ + + +// The args a Mart step takes, as an action declares them. +constexpr StoryArgSpec MART_SPECS[] = { + {.key = "map", .type = StoryArgType::TEXT, .required = true}, + {.key = "budget", .type = StoryArgType::NUMBER, .required = true}, + {.key = "floor", .type = StoryArgType::NUMBER}, + {.key = "if_present", .type = StoryArgType::FLAG}, +}; + +constexpr std::string_view MART_POSITIONAL[] = {"map"}; + + +// An old variant, read with an action's positional keys and checked +// against its specs. +struct ArgsInput{ + std::string_view name; + std::string_view variant; + std::span specs = MART_SPECS; + std::span positional = MART_POSITIONAL; +}; + +// The action name and args it gives, and the first error: the split's, +// else the specs'. +struct ArgsExpectation{ + std::string_view action; + StoryArgs args = {}; + std::string_view error = {}; +}; + +struct ArgsCase{ + ArgsInput input; + ArgsExpectation expectation; +}; + +const ArgsCase CASES[] = { + {.input = {.name = "mart", .variant = "mart CeruleanCity_Mart budget=1500 floor=1500 if_present"}, + .expectation = {.action = "mart", .args = {{"map", std::string("CeruleanCity_Mart")}, {"budget", int64_t(1500)}, {"floor", int64_t(1500)}, {"if_present", true}}}}, + {.input = {.name = "plain name", .variant = "run", .specs = {}, .positional = {}}, .expectation = {.action = "run"}}, + {.input = {.name = "words stay words", .variant = "teach tutor forget=tail-whip", .specs = {}, .positional = {}}, + .expectation = {.action = "teach", .args = {{"tutor", true}, {"forget", std::string("tail-whip")}}, .error = "unknown argument \"forget\""}}, + {.input = {.name = "unknown key", .variant = "mart M budget=1 budgte=1"}, + .expectation = {.action = "mart", .args = {{"map", std::string("M")}, {"budget", int64_t(1)}, {"budgte", int64_t(1)}}, .error = "unknown argument \"budgte\""}}, + {.input = {.name = "wrong type", .variant = "mart M budget=lots"}, + .expectation = {.action = "mart", .args = {{"map", std::string("M")}, {"budget", std::string("lots")}}, .error = "argument \"budget\" has the wrong type"}}, + {.input = {.name = "missing required", .variant = "mart M"}, + .expectation = {.action = "mart", .args = {{"map", std::string("M")}}, .error = "missing argument \"budget\""}}, + {.input = {.name = "repeated key", .variant = "mart M budget=1 budget=2"}, + .expectation = {.action = "mart", .args = {{"map", std::string("M")}, {"budget", int64_t(1)}}, .error = "\"budget=2\" repeats an argument"}}, +}; + + +// "" when `test` reads as expected, else what differs. +std::string check_case(const ArgsCase& test){ + const LegacyVariant read = split_legacy_variant(test.input.variant, test.input.positional); + const std::string error = read.error.empty() ? story_args_error(read.args, test.input.specs) : read.error; + if (read.name != test.expectation.action || read.args != test.expectation.args){ + return "read \"" + read.name + "\" with other args"; + } + if (error != test.expectation.error){ + return "expected error \"" + std::string(test.expectation.error) + "\", got \"" + error + "\""; + } + return ""; +} + + +#ifndef PA_STORY_DATA_STANDALONE + +class StoryArgsTest : public UnitTest{ +public: + explicit StoryArgsTest(ArgsCase test) + : UnitTest("Pokemon::StoryArgs - " + std::string(test.input.name)) + , m_case(test) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const std::string error = check_case(m_case); + return error.empty() ? UnitTestResult(true) : UnitTestResult(error); + } + +private: + ArgsCase m_case; +}; + +#endif + + +} // namespace + + +#ifndef PA_STORY_DATA_STANDALONE + +void add_tests_StoryArgs(UnitTestDatabase& database){ + for (const ArgsCase& test : CASES){ + database.add(test); + } +} + +#endif + + + +} // namespace Pokemon +} // namespace PokemonAutomation + + +#ifdef PA_STORY_DATA_STANDALONE + +int main(){ + int failed = 0; + for (const auto& test : PokemonAutomation::Pokemon::CASES){ + const std::string error = PokemonAutomation::Pokemon::check_case(test); + std::cout << test.input.name << ": " << (error.empty() ? "PASS" : error) << '\n'; + failed += !error.empty(); + } + return failed != 0; +} + +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp index 78427f9667..a1483a3505 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonArray.h" #include "Common/Cpp/Json/JsonObject.h" @@ -177,6 +178,31 @@ std::vector read_items(const JsonValue& value, std::string_view sourc return result; } +JsonObject args_json(const StoryArgs& args){ + JsonObject result; + for (const auto& [key, value] : args){ + std::visit([&](const auto& held){ result[key] = held; }, value); + } + return result; +} + +// A step's args: flags, whole numbers and words; any other value fails. +StoryArgs read_args(const JsonValue& value, std::string_view source){ + StoryArgs result; + for (const auto& [key, item] : value.to_object_throw(std::string(source))){ + if (item.is_boolean()){ + result[key] = item.to_boolean_throw(); + }else if (item.is_integer()){ + result[key] = item.to_integer_throw(); + }else if (item.is_string()){ + result[key] = item.to_string_throw(); + }else{ + throw ParseException(std::format("{}: argument {} is not a flag, whole number or word", source, key)); + } + } + return result; +} + // `result` with the step's yes/no and count fields read from `root`. StoryStep read_step_options(StoryStep result, const JsonObject& root, std::string_view source){ if (const JsonValue* field = root.get_value("fight_wild")){ @@ -292,6 +318,9 @@ JsonValue story_step_json(const StoryStep& step){ if (!step.variant.empty()){ result["variant"] = step.variant; } + if (!step.args.empty()){ + result["args"] = args_json(step.args); + } if (!step.skip_if.empty()){ result["skip_if"] = strings_json(step.skip_if); } @@ -303,7 +332,7 @@ JsonValue story_step_json(const StoryStep& step){ StoryStep read_story_step(const JsonValue& value, std::string_view source){ const JsonObject& root = fields(value, {"id", "kind", "why", "target", "direction", "legs", "fight_wild", "yes", - "may_lose", "battles", "items", "variant", "skip_if", "declared_after"}, source); + "may_lose", "battles", "items", "variant", "args", "skip_if", "declared_after"}, source); StoryStep result; result.name = root.get_string_throw("id"); if (result.name.empty()){ @@ -329,6 +358,9 @@ StoryStep read_story_step(const JsonValue& value, std::string_view source){ if (const JsonValue* field = root.get_value("variant")){ result.variant = field->to_string_throw(); } + if (const JsonValue* field = root.get_value("args")){ + result.args = read_args(*field, source); + } if (const JsonValue* field = root.get_value("skip_if")){ result.skip_if = read_strings(*field, source); } diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp index 269b9f4412..60178eae1f 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp @@ -57,6 +57,11 @@ const WireCase CASES[] = { {.input = {.name = "unknown facing stays absent", .record = Record::POSE, .json = R"({"map":"M","certainty":"tile","where":{"kind":"tiles","tiles":[[0,0]]}})"}, .expectation = {.canonical = R"({"map":"M","certainty":"tile","where":{"kind":"tiles","tiles":[[0,0]]}})"}}, {.input = {.name = "empty position differs from no position", .record = Record::POSE, .json = R"({"map":"M","certainty":"unknown","where":{"kind":"tiles","tiles":[]}})"}, .expectation = {.canonical = R"({"map":"M","certainty":"unknown","where":{"kind":"tiles","tiles":[]}})"}}, {.input = {.name = "ordered checkpoint", .record = Record::CHECKPOINT, .json = R"({"id":"00.05","title":"Parcel","events":[{"id":"first","kind":"route"},{"id":"second","kind":"scene"}]})"}, .expectation = {.canonical = R"({"id":"00.05","title":"Parcel","events":[{"id":"first","kind":"route"},{"id":"second","kind":"scene"}]})"}}, + {.input = {.name = "args", .json = R"({"id":"buy","kind":"game_action","variant":"mart","args":{"map":"CeruleanCity_Mart","budget":3000,"if_present":true}})"}, .expectation = {.canonical = R"({"id":"buy","kind":"game_action","variant":"mart","args":{"budget":3000,"if_present":true,"map":"CeruleanCity_Mart"}})"}}, + {.input = {.name = "empty args are omitted", .json = R"({"id":"buy","kind":"game_action","variant":"mart","args":{}})"}, .expectation = {.canonical = R"({"id":"buy","kind":"game_action","variant":"mart"})"}}, + {.input = {.name = "fractional arg", .json = R"({"id":"a","kind":"game_action","variant":"mart","args":{"budget":1.5}})"}, .expectation = {.rejected = true}}, + {.input = {.name = "nested arg", .json = R"({"id":"a","kind":"game_action","variant":"mart","args":{"items":[1]}})"}, .expectation = {.rejected = true}}, + {.input = {.name = "args not an object", .json = R"({"id":"a","kind":"game_action","variant":"mart","args":["map"]})"}, .expectation = {.rejected = true}}, {.input = {.name = "unknown field", .json = R"({"id":"a","kind":"talk","mystery":0})"}, .expectation = {.rejected = true}}, {.input = {.name = "unknown action", .json = R"({"id":"a","kind":"magic"})"}, .expectation = {.rejected = true}}, {.input = {.name = "negative count", .json = R"({"id":"a","kind":"train","battles":-1})"}, .expectation = {.rejected = true}}, diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp index 6df0e9df31..a10163f584 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp @@ -66,6 +66,32 @@ std::optional variant_number(std::string_view text, size_t max_digits){ return value; } +LegacyVariant split_legacy_variant(std::string_view variant, std::span positional){ + LegacyVariant result; + const std::vector words = variant_words(variant); + result.name = variant_name(variant); + size_t next = 0; + for (size_t c = result.name.empty() ? 0 : 1; c < words.size(); c++){ + std::string key; + StoryArgValue value = true; + if (const std::optional pair = variant_pair(words[c])){ + key = pair->key; + const std::optional number = variant_number(pair->value, 18); + value = number ? StoryArgValue(int64_t(*number)) : StoryArgValue(std::string(pair->value)); + }else if (next < positional.size()){ + key = positional[next++]; + value = std::string(words[c]); + }else{ + key = words[c]; + } + if (!result.args.emplace(std::move(key), std::move(value)).second){ + result.error = "\"" + std::string(words[c]) + "\" repeats an argument"; + return result; + } + } + return result; +} + } // namespace Pokemon diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h index 79f854bdd6..bd8c02343a 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h @@ -8,6 +8,11 @@ * picks the step's action (Pokemon_StoryActions.h) and the action reads * the rest. * + * Deprecated: a step's variant is now a plain action name and its + * arguments are structured (StoryStep::args, Types/Pokemon_StoryArgs.h). + * split_legacy_variant() reads an old variant into that form while plans + * and callers move over; the word helpers go once none reads them. + * */ #ifndef PokemonAutomation_Pokemon_StoryVariant_H @@ -15,8 +20,11 @@ #include #include +#include +#include #include #include +#include "Types/Pokemon_StoryArgs.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -46,6 +54,21 @@ std::optional variant_value(std::string_view variant, std::str std::optional variant_number(std::string_view text, size_t max_digits = 6); +// An old variant as an action name and its args, or why it can't be read. +struct LegacyVariant{ + std::string name; + StoryArgs args; + std::string error; +}; + +// `variant` written the old way ("mart CeruleanCity_Mart budget=3000 +// if_present"): its first word is the name; the next plain words take the +// keys in `positional`, in order, and plain words after those are flags; +// "key=value" words are numbers when all digits, else words. It reads no +// action's specs: check the args against them (story_args_error()). +LegacyVariant split_legacy_variant(std::string_view variant, std::span positional = {}); + + } // namespace Pokemon } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp new file mode 100644 index 0000000000..c4da5548fd --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp @@ -0,0 +1,84 @@ +/* Story Args + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Pokemon_StoryArgs.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +namespace{ + + +template +const Type* find_arg(const StoryArgs& args, std::string_view key){ + const auto iter = args.find(key); + return iter == args.end() ? nullptr : std::get_if(&iter->second); +} + +bool has_type(const StoryArgValue& value, StoryArgType type){ + switch (type){ + case StoryArgType::FLAG: + return std::holds_alternative(value); + case StoryArgType::NUMBER: + return std::holds_alternative(value); + case StoryArgType::TEXT: + return std::holds_alternative(value); + } + return false; +} + +const StoryArgSpec* find_spec(std::span specs, std::string_view key){ + for (const StoryArgSpec& spec : specs){ + if (spec.key == key){ + return &spec; + } + } + return nullptr; +} + + +} // namespace + + +std::optional story_arg_number(const StoryArgs& args, std::string_view key){ + const int64_t* value = find_arg(args, key); + return value ? std::optional(*value) : std::nullopt; +} + +std::optional story_arg_text(const StoryArgs& args, std::string_view key){ + const std::string* value = find_arg(args, key); + return value ? std::optional(*value) : std::nullopt; +} + +bool story_arg_flag(const StoryArgs& args, std::string_view key){ + const bool* value = find_arg(args, key); + return value != nullptr && *value; +} + +std::string story_args_error(const StoryArgs& args, std::span specs){ + for (const auto& [key, value] : args){ + const StoryArgSpec* spec = find_spec(specs, key); + if (spec == nullptr){ + return std::format("unknown argument \"{}\"", key); + } + if (!has_type(value, spec->type)){ + return std::format("argument \"{}\" has the wrong type", key); + } + } + for (const StoryArgSpec& spec : specs){ + if (spec.required && args.find(spec.key) == args.end()){ + return std::format("missing argument \"{}\"", spec.key); + } + } + return ""; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h new file mode 100644 index 0000000000..96214deed7 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h @@ -0,0 +1,68 @@ +/* Story Args + * + * From: https://github.com/PokemonAutomation/ + * + * A step's arguments (StoryStep::args): named flags, whole numbers and + * words that its action reads, e.g. a Mart step's {"map": + * "CeruleanCity_Mart", "budget": 3000}. Each action declares the ones it + * takes (StoryArgSpec); a game checks a step's args against them when it + * loads the step, so a typo fails at load instead of mid-run. + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryArgs_H +#define PokemonAutomation_Pokemon_StoryArgs_H + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// One argument's value: a flag, a whole number or a word. +using StoryArgValue = std::variant; + +// A step's arguments by name. +using StoryArgs = std::map>; + + +// The number `key` holds; nothing if absent or not a number. +std::optional story_arg_number(const StoryArgs& args, std::string_view key); + +// The word `key` holds; nothing if absent or not a word. +std::optional story_arg_text(const StoryArgs& args, std::string_view key); + +// Whether flag `key` is set; absent is unset. +bool story_arg_flag(const StoryArgs& args, std::string_view key); + + +// The type an action declares for an argument. +enum class StoryArgType{ + FLAG, + NUMBER, + TEXT, +}; + +// An argument an action takes. +struct StoryArgSpec{ + std::string_view key; + StoryArgType type = StoryArgType::TEXT; + bool required = false; +}; + +// Why `args` don't fit `specs`: a key no spec names, a value of another +// type, or a required one missing. Empty when they fit. +std::string story_args_error(const StoryArgs& args, std::span specs); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h index 457725dc05..8255c60e3f 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h @@ -18,6 +18,7 @@ #include #include "Pokemon/AutoStory/Pokemon_StoryEvents.h" #include "Pokemon/Navigation/Pokemon_Grid.h" +#include "Pokemon_StoryArgs.h" #include "Pokemon_StoryStepKind.h" namespace PokemonAutomation{ @@ -61,8 +62,12 @@ struct StoryStep{ // 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. + // in the game's executor. A plain name; what the action needs beyond + // it goes in `args`. std::string variant; + // The arguments the variant's action reads (a Mart's map and budget), + // checked against the ones it declares when the game loads the step. + StoryArgs args; // Skipped when all of these hold (and there is at least one): names // the game's executor answers (StoryStepExecutor::holds()), run flags diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 4630157b77..736bed62e3 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1512,11 +1512,14 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp + Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp Source/Pokemon/AutoStory/Pokemon_StorySteps.h Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.h + Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp + Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h diff --git a/devtools/story-data-tests/CMakeLists.txt b/devtools/story-data-tests/CMakeLists.txt index 978db4f6d0..78d07af69e 100644 --- a/devtools/story-data-tests/CMakeLists.txt +++ b/devtools/story-data-tests/CMakeLists.txt @@ -11,6 +11,7 @@ add_executable(StoryDataTests "${STORY}/Pokemon_StoryStepJson.cpp" "${STORY}/Pokemon_StoryStepJson_Tests.cpp" "${STORY}/Pokemon_TilePose.cpp" + "${STORY}/Types/Pokemon_StoryArgs.cpp" "${ROOT}/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Grid.cpp" "${ROOT}/Common/Cpp/Exceptions.cpp" "${ROOT}/Common/Cpp/Json/JsonValue.cpp" @@ -26,8 +27,12 @@ list(REMOVE_ITEM STORY_DATA_SOURCES "${STORY}/Pokemon_StoryStepJson_Tests.cpp") add_executable(AuthoredCheckpointTests ${STORY_DATA_SOURCES} "${STORY}/Pokemon_AuthoredCheckpointJson.cpp" "${STORY}/Pokemon_AuthoredCheckpointJson_Tests.cpp") +add_executable(StoryArgsTests + "${STORY}/Types/Pokemon_StoryArgs.cpp" + "${STORY}/Pokemon_StoryVariant.cpp" + "${STORY}/Pokemon_StoryArgs_Tests.cpp") -foreach(TEST_TARGET IN ITEMS StoryDataTests AuthoredCheckpointTests) +foreach(TEST_TARGET IN ITEMS StoryDataTests AuthoredCheckpointTests StoryArgsTests) target_include_directories(${TEST_TARGET} PRIVATE "${ROOT}" "${ROOT}/SerialPrograms/Source") target_compile_features(${TEST_TARGET} PRIVATE cxx_std_23) target_compile_definitions(${TEST_TARGET} PRIVATE PA_STORY_DATA_STANDALONE) @@ -41,3 +46,4 @@ endif() endforeach() add_test(NAME StoryData COMMAND StoryDataTests) add_test(NAME AuthoredCheckpoint COMMAND AuthoredCheckpointTests) +add_test(NAME StoryArgs COMMAND StoryArgsTests) diff --git a/docs/design/AutoStory.md b/docs/design/AutoStory.md index d14601e42e..4e5d1eb607 100644 --- a/docs/design/AutoStory.md +++ b/docs/design/AutoStory.md @@ -885,3 +885,48 @@ among the agents is recorded outside this doc. trade-back, or a levelled Pokémon brought in from another save. Optional trades the game itself offers (e.g. the Vermilion Farfetch'd) are allowed only behind an option. + +## 10. Step arguments (user, 2026-11-09) + +A story step's `variant` used to carry its action's arguments as free text, +for example `"mart CeruleanCity_Mart budget=1500 floor=1500 potion=3 +awakening=2"`. Each action parsed its own words, so a typo or a missing +argument showed up only when the step ran, and the Python reader in +PokemonSimulation had to copy every action's parser to read a plan. + +Now `variant` is only the action's name, the key it is registered under in a +game's action table. Its arguments move to `args`, a JSON object on the step +whose values are flags, whole numbers or words (`StoryStep::args`, +`Types/Pokemon_StoryArgs.h`). Each action declares the arguments it takes, +with a type and whether each one is required (`StoryArgSpec`). A game checks +every step's `args` against those specs when it loads the plan, so an +unknown key, a word where a number belongs or a missing budget fails at +load. Items to buy stay in the step's `items` list and are not repeated in +`args`. The shared JSON codec reads and writes `args` without knowing any +action, and omits it when it is empty. + +During the migration, a plan may still carry an old variant string. +`split_legacy_variant()` turns it into a name and args: the leading plain +words take the action's positional keys (a Mart's `map`), `key=value` +words become numbers when they are all digits and words otherwise, and any +remaining plain word becomes a flag (`if_present`). The word helpers in +`Pokemon_StoryVariant.h` are deprecated and will be removed once no action +reads them. + +The actions that take arguments today, all FRLG (BDSP's are listed for a +later change): + +| Action | Arguments | Notes | +|---|---|---| +| `mart` | `map` word, required; `budget` number, required; `floor` number | What to buy goes in `items`. | +| `teach` | `item` word (`tm03`, `hm01`, `tutor`), `move` word, `slot` number, all required; `forget` word | | +| `pc` | `op` word (`deposit`), `slot` number, `box` number, all required; `if_present` flag | | +| `grind`, `grind_check` | `slot`, `level`, `encounters` numbers and `pace` word, required; `heal_every`, `chance` numbers; `beat` word | `grind_check` takes the same arguments. | +| `capture` | `species` word, `catches`, `encounters`, `balls` numbers, all required; `pace` word | | +| `level` | `slot`, `min` numbers, required | Built in C++, not in a plan. | +| `fossil` | `fossil` word (`helix`, `dome`), required | | +| `use_item` | `item` word, required; `slot` number | 00.30 onwards. | +| `lift_floor` | `from`, `to` words, required | 00.30 onwards. | + +Plain names such as `run`, `trainer_line`, `cut_tree` and `bike` take no +arguments and stay as they are. From dd9529b51b6052b8080bd2871b44f1daa595e084 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 20:45:06 -0800 Subject: [PATCH 4/8] AutoStory: shared story range options and story log lines StoryRangeOptions is the start/end checkpoint dropdowns over one list with the start-before-end check; StoryRunOptions is now that group plus the run policy, with the same keys and public members. Pokemon_StoryLog holds the checkpoint start/attempt/pass/reset lines, event_log_line and a stats counts helper, with no engine dependency; run_checkpoint, run_segment and record_story_stats use it, with unchanged output. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../ComputerPrograms_UnitTestRunner.cpp | 2 + .../AutoStory/Pokemon_AutoStory_Tests.h | 4 + .../Pokemon/AutoStory/Pokemon_StoryEvents.cpp | 7 +- .../Pokemon/AutoStory/Pokemon_StoryEvents.h | 6 +- .../Pokemon/AutoStory/Pokemon_StoryLog.cpp | 75 ++++++++ .../Pokemon/AutoStory/Pokemon_StoryLog.h | 76 ++++++++ .../AutoStory/Pokemon_StoryLog_Tests.cpp | 173 ++++++++++++++++++ .../AutoStory/Pokemon_StorySegments.cpp | 19 +- .../Pokemon/AutoStory/Pokemon_StorySteps.cpp | 7 +- .../Options/Pokemon_StoryRangeOptions.cpp | 51 ++++++ .../Options/Pokemon_StoryRangeOptions.h | 48 +++++ .../Pokemon_StoryRangeOptions_Tests.cpp | 134 ++++++++++++++ .../Options/Pokemon_StoryRunOptions.cpp | 31 +--- .../Pokemon/Options/Pokemon_StoryRunOptions.h | 17 +- SerialPrograms/cmake/SourceFiles.cmake | 6 + 15 files changed, 591 insertions(+), 65 deletions(-) create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h create mode 100644 SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index df98fc4df2..266e64a2f3 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -83,6 +83,8 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ Pokemon::add_tests_StorySteps(ret); Pokemon::add_tests_StoryStepJson(ret); Pokemon::add_tests_AuthoredCheckpointJson(ret); + Pokemon::add_tests_StoryLog(ret); + Pokemon::add_tests_StoryRangeOptions(ret); NintendoSwitch::add_tests_CheckOnlineDetector(ret); NintendoSwitch::add_tests_FailedToConnectDetector(ret); NintendoSwitch::add_tests_UpdatePopupDetector(ret); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h index 0efa649a7c..9f975f6b39 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -25,6 +25,10 @@ void add_tests_StorySteps(UnitTestDatabase& database); void add_tests_StoryStepJson(UnitTestDatabase& database); // Registers unit tests for the shared authored envelope, game identity and embedded-resource loading. void add_tests_AuthoredCheckpointJson(UnitTestDatabase& database); +// Registers unit tests for the shared checkpoint log lines and stats counts. +void add_tests_StoryLog(UnitTestDatabase& database); +// Registers unit tests for the shared start/end checkpoint range options. +void add_tests_StoryRangeOptions(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp index 35c61a688c..f9a87c6778 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp @@ -113,11 +113,6 @@ const char* recovery_name(RecoveryKind kind){ } -std::string event_log_line(const std::string& checkpoint, size_t number, const std::string& event, const std::string& status){ - return std::format("[Event] {}/{} {}: {}", checkpoint, number, event, status); -} - - namespace{ // Runs an action; if it succeeded knowing where it left the player, that @@ -410,7 +405,7 @@ Outcome finish_checkpoint(RunState& state, Checkpoint& checkpoint){ CheckpointReport run_checkpoint(RunState& state, Checkpoint& checkpoint, EventLogger& log){ CheckpointReport report; const std::string& id = checkpoint.id; - log(std::format("[Checkpoint] {}: start, {} events", id, checkpoint.events.size())); + log(checkpoint_start_line(id, checkpoint.events.size())); std::optional failing = first_failing(checkpoint.start, state); if (failing){ return stop_checkpoint(report, log, id, outcome_failed("start_failed", *failing)); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h index 388ce48b03..582518b9c3 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h @@ -34,6 +34,7 @@ #include #include #include "Common/Cpp/MoveOnlyFunction.h" +#include "Pokemon_StoryLog.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -237,11 +238,6 @@ struct CheckpointReport{ using EventLogger = MoveOnlyFunction; -// One log line, in a format the lab watcher parses: -// [Event] 00.04/3 talk_mom: start -// [Event] 00.04/3 talk_mom: failed: expect_failed: has_parcel -std::string event_log_line(const std::string& checkpoint, size_t number, const std::string& event, const std::string& status); - // Problems an author should fix before running: unnamed or empty events, // missing actions, recoveries out of escalation order. std::vector lint_checkpoint(const Checkpoint& checkpoint); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp new file mode 100644 index 0000000000..8b5b3e0dca --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp @@ -0,0 +1,75 @@ +/* Story Log + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Pokemon_StoryLog.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +namespace{ + +std::string resets_text(size_t attempt){ + if (attempt <= 1){ + return "one-shot"; + } + return std::format("{} reset{}", attempt - 1, attempt == 2 ? "" : "s"); +} + +} // namespace + + +std::string event_log_line(std::string_view checkpoint, size_t number, std::string_view event, std::string_view status){ + return std::format("[Event] {}/{} {}: {}", checkpoint, number, event, status); +} + +std::string checkpoint_start_line(std::string_view checkpoint){ + return std::format("[Checkpoint] {}: start", checkpoint); +} + +std::string checkpoint_start_line(std::string_view checkpoint, size_t events){ + return std::format("[Checkpoint] {}: start, {} events", checkpoint, events); +} + +std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt){ + return std::format("[Checkpoint] {}: attempt {}", checkpoint, attempt); +} + +std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt, size_t attempts){ + return std::format("[Checkpoint] {}: attempt {}/{}", checkpoint, attempt, attempts); +} + +std::string checkpoint_pass_line(std::string_view checkpoint, size_t attempt, size_t in_place){ + return std::format( + "[Checkpoint] {}: passed on attempt {} ({}){}", + checkpoint, attempt, resets_text(attempt), + in_place == 0 ? "" : std::format(", {} in place", in_place) + ); +} + +std::string checkpoint_reset_line(std::string_view checkpoint, size_t next_attempt){ + return std::format("[Checkpoint] {}: failed, resetting for attempt {}", checkpoint, next_attempt); +} + +std::string checkpoint_reset_line(std::string_view segment, std::string_view checkpoint, size_t next_attempt, size_t attempts){ + return std::format( + "[Segment] {}: {} failed, reloading for attempt {}/{}", + segment, checkpoint, next_attempt, attempts + ); +} + + +void record_story_counts(AutoStoryStats& stats, const StoryRunCounts& counts){ + stats.m_segment += counts.segments; + stats.m_checkpoint += counts.checkpoints; + stats.m_reset += counts.resets; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h new file mode 100644 index 0000000000..5b5df714a9 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h @@ -0,0 +1,76 @@ +/* Story Log + * + * From: https://github.com/PokemonAutomation/ + * + * The log lines an AutoStory program writes around its checkpoints, in + * one format for every game: the story engine (Pokemon_StoryEvents.h, + * Pokemon_StorySegments.h) writes them, and a game with its own checkpoint + * loop (SV's) writes the same lines, so the lab watcher and whoever reads + * a log find each checkpoint's start, attempts, resets and pass the same + * way. Also the program's stats counts. Nothing here runs a story. + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryLog_H +#define PokemonAutomation_Pokemon_StoryLog_H + +#include +#include +#include +#include "Pokemon_AutoStoryStats.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// One event's line: +// [Event] 00.04/3 talk_mom: start +// [Event] 00.04/3 talk_mom: failed: expect_failed: has_parcel +std::string event_log_line(std::string_view checkpoint, size_t number, std::string_view event, std::string_view status); + +// A checkpoint starts: +// [Checkpoint] 00.04: start +std::string checkpoint_start_line(std::string_view checkpoint); +// The story engine starts a checkpoint of `events` events: +// [Checkpoint] 00.04: start, 3 events +std::string checkpoint_start_line(std::string_view checkpoint, size_t events); + +// An attempt at a checkpoint starts, counting from 1: +// [Checkpoint] 00.04: attempt 2 +std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt); +// The same, out of the program's limit of `attempts`: +// [Checkpoint] 00.04: attempt 2/3 +std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt, size_t attempts); + +// A checkpoint passed on attempt `attempt`, after `in_place` recoveries in +// place: +// [Checkpoint] 00.03: passed on attempt 1 (one-shot) +// [Checkpoint] 00.04: passed on attempt 3 (2 resets), 1 in place +std::string checkpoint_pass_line(std::string_view checkpoint, size_t attempt, size_t in_place = 0); + +// An attempt failed and the game resets to the checkpoint's save for +// attempt `next_attempt`: +// [Checkpoint] 00.04: failed, resetting for attempt 2 +std::string checkpoint_reset_line(std::string_view checkpoint, size_t next_attempt); +// The story engine reloads within segment `segment`, out of `attempts`: +// [Segment] 00: 00.02 failed, reloading for attempt 2/3 +std::string checkpoint_reset_line(std::string_view segment, std::string_view checkpoint, size_t next_attempt, size_t attempts); + + +// What a story run adds to the program's stats. +struct StoryRunCounts{ + size_t segments = 0; + // Checkpoints passed (and saved at). + size_t checkpoints = 0; + // Game resets or reloads after failed attempts. + size_t resets = 0; +}; + +// Adds `counts` to the shared AutoStory stats. +void record_story_counts(AutoStoryStats& stats, const StoryRunCounts& counts); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp new file mode 100644 index 0000000000..7737d2cb41 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp @@ -0,0 +1,173 @@ +/* Story Log Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon_AutoStory_Tests.h" +#include "Pokemon_StoryLog.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + +namespace{ + +// A formatter's line, made when the table is built. +struct StoryLogInput{ + std::string name; + std::string line; +}; + +// The exact line: the lab watcher and the engine tests read these. +struct StoryLogExpectation{ + std::string line; +}; + +// One formatter call and the line it must make. +struct StoryLogCase{ + StoryLogInput input; + StoryLogExpectation expectation; +}; + +class Test_StoryLog : public UnitTest{ +public: + Test_StoryLog(StoryLogCase test_case) + : UnitTest("Pokemon::StoryLog - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + if (m_case.input.line != m_case.expectation.line){ + return "Expected \"" + m_case.expectation.line + "\", got \"" + m_case.input.line + "\""; + } + return true; + } + +private: + const StoryLogCase m_case; +}; + +const StoryLogCase STORY_LOG_CASES[] = { + { + .input = {.name = "event", .line = event_log_line("00.04", 3, "talk_mom", "start")}, + .expectation = {.line = "[Event] 00.04/3 talk_mom: start"}, + }, + { + .input = {.name = "checkpoint start", .line = checkpoint_start_line("00.04")}, + .expectation = {.line = "[Checkpoint] 00.04: start"}, + }, + { + .input = {.name = "checkpoint start with its events", .line = checkpoint_start_line("00.04", 3)}, + .expectation = {.line = "[Checkpoint] 00.04: start, 3 events"}, + }, + { + .input = {.name = "attempt", .line = checkpoint_attempt_line("00.04", 2)}, + .expectation = {.line = "[Checkpoint] 00.04: attempt 2"}, + }, + { + .input = {.name = "attempt out of a limit", .line = checkpoint_attempt_line("00.04", 2, 3)}, + .expectation = {.line = "[Checkpoint] 00.04: attempt 2/3"}, + }, + { + .input = {.name = "one-shot pass", .line = checkpoint_pass_line("00.03", 1)}, + .expectation = {.line = "[Checkpoint] 00.03: passed on attempt 1 (one-shot)"}, + }, + { + .input = {.name = "pass after one reset", .line = checkpoint_pass_line("00.04", 2)}, + .expectation = {.line = "[Checkpoint] 00.04: passed on attempt 2 (1 reset)"}, + }, + { + .input = {.name = "pass after resets and recoveries in place", .line = checkpoint_pass_line("00.04", 3, 1)}, + .expectation = {.line = "[Checkpoint] 00.04: passed on attempt 3 (2 resets), 1 in place"}, + }, + { + .input = {.name = "reset", .line = checkpoint_reset_line("00.04", 2)}, + .expectation = {.line = "[Checkpoint] 00.04: failed, resetting for attempt 2"}, + }, + { + .input = {.name = "reload within a segment", .line = checkpoint_reset_line("00", "00.02", 2, 3)}, + .expectation = {.line = "[Segment] 00: 00.02 failed, reloading for attempt 2/3"}, + }, +}; + + +// Counts added to fresh stats, in order. +struct StoryCountsInput{ + std::string name; + std::vector added; +}; + +// The stats after them: counts add up rather than replace. +struct StoryCountsExpectation{ + uint64_t segments; + uint64_t checkpoints; + uint64_t resets; +}; + +// Counts recorded into the AutoStory stats and the totals they must reach. +struct StoryCountsCase{ + StoryCountsInput input; + StoryCountsExpectation expectation; +}; + +class Test_StoryCounts : public UnitTest{ +public: + Test_StoryCounts(StoryCountsCase test_case) + : UnitTest("Pokemon::StoryLog - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + AutoStoryStats stats; + for (const StoryRunCounts& counts : m_case.input.added){ + record_story_counts(stats, counts); + } + const StoryCountsExpectation& expected = m_case.expectation; + if (stats.m_segment != expected.segments || stats.m_checkpoint != expected.checkpoints || stats.m_reset != expected.resets){ + return "Expected " + std::to_string(expected.segments) + " segments, " + std::to_string(expected.checkpoints) + + " checkpoints and " + std::to_string(expected.resets) + " resets, got " + + std::to_string(stats.m_segment) + ", " + std::to_string(stats.m_checkpoint) + + " and " + std::to_string(stats.m_reset); + } + return true; + } + +private: + const StoryCountsCase m_case; +}; + +const StoryCountsCase STORY_COUNTS_CASES[] = { + { + .input = {.name = "a segment's counts in the stats", .added = {{.segments = 1, .checkpoints = 2, .resets = 3}}}, + .expectation = {.segments = 1, .checkpoints = 2, .resets = 3}, + }, + { + .input = { + .name = "a checkpoint loop's counts add up", + .added = {{.segments = 0, .checkpoints = 1, .resets = 0}, {.segments = 0, .checkpoints = 0, .resets = 1}, {.segments = 0, .checkpoints = 1, .resets = 0}}, + }, + .expectation = {.segments = 0, .checkpoints = 2, .resets = 1}, + }, +}; + +} // namespace + + +void add_tests_StoryLog(UnitTestDatabase& database){ + for (const StoryLogCase& test_case : STORY_LOG_CASES){ + database.add(test_case); + } + for (const StoryCountsCase& test_case : STORY_COUNTS_CASES){ + database.add(test_case); + } +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp index 5653725467..358f606dc6 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp @@ -6,6 +6,7 @@ #include #include +#include "Pokemon_StoryLog.h" #include "Pokemon_StorySegments.h" namespace PokemonAutomation{ @@ -121,13 +122,6 @@ void add_policy_recoveries(Checkpoint& checkpoint, const SegmentPolicy& policy, } } -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; `run` says // how many attempts and recoveries it took. @@ -148,11 +142,7 @@ CheckpointReport run_row( CheckpointReport result = run_checkpoint(state, checkpoint, log); if (result.completed){ run.passed = true; - say(hooks, std::format( - "[Checkpoint] {}: passed on attempt {} ({}){}", - spec.id, attempt, resets_text(attempt), - run.recoveries == 0 ? "" : std::format(", {} in place", run.recoveries) - )); + say(hooks, checkpoint_pass_line(spec.id, attempt, run.recoveries)); return result; } const bool not_running = result.failure.failure == GAME_NOT_RUNNING; @@ -167,10 +157,7 @@ CheckpointReport run_row( if (attempt >= policy.attempts_per_checkpoint){ return result; } - say(hooks, std::format( - "[Segment] {}: {} failed, reloading for attempt {}/{}", - segment.id, spec.id, attempt + 1, policy.attempts_per_checkpoint - )); + say(hooks, checkpoint_reset_line(segment.id, spec.id, attempt + 1, policy.attempts_per_checkpoint)); report.reloads++; Outcome reloaded = reload(state, hooks); if (!reloaded.succeeded()){ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp index 5309b3e496..07cf478f66 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp @@ -8,6 +8,7 @@ #include #include #include +#include "Pokemon_StoryLog.h" #include "Pokemon_StorySteps.h" namespace PokemonAutomation{ @@ -162,13 +163,13 @@ std::vector lint_story_checkpoint(const StoryCheckpoint& data){ void record_story_stats(AutoStoryStats& stats, const SegmentReport& report){ - stats.m_segment++; + StoryRunCounts counts{.segments = 1, .checkpoints = 0, .resets = report.reloads}; for (const CheckpointRun& run : report.runs){ if (run.passed){ - stats.m_checkpoint++; + counts.checkpoints++; } } - stats.m_reset += report.reloads; + record_story_counts(stats, counts); } diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp new file mode 100644 index 0000000000..8526f48574 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp @@ -0,0 +1,51 @@ +/* Story Range Options + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Pokemon_StoryRangeOptions.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +StoryRangeOptions::StoryRangeOptions(std::string label, const StringSelectDatabase& checkpoints) + : GroupOption(std::move(label), LockMode::LOCK_WHILE_RUNNING) + , START_CHECKPOINT( + "Start checkpoint:
The program starts from this checkpoint's save.", + checkpoints, + LockMode::LOCK_WHILE_RUNNING, + 0 + ) + , END_CHECKPOINT( + "End checkpoint:
The program stops after this checkpoint.", + checkpoints, + LockMode::LOCK_WHILE_RUNNING, + checkpoints.size() == 0 ? 0 : checkpoints.size() - 1 + ) +{ + PA_ADD_OPTION(START_CHECKPOINT); + PA_ADD_OPTION(END_CHECKPOINT); +} + +size_t StoryRangeOptions::start_index() const{ + return START_CHECKPOINT.index(); +} + +size_t StoryRangeOptions::end_index() const{ + return END_CHECKPOINT.index(); +} + +std::string StoryRangeOptions::check_validity() const{ + if (start_index() > end_index()){ + return "The start checkpoint comes after the end checkpoint."; + } + return GroupOption::check_validity(); +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h new file mode 100644 index 0000000000..26afe4cafd --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h @@ -0,0 +1,48 @@ +/* Story Range Options + * + * From: https://github.com/PokemonAutomation/ + * + * The start and end checkpoint dropdowns of an AutoStory program, over one + * list of the story's checkpoints, and the check that the start does not + * come after the end. StoryRunOptions (Pokemon_StoryRunOptions.h) is this + * group plus the run policy. + * + */ + +#ifndef PokemonAutomation_Pokemon_StoryRangeOptions_H +#define PokemonAutomation_Pokemon_StoryRangeOptions_H + +#include +#include "Common/Cpp/Options/GroupOption.h" +#include "CommonTools/Options/StringSelectOption.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// A program shows the group, or adds START_CHECKPOINT and END_CHECKPOINT +// to its own options under its own keys (keeping the keys its saved +// settings already use) and calls check_validity() from its own. +class StoryRangeOptions : public GroupOption{ +public: + // `checkpoints`: the dropdown database (make_story_select_database() + // or story_checkpoint_database()), kept alive by the caller. The + // dropdowns start on its first and last entries. + StoryRangeOptions(std::string label, const StringSelectDatabase& checkpoints); + + // The checkpoints chosen, by index in the database, both inclusive. + size_t start_index() const; + size_t end_index() const; + // "The start checkpoint comes after the end checkpoint." if it does. + virtual std::string check_validity() const override; + +public: + StringSelectOption START_CHECKPOINT; + StringSelectOption END_CHECKPOINT; +}; + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp new file mode 100644 index 0000000000..1126620526 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp @@ -0,0 +1,134 @@ +/* Story Range Options Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/AutoStory/Pokemon_AutoStorySelect.h" +#include "Pokemon/AutoStory/Pokemon_AutoStory_Tests.h" +#include "Pokemon_StoryRangeOptions.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + +namespace{ + +// A stand-in for a story checkpoint: only its name is read. +struct NamedCheckpoint{ + std::string label; + std::string name() const{ + return label; + } +}; + +// Two tutorial checkpoints, then three of the main story, as SV's list is +// laid out. +const std::vector>& story_list(){ + static const std::vector> list = []{ + std::vector> ret; + for (const char* label : {"tutorial 0", "tutorial 1", "main 0", "main 1", "main 2"}){ + ret.emplace_back(std::make_unique(NamedCheckpoint{.label = label})); + } + return ret; + }(); + return list; +} + +// The main story's dropdowns: entries 2..4 of the list, slugged 2..4. +const StringSelectDatabase& main_story_database(){ + static const StringSelectDatabase database = make_story_select_database(story_list(), 2, story_list().size()); + return database; +} + +// The start and end set by slug, as saved settings load them; empty +// leaves the default. +struct StoryRangeInput{ + std::string name; + std::string start_slug; + std::string end_slug; +}; + +// The indexes in the dropdown, and whether the range is valid. +struct StoryRangeExpectation{ + size_t start; + size_t end; + bool valid; +}; + +// A range chosen in the main story's dropdowns and what the group reads from it. +struct StoryRangeCase{ + StoryRangeInput input; + StoryRangeExpectation expectation; +}; + +class Test_StoryRange : public UnitTest{ +public: + Test_StoryRange(StoryRangeCase test_case) + : UnitTest("Pokemon::StoryRangeOptions - " + test_case.input.name) + , m_case(std::move(test_case)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + StoryRangeOptions range("Checkpoints", main_story_database()); + if (!m_case.input.start_slug.empty()){ + range.START_CHECKPOINT.set_by_slug(m_case.input.start_slug); + } + if (!m_case.input.end_slug.empty()){ + range.END_CHECKPOINT.set_by_slug(m_case.input.end_slug); + } + const StoryRangeExpectation& expected = m_case.expectation; + if (range.start_index() != expected.start || range.end_index() != expected.end){ + return "Expected indexes " + std::to_string(expected.start) + " to " + std::to_string(expected.end) + + ", got " + std::to_string(range.start_index()) + " to " + std::to_string(range.end_index()); + } + const std::string error = range.check_validity(); + if (error.empty() != expected.valid){ + return expected.valid ? "Expected a valid range, got: " + error : "Expected the range refused."; + } + if (!expected.valid && error != "The start checkpoint comes after the end checkpoint."){ + return "Unexpected message: " + error; + } + return true; + } + +private: + const StoryRangeCase m_case; +}; + +const StoryRangeCase STORY_RANGE_CASES[] = { + { + .input = {.name = "the whole database by default", .start_slug = "", .end_slug = ""}, + .expectation = {.start = 0, .end = 2, .valid = true}, + }, + { + .input = {.name = "slugs are whole-list indexes, indexes are the dropdown's", .start_slug = "3", .end_slug = "4"}, + .expectation = {.start = 1, .end = 2, .valid = true}, + }, + { + .input = {.name = "one checkpoint", .start_slug = "3", .end_slug = "3"}, + .expectation = {.start = 1, .end = 1, .valid = true}, + }, + { + .input = {.name = "a start after the end is refused", .start_slug = "4", .end_slug = "2"}, + .expectation = {.start = 2, .end = 0, .valid = false}, + }, +}; + +} // namespace + + +void add_tests_StoryRangeOptions(UnitTestDatabase& database){ + for (const StoryRangeCase& test_case : STORY_RANGE_CASES){ + database.add(test_case); + } +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp index 5ce0fb7ab0..9f8da731a0 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp @@ -76,19 +76,7 @@ std::string StoryVerifyOptions::check_validity() const{ StoryRunOptions::StoryRunOptions(const StringSelectDatabase& checkpoints) - : GroupOption("Story Run", LockMode::LOCK_WHILE_RUNNING) - , START_CHECKPOINT( - "Start checkpoint:
The program starts from this checkpoint's save.", - checkpoints, - LockMode::LOCK_WHILE_RUNNING, - 0 - ) - , END_CHECKPOINT( - "End checkpoint:
The program stops after this checkpoint.", - checkpoints, - LockMode::LOCK_WHILE_RUNNING, - checkpoints.size() == 0 ? 0 : checkpoints.size() - 1 - ) + : StoryRangeOptions("Story Run", checkpoints) , 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, @@ -113,8 +101,6 @@ StoryRunOptions::StoryRunOptions(const StringSelectDatabase& checkpoints) Language::English ) { - PA_ADD_OPTION(START_CHECKPOINT); - PA_ADD_OPTION(END_CHECKPOINT); PA_ADD_OPTION(ATTEMPTS_PER_CHECKPOINT); PA_ADD_OPTION(STOP_AT_FIRST_FAILURE); PA_ADD_OPTION(CHECKS); @@ -122,22 +108,15 @@ StoryRunOptions::StoryRunOptions(const StringSelectDatabase& checkpoints) PA_ADD_OPTION(VERIFY); } -size_t StoryRunOptions::start_index() const{ - return START_CHECKPOINT.index(); -} - -size_t StoryRunOptions::end_index() const{ - return END_CHECKPOINT.index(); -} - std::string StoryRunOptions::check_validity() const{ - if (start_index() > end_index()){ - return "The start checkpoint comes after the end checkpoint."; + std::string error = StoryRangeOptions::check_validity(); + if (!error.empty()){ + return error; } if (VERIFY.enabled() && start_index() != end_index()){ return "Verify mode runs one checkpoint: set the end checkpoint to the start checkpoint."; } - return GroupOption::check_validity(); + return error; } SegmentPolicy StoryRunOptions::policy() const{ diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h index f362ce875f..e166c57fd2 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -3,7 +3,8 @@ * From: https://github.com/PokemonAutomation/ * * The options every AutoStory program shows for where to start and end, how - * hard to try and how much to check: the same group in each game, read into the shared + * hard to try and how much to check: the shared range group + * (Pokemon_StoryRangeOptions.h) plus the run policy, read into the shared * SegmentPolicy (Pokemon/AutoStory/Pokemon_StorySegments.h), the checks * level (Types/Pokemon_StoryChecks.h) and the language the game's steps * read text in. @@ -28,6 +29,7 @@ #include "Pokemon/AutoStory/Pokemon_StorySegments.h" #include "Pokemon/AutoStory/Types/Pokemon_StoryChecks.h" #include "Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h" +#include "Pokemon_StoryRangeOptions.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -83,17 +85,16 @@ class StoryVerifyOptions : public GroupOption{ }; -// Configuration options panel for selecting story checkpoints and execution mode. -class StoryRunOptions : public GroupOption{ +// The range group (START_CHECKPOINT, END_CHECKPOINT, start_index(), +// end_index()) with the run policy added to the same group, so the saved +// settings keep one flat object. +class StoryRunOptions : public StoryRangeOptions{ public: // `checkpoints`: story_checkpoint_database() of the game's story, kept // alive by the caller; the dropdowns start on its first and last. explicit StoryRunOptions(const StringSelectDatabase& checkpoints); - // The checkpoints to run, by index in the database, both inclusive. - size_t start_index() const; - size_t end_index() const; - // "The start checkpoint comes after the end checkpoint." if it does. + // The range's check, then verify mode's: one checkpoint only. virtual std::string check_validity() const override; // The policy these options set. In-place recoveries are the game's to @@ -105,8 +106,6 @@ class StoryRunOptions : public GroupOption{ Language language() const; public: - StringSelectOption START_CHECKPOINT; - StringSelectOption END_CHECKPOINT; SimpleIntegerOption ATTEMPTS_PER_CHECKPOINT; BooleanCheckBoxOption STOP_AT_FIRST_FAILURE; EnumDropdownOption CHECKS; diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 4630157b77..3c8cc0ad01 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1491,6 +1491,9 @@ 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_StoryRangeOptions.cpp + Source/Pokemon/Options/Pokemon_StoryRangeOptions.h + Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp Source/Pokemon/Options/Pokemon_StoryRunOptions.h Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h @@ -1501,6 +1504,9 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp Source/Pokemon/AutoStory/Pokemon_StoryEvents.h Source/Pokemon/AutoStory/Pokemon_StoryEvents_Tests.cpp + Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp + Source/Pokemon/AutoStory/Pokemon_StoryLog.h + Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp Source/Pokemon/AutoStory/Pokemon_StorySegments.h Source/Pokemon/AutoStory/Pokemon_StorySegments_Tests.cpp From 48c10b75c67e1e1c297fb8fdf70fa7cf16e2a134 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 20:49:18 -0800 Subject: [PATCH 5/8] StoryRangeOptions: let a program unlock the end while running SV reads its end checkpoint after each checkpoint and leaves the end dropdown unlocked during a run; the range group now takes that lock mode. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp | 4 ++-- .../Source/Pokemon/Options/Pokemon_StoryRangeOptions.h | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp index 8526f48574..d294e5e386 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.cpp @@ -11,7 +11,7 @@ namespace PokemonAutomation{ namespace Pokemon{ -StoryRangeOptions::StoryRangeOptions(std::string label, const StringSelectDatabase& checkpoints) +StoryRangeOptions::StoryRangeOptions(std::string label, const StringSelectDatabase& checkpoints, LockMode end_lock) : GroupOption(std::move(label), LockMode::LOCK_WHILE_RUNNING) , START_CHECKPOINT( "Start checkpoint:
The program starts from this checkpoint's save.", @@ -22,7 +22,7 @@ StoryRangeOptions::StoryRangeOptions(std::string label, const StringSelectDataba , END_CHECKPOINT( "End checkpoint:
The program stops after this checkpoint.", checkpoints, - LockMode::LOCK_WHILE_RUNNING, + end_lock, checkpoints.size() == 0 ? 0 : checkpoints.size() - 1 ) { diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h index 26afe4cafd..809f3324d2 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions.h @@ -27,8 +27,13 @@ class StoryRangeOptions : public GroupOption{ public: // `checkpoints`: the dropdown database (make_story_select_database() // or story_checkpoint_database()), kept alive by the caller. The - // dropdowns start on its first and last entries. - StoryRangeOptions(std::string label, const StringSelectDatabase& checkpoints); + // dropdowns start on its first and last entries. `end_lock`: + // UNLOCK_WHILE_RUNNING for a program that reads the end after each + // checkpoint, so the end can be moved during a run. + StoryRangeOptions( + std::string label, const StringSelectDatabase& checkpoints, + LockMode end_lock = LockMode::LOCK_WHILE_RUNNING + ); // The checkpoints chosen, by index in the database, both inclusive. size_t start_index() const; From c766e871c3e213620f86d4e34d5a8015315ed2c8 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 20:55:12 -0800 Subject: [PATCH 6/8] AutoStory tests: register the story engine suites through one call Keeps the unit test runner's list short as the stacked layers add suites. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../ComputerPrograms_UnitTestRunner.cpp | 9 +-------- .../Pokemon/AutoStory/Pokemon_AutoStory_Tests.h | 12 ++++++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index 9f629691a4..dfbabc0d0d 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -78,14 +78,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ Pokemon::add_tests_MapData(ret); Pokemon::add_tests_RuntimeMap(ret); Pokemon::add_tests_RouteLog(ret); - Pokemon::add_tests_StoryEvents(ret); - Pokemon::add_tests_StorySegments(ret); - Pokemon::add_tests_StorySteps(ret); - Pokemon::add_tests_StoryStepJson(ret); - Pokemon::add_tests_StoryArgs(ret); - Pokemon::add_tests_AuthoredCheckpointJson(ret); - Pokemon::add_tests_StoryLog(ret); - Pokemon::add_tests_StoryRangeOptions(ret); + Pokemon::add_tests_AutoStory(ret); NintendoSwitch::add_tests_CheckOnlineDetector(ret); NintendoSwitch::add_tests_FailedToConnectDetector(ret); NintendoSwitch::add_tests_UpdatePopupDetector(ret); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h index 0c3e2218da..24af2d8db9 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -32,6 +32,18 @@ void add_tests_StoryLog(UnitTestDatabase& database); // Registers unit tests for the shared start/end checkpoint range options. void add_tests_StoryRangeOptions(UnitTestDatabase& database); +// Registers every suite above, for the unit test runner. +inline void add_tests_AutoStory(UnitTestDatabase& database){ + add_tests_StoryEvents(database); + add_tests_StorySegments(database); + add_tests_StorySteps(database); + add_tests_StoryStepJson(database); + add_tests_StoryArgs(database); + add_tests_AuthoredCheckpointJson(database); + add_tests_StoryLog(database); + add_tests_StoryRangeOptions(database); +} + } // namespace Pokemon From a940ce00623a145a3bfce39d70c96122f9aae18e Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 22:35:44 -0800 Subject: [PATCH 7/8] AutoStory: move the JSON codecs to Data/, StorySteps to StoryStepExecutor, StoryChecks to StoryCheckLevel File moves and path updates only (#312 review): the story-step and authored-checkpoint JSON modules are I/O, so they go under Pokemon/AutoStory/Data/; Pokemon_StorySteps is named for what it holds, the StoryStepExecutor; Types/Pokemon_StoryChecks becomes Pokemon_StoryCheckLevel ahead of the type's rename. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Pokemon_AuthoredCheckpointJson.cpp | 0 .../Pokemon_AuthoredCheckpointJson.h | 2 +- .../Pokemon_AuthoredCheckpointJson_Tests.cpp | 2 +- .../{ => Data}/Pokemon_StoryStepJson.cpp | 0 .../{ => Data}/Pokemon_StoryStepJson.h | 4 ++-- .../Pokemon_StoryStepJson_Tests.cpp | 2 +- .../AutoStory/Pokemon_AutoStory_Tests.h | 4 ++-- ...teps.cpp => Pokemon_StoryStepExecutor.cpp} | 2 +- ...orySteps.h => Pokemon_StoryStepExecutor.h} | 6 ++--- ...pp => Pokemon_StoryStepExecutor_Tests.cpp} | 12 +++++----- ...Checks.cpp => Pokemon_StoryCheckLevel.cpp} | 2 +- ...toryChecks.h => Pokemon_StoryCheckLevel.h} | 4 ++-- .../AutoStory/Types/Pokemon_StoryCheckpoint.h | 2 +- .../AutoStory/Types/Pokemon_StoryStep.h | 4 ++-- .../AutoStory/Types/Pokemon_StoryStepKind.h | 2 +- .../Pokemon/Options/Pokemon_StoryRunOptions.h | 4 ++-- SerialPrograms/cmake/SourceFiles.cmake | 24 +++++++++---------- devtools/story-data-tests/CMakeLists.txt | 10 ++++---- 18 files changed, 43 insertions(+), 43 deletions(-) rename SerialPrograms/Source/Pokemon/AutoStory/{ => Data}/Pokemon_AuthoredCheckpointJson.cpp (100%) rename SerialPrograms/Source/Pokemon/AutoStory/{ => Data}/Pokemon_AuthoredCheckpointJson.h (94%) rename SerialPrograms/Source/Pokemon/AutoStory/{ => Data}/Pokemon_AuthoredCheckpointJson_Tests.cpp (97%) rename SerialPrograms/Source/Pokemon/AutoStory/{ => Data}/Pokemon_StoryStepJson.cpp (100%) rename SerialPrograms/Source/Pokemon/AutoStory/{ => Data}/Pokemon_StoryStepJson.h (90%) rename SerialPrograms/Source/Pokemon/AutoStory/{ => Data}/Pokemon_StoryStepJson_Tests.cpp (97%) rename SerialPrograms/Source/Pokemon/AutoStory/{Pokemon_StorySteps.cpp => Pokemon_StoryStepExecutor.cpp} (96%) rename SerialPrograms/Source/Pokemon/AutoStory/{Pokemon_StorySteps.h => Pokemon_StoryStepExecutor.h} (94%) rename SerialPrograms/Source/Pokemon/AutoStory/{Pokemon_StorySteps_Tests.cpp => Pokemon_StoryStepExecutor_Tests.cpp} (96%) rename SerialPrograms/Source/Pokemon/AutoStory/Types/{Pokemon_StoryChecks.cpp => Pokemon_StoryCheckLevel.cpp} (86%) rename SerialPrograms/Source/Pokemon/AutoStory/Types/{Pokemon_StoryChecks.h => Pokemon_StoryCheckLevel.h} (84%) diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.cpp similarity index 100% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp rename to SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.cpp diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h similarity index 94% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h rename to SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h index 5bcbc181a2..fd9116eb03 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h @@ -13,7 +13,7 @@ #include #include #include "Common/Cpp/Json/JsonObject.h" -#include "Types/Pokemon_StoryCheckpoint.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h" namespace PokemonAutomation{ namespace Pokemon{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson_Tests.cpp similarity index 97% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp rename to SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson_Tests.cpp index d01a4fe633..346d31505a 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson_Tests.cpp @@ -12,7 +12,7 @@ #include "Pokemon_AuthoredCheckpointJson.h" #include "Pokemon_StoryStepJson.h" #ifndef PA_STORY_DATA_STANDALONE -#include "Pokemon_AutoStory_Tests.h" +#include "Pokemon/AutoStory/Pokemon_AutoStory_Tests.h" #endif namespace PokemonAutomation{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.cpp similarity index 100% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp rename to SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.cpp diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.h similarity index 90% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h rename to SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.h index b50caadc58..e02b3508d0 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.h @@ -13,8 +13,8 @@ #include #include "Common/Cpp/Json/JsonValue.h" #include "Pokemon/AutoStory/Pokemon_StoryEvents.h" -#include "Types/Pokemon_StoryCheckpoint.h" -#include "Types/Pokemon_StoryStep.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryStep.h" namespace PokemonAutomation{ namespace Pokemon{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp similarity index 97% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp rename to SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp index 60178eae1f..f2ca6e9b0b 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp @@ -16,7 +16,7 @@ #include "Common/Cpp/Json/JsonArray.h" #include "Pokemon_StoryStepJson.h" #ifndef PA_STORY_DATA_STANDALONE -#include "Pokemon_AutoStory_Tests.h" +#include "Pokemon/AutoStory/Pokemon_AutoStory_Tests.h" #endif namespace PokemonAutomation{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h index 24af2d8db9..0379857071 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -20,7 +20,7 @@ void add_tests_StoryEvents(UnitTestDatabase& database); // Registers unit tests for story segment running, verification, and branching. void add_tests_StorySegments(UnitTestDatabase& database); // Registers unit tests for high-level story step dispatching and table conversion. -void add_tests_StorySteps(UnitTestDatabase& database); +void add_tests_StoryStepExecutor(UnitTestDatabase& database); // Registers unit tests for the lossless shared story-data wire contract. void add_tests_StoryStepJson(UnitTestDatabase& database); // Registers unit tests for step args: action specs and reading old variant strings. @@ -36,7 +36,7 @@ void add_tests_StoryRangeOptions(UnitTestDatabase& database); inline void add_tests_AutoStory(UnitTestDatabase& database){ add_tests_StoryEvents(database); add_tests_StorySegments(database); - add_tests_StorySteps(database); + add_tests_StoryStepExecutor(database); add_tests_StoryStepJson(database); add_tests_StoryArgs(database); add_tests_AuthoredCheckpointJson(database); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp similarity index 96% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp rename to SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp index 07cf478f66..49736227b5 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp @@ -9,7 +9,7 @@ #include #include #include "Pokemon_StoryLog.h" -#include "Pokemon_StorySteps.h" +#include "Pokemon_StoryStepExecutor.h" namespace PokemonAutomation{ namespace Pokemon{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h similarity index 94% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h rename to SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h index 7b76155d63..f718276ea1 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h @@ -29,8 +29,8 @@ * */ -#ifndef PokemonAutomation_Pokemon_StorySteps_H -#define PokemonAutomation_Pokemon_StorySteps_H +#ifndef PokemonAutomation_Pokemon_StoryStepExecutor_H +#define PokemonAutomation_Pokemon_StoryStepExecutor_H #include #include @@ -41,7 +41,7 @@ #include "Pokemon_StoryEvents.h" #include "Pokemon_StorySegments.h" #include "Pokemon_TilePose.h" -#include "Types/Pokemon_StoryChecks.h" +#include "Types/Pokemon_StoryCheckLevel.h" #include "Types/Pokemon_StoryCheckpoint.h" #include "Types/Pokemon_StoryStep.h" #include "Types/Pokemon_StoryStepKind.h" diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp similarity index 96% rename from SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp rename to SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp index a61a345ee2..e65e9c2375 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp @@ -22,7 +22,7 @@ #include "Pokemon/Options/Pokemon_StoryRunOptions.h" #include "Pokemon_StoryEvents.h" #include "Pokemon_StorySegments.h" -#include "Pokemon_StorySteps.h" +#include "Pokemon_StoryStepExecutor.h" #include "Pokemon_TilePose.h" #include "Pokemon_AutoStory_Tests.h" @@ -742,10 +742,10 @@ struct StoryStepTestCase{ StoryStepExpectation expectation; }; -class Test_StorySteps : public UnitTest{ +class Test_StoryStepExecutor : public UnitTest{ public: - explicit Test_StorySteps(StoryStepTestCase scenario) - : UnitTest("Pokemon::StorySteps - " + scenario.input.name) + explicit Test_StoryStepExecutor(StoryStepTestCase scenario) + : UnitTest("Pokemon::StoryStepExecutor - " + scenario.input.name) , m_scenario(std::move(scenario)) {} virtual UnitTestResult run(Logger&, CancellableScope&) const override{ @@ -826,9 +826,9 @@ const StoryStepTestCase STORY_STEP_CASES[] = { } // namespace -void add_tests_StorySteps(UnitTestDatabase& database){ +void add_tests_StoryStepExecutor(UnitTestDatabase& database){ for (const auto& scenario : STORY_STEP_CASES){ - database.add(scenario); + database.add(scenario); } } diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp similarity index 86% rename from SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp rename to SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp index 1ea9bbb92a..a381e58c37 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp @@ -4,7 +4,7 @@ * */ -#include "Pokemon_StoryChecks.h" +#include "Pokemon_StoryCheckLevel.h" namespace PokemonAutomation{ namespace Pokemon{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h similarity index 84% rename from SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h rename to SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h index 3dba180603..337df1d541 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h @@ -8,8 +8,8 @@ * */ -#ifndef PokemonAutomation_Pokemon_StoryChecks_H -#define PokemonAutomation_Pokemon_StoryChecks_H +#ifndef PokemonAutomation_Pokemon_StoryCheckLevel_H +#define PokemonAutomation_Pokemon_StoryCheckLevel_H namespace PokemonAutomation{ namespace Pokemon{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h index 7071035402..ca72d4ee47 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h @@ -4,7 +4,7 @@ * * A checkpoint of a tile game's story table as plain data: where its save * leaves the player and its steps in order. build_story_checkpoint() - * (Pokemon_StorySteps.h) turns it into engine events. + * (Pokemon_StoryStepExecutor.h) turns it into engine events. * */ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h index 8255c60e3f..ef59d1e8a8 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h @@ -4,8 +4,8 @@ * * One step of a tile game's story (FRLG, BDSP) as plain data: its kind * and the fields that kind reads. A game's executor does it - * (Pokemon_StorySteps.h); the shared JSON reads and writes it - * (Pokemon_StoryStepJson.h). + * (Pokemon_StoryStepExecutor.h); the shared JSON reads and writes it + * (Data/Pokemon_StoryStepJson.h). * */ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h index a62d40e7d6..2bc8bb948c 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h @@ -3,7 +3,7 @@ * From: https://github.com/PokemonAutomation/ * * The kinds of story step the tile games (FRLG, BDSP) share. A game's - * executor does each kind its own way (Pokemon_StorySteps.h). + * executor does each kind its own way (Pokemon_StoryStepExecutor.h). * */ diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h index e166c57fd2..5a63cfbe95 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -6,7 +6,7 @@ * hard to try and how much to check: the shared range group * (Pokemon_StoryRangeOptions.h) plus the run policy, read into the shared * SegmentPolicy (Pokemon/AutoStory/Pokemon_StorySegments.h), the checks - * level (Types/Pokemon_StoryChecks.h) and the language the game's steps + * level (Types/Pokemon_StoryCheckLevel.h) and the language the game's steps * read text in. * */ @@ -27,7 +27,7 @@ #include "CommonTools/Options/StringSelectOption.h" #include "Pokemon/AutoStory/Pokemon_AutoStorySelect.h" #include "Pokemon/AutoStory/Pokemon_StorySegments.h" -#include "Pokemon/AutoStory/Types/Pokemon_StoryChecks.h" +#include "Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h" #include "Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h" #include "Pokemon_StoryRangeOptions.h" diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 894a73724c..87a5ee7896 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1496,11 +1496,18 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp Source/Pokemon/Options/Pokemon_StoryRunOptions.h + Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.cpp + Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h + Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson_Tests.cpp + Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.cpp + Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.h + Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.cpp Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h Source/Pokemon/AutoStory/Pokemon_AutoStoryStats.h Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h + Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp Source/Pokemon/AutoStory/Pokemon_StoryEvents.h Source/Pokemon/AutoStory/Pokemon_StoryEvents_Tests.cpp @@ -1510,24 +1517,17 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp Source/Pokemon/AutoStory/Pokemon_StorySegments.h Source/Pokemon/AutoStory/Pokemon_StorySegments_Tests.cpp - Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.cpp - Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h - Source/Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson_Tests.cpp + Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp + Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h + Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp Source/Pokemon/AutoStory/Pokemon_StoryVariant.cpp Source/Pokemon/AutoStory/Pokemon_StoryVariant.h - Source/Pokemon/AutoStory/Pokemon_StoryStepJson.cpp - Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h - Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp - Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp - Source/Pokemon/AutoStory/Pokemon_StorySteps.cpp - Source/Pokemon/AutoStory/Pokemon_StorySteps.h - Source/Pokemon/AutoStory/Pokemon_StorySteps_Tests.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.cpp Source/Pokemon/AutoStory/Pokemon_TilePose.h Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h - Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.cpp - Source/Pokemon/AutoStory/Types/Pokemon_StoryChecks.h + Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp + Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h Source/Pokemon/AutoStory/Types/Pokemon_StoryStep.h Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp diff --git a/devtools/story-data-tests/CMakeLists.txt b/devtools/story-data-tests/CMakeLists.txt index 78d07af69e..84271bd814 100644 --- a/devtools/story-data-tests/CMakeLists.txt +++ b/devtools/story-data-tests/CMakeLists.txt @@ -8,8 +8,8 @@ enable_testing() get_filename_component(ROOT "../.." ABSOLUTE) set(STORY "${ROOT}/SerialPrograms/Source/Pokemon/AutoStory") add_executable(StoryDataTests - "${STORY}/Pokemon_StoryStepJson.cpp" - "${STORY}/Pokemon_StoryStepJson_Tests.cpp" + "${STORY}/Data/Pokemon_StoryStepJson.cpp" + "${STORY}/Data/Pokemon_StoryStepJson_Tests.cpp" "${STORY}/Pokemon_TilePose.cpp" "${STORY}/Types/Pokemon_StoryArgs.cpp" "${ROOT}/SerialPrograms/Source/Pokemon/Navigation/Pokemon_Grid.cpp" @@ -23,10 +23,10 @@ add_executable(StoryDataTests "${ROOT}/Common/Cpp/Strings/Unicode.cpp") # Reuse the Qt/controller-free support sources; each codec has its own test main. get_target_property(STORY_DATA_SOURCES StoryDataTests SOURCES) -list(REMOVE_ITEM STORY_DATA_SOURCES "${STORY}/Pokemon_StoryStepJson_Tests.cpp") +list(REMOVE_ITEM STORY_DATA_SOURCES "${STORY}/Data/Pokemon_StoryStepJson_Tests.cpp") add_executable(AuthoredCheckpointTests ${STORY_DATA_SOURCES} - "${STORY}/Pokemon_AuthoredCheckpointJson.cpp" - "${STORY}/Pokemon_AuthoredCheckpointJson_Tests.cpp") + "${STORY}/Data/Pokemon_AuthoredCheckpointJson.cpp" + "${STORY}/Data/Pokemon_AuthoredCheckpointJson_Tests.cpp") add_executable(StoryArgsTests "${STORY}/Types/Pokemon_StoryArgs.cpp" "${STORY}/Pokemon_StoryVariant.cpp" From e8833b3450ac74f512cf5c0881869f30b1cf94e1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 22:42:02 -0800 Subject: [PATCH 8/8] AutoStory: review fixes to the story engine's API (#312) - StoryStepExecutor.h keeps the executor and building checkpoints: tile_pose() and facing_of() are in Pokemon_TilePose (merged from #114), record_story_stats() becomes SegmentReport::story_counts() for record_story_counts(), and build_story_checkpoint() takes an optional PreparedSteps* instead of a second overload. - StoryLog: default arguments instead of overloads; the segment's reload line is segment_reload_line(). - StoryChecks is StoryCheckLevel; names come back as std::string, and GAME_NOT_RUNNING is a std::string_view constant: no char* in new code. - StoryArgType::FLAG is BOOLEAN; story_args_errors() returns every error. - Authored checkpoint JSON: validate_json_fields(), `game`, and an EmbeddedJsonFile alias for the embedded resources. - StoryRunOptions takes the game's languages from the new Pokemon_GameLanguages (generations 8-9, and FireRed/LeafGreen), and the checkpoint dropdown is built from a span of checkpoints without the StoryCheckpointEntry adapter. - Read-only strings and sequences as std::string_view and std::span. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../Data/Pokemon_AuthoredCheckpointJson.cpp | 14 ++-- .../Data/Pokemon_AuthoredCheckpointJson.h | 22 +++++-- .../AutoStory/Data/Pokemon_StoryStepJson.cpp | 2 +- .../Data/Pokemon_StoryStepJson_Tests.cpp | 2 +- .../AutoStory/Pokemon_StoryArgs_Tests.cpp | 15 +++-- .../Pokemon/AutoStory/Pokemon_StoryEvents.cpp | 2 +- .../Pokemon/AutoStory/Pokemon_StoryEvents.h | 3 +- .../Pokemon/AutoStory/Pokemon_StoryLog.cpp | 24 ++++--- .../Pokemon/AutoStory/Pokemon_StoryLog.h | 18 ++--- .../AutoStory/Pokemon_StoryLog_Tests.cpp | 2 +- .../AutoStory/Pokemon_StorySegments.cpp | 12 +++- .../Pokemon/AutoStory/Pokemon_StorySegments.h | 4 ++ .../AutoStory/Pokemon_StoryStepExecutor.cpp | 65 ++++--------------- .../AutoStory/Pokemon_StoryStepExecutor.h | 43 ++++-------- .../Pokemon_StoryStepExecutor_Tests.cpp | 32 ++++----- .../Pokemon/AutoStory/Pokemon_StoryVariant.h | 2 +- .../AutoStory/Types/Pokemon_StoryArgs.cpp | 18 ++--- .../AutoStory/Types/Pokemon_StoryArgs.h | 9 +-- .../Types/Pokemon_StoryCheckLevel.cpp | 8 +-- .../AutoStory/Types/Pokemon_StoryCheckLevel.h | 8 ++- .../AutoStory/Types/Pokemon_StoryStepKind.cpp | 2 +- .../AutoStory/Types/Pokemon_StoryStepKind.h | 4 +- .../Pokemon/Options/Pokemon_GameLanguages.cpp | 43 ++++++++++++ .../Pokemon/Options/Pokemon_GameLanguages.h | 35 ++++++++++ .../Pokemon_StoryRangeOptions_Tests.cpp | 5 +- .../Options/Pokemon_StoryRunOptions.cpp | 41 +++++------- .../Pokemon/Options/Pokemon_StoryRunOptions.h | 44 ++++--------- SerialPrograms/cmake/SourceFiles.cmake | 2 + 28 files changed, 259 insertions(+), 222 deletions(-) create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.cpp create mode 100644 SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.h diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.cpp index 63cce48b7a..c22ec33f98 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.cpp @@ -14,7 +14,7 @@ namespace PokemonAutomation{ namespace Pokemon{ -void require_story_json_fields( +void validate_json_fields( const JsonObject& object, std::initializer_list fields, std::string_view source ){ if (object.size() != fields.size()){ @@ -26,12 +26,12 @@ void require_story_json_fields( } AuthoredCheckpoint read_authored_checkpoint( - const JsonValue& value, std::string_view expected_game, std::string_view source + const JsonValue& value, std::string_view game, std::string_view source ){ const JsonObject& root = value.to_object_throw(std::string(source)); - require_story_json_fields(root, {"format", "game", "checkpoint", "execution"}, source); - if (expected_game.empty() || root.get_string_throw("format") != "pokesim-authored-checkpoint/1" - || root.get_string_throw("game") != expected_game + validate_json_fields(root, {"format", "game", "checkpoint", "execution"}, source); + if (game.empty() || root.get_string_throw("format") != "pokesim-authored-checkpoint/1" + || root.get_string_throw("game") != game ){ throw ParseException(std::format("{}: unsupported authored checkpoint format/game.", source)); } @@ -54,11 +54,11 @@ JsonValue authored_checkpoint_json(const StoryCheckpoint& checkpoint, std::strin } AuthoredCheckpoint load_authored_checkpoint( - std::span> files, std::string_view file, std::string_view expected_game + std::span files, std::string_view file, std::string_view game ){ for (const auto& [name, text] : files){ if (name == file){ - return read_authored_checkpoint(parse_json(text), expected_game, file); + return read_authored_checkpoint(parse_json(text), game, file); } } throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Authored checkpoint is not embedded: " + std::string(file)); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h index fd9116eb03..68fa923cae 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_AuthoredCheckpointJson.h @@ -2,6 +2,11 @@ * * From: https://github.com/PokemonAutomation/ * + * Reading and writing an authored checkpoint file: the shared + * StoryCheckpoint (Pokemon_StoryStepJson.h) in an envelope that names the + * game, plus the game's own execution object, left as JSON for the game + * to read. Only parsing and encoding: nothing here binds or runs a step. + * */ #ifndef PokemonAutomation_AuthoredCheckpointJson_H @@ -28,24 +33,29 @@ struct AuthoredCheckpoint{ // Require exactly these fields, including fields whose value is null/zero/false. // Missing/unknown fields throw ParseException with the document source. -void require_story_json_fields( +void validate_json_fields( const JsonObject& object, std::initializer_list fields, std::string_view source ); -// Read the strict authored envelope and common checkpoint for expected_game. +// Read the strict authored envelope and common checkpoint of `game`. // The game adapter must still validate execution fields and values before use. AuthoredCheckpoint read_authored_checkpoint( - const JsonValue& value, std::string_view expected_game, std::string_view source + const JsonValue& value, std::string_view game, std::string_view source ); // Encode the authored envelope; execution is supplied by the game adapter. // This does not encode game bookkeeping or the legacy native-export envelope. JsonValue authored_checkpoint_json(const StoryCheckpoint& checkpoint, std::string_view game, JsonObject execution); -// Find and parse an embedded (filename without .json, text) resource for this game. -// Missing resources throw InternalProgramError; malformed documents throw ParseException. +// A JSON file built into the program: its name without ".json", and its +// text. +using EmbeddedJsonFile = std::pair; + +// Find embedded file `file` among `files` and parse it as `game`'s +// authored checkpoint. Missing resources throw InternalProgramError; +// malformed documents throw ParseException. AuthoredCheckpoint load_authored_checkpoint( - std::span> files, std::string_view file, std::string_view expected_game + std::span files, std::string_view file, std::string_view game ); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.cpp index a1483a3505..dfca06f04f 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson.cpp @@ -79,7 +79,7 @@ const JsonObject& fields(const JsonValue& value, std::initializer_list -Enum read_enum(const std::array, N>& names, const std::string& name, std::string_view source){ +Enum read_enum(const std::array, N>& names, std::string_view name, std::string_view source){ for (const auto& [value, label] : names){ if (label == name){ return value; diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp index f2ca6e9b0b..13b986b649 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Data/Pokemon_StoryStepJson_Tests.cpp @@ -108,7 +108,7 @@ std::string check_case(const WireCase& test){ #ifdef PA_STORY_DATA_STANDALONE // A shared case file's record kind. -Record record_kind(const std::string& name){ +Record record_kind(std::string_view name){ if (name == "pose"){ return Record::POSE; } diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp index 889639d650..6f24c76588 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryArgs_Tests.cpp @@ -30,7 +30,7 @@ constexpr StoryArgSpec MART_SPECS[] = { {.key = "map", .type = StoryArgType::TEXT, .required = true}, {.key = "budget", .type = StoryArgType::NUMBER, .required = true}, {.key = "floor", .type = StoryArgType::NUMBER}, - {.key = "if_present", .type = StoryArgType::FLAG}, + {.key = "if_present", .type = StoryArgType::BOOLEAN}, }; constexpr std::string_view MART_POSITIONAL[] = {"map"}; @@ -45,8 +45,8 @@ struct ArgsInput{ std::span positional = MART_POSITIONAL; }; -// The action name and args it gives, and the first error: the split's, -// else the specs'. +// The action name and args it gives, and its errors joined by "; ": the +// split's, else each of the specs'. struct ArgsExpectation{ std::string_view action; StoryArgs args = {}; @@ -63,7 +63,7 @@ const ArgsCase CASES[] = { .expectation = {.action = "mart", .args = {{"map", std::string("CeruleanCity_Mart")}, {"budget", int64_t(1500)}, {"floor", int64_t(1500)}, {"if_present", true}}}}, {.input = {.name = "plain name", .variant = "run", .specs = {}, .positional = {}}, .expectation = {.action = "run"}}, {.input = {.name = "words stay words", .variant = "teach tutor forget=tail-whip", .specs = {}, .positional = {}}, - .expectation = {.action = "teach", .args = {{"tutor", true}, {"forget", std::string("tail-whip")}}, .error = "unknown argument \"forget\""}}, + .expectation = {.action = "teach", .args = {{"tutor", true}, {"forget", std::string("tail-whip")}}, .error = "unknown argument \"forget\"; unknown argument \"tutor\""}}, {.input = {.name = "unknown key", .variant = "mart M budget=1 budgte=1"}, .expectation = {.action = "mart", .args = {{"map", std::string("M")}, {"budget", int64_t(1)}, {"budgte", int64_t(1)}}, .error = "unknown argument \"budgte\""}}, {.input = {.name = "wrong type", .variant = "mart M budget=lots"}, @@ -78,7 +78,12 @@ const ArgsCase CASES[] = { // "" when `test` reads as expected, else what differs. std::string check_case(const ArgsCase& test){ const LegacyVariant read = split_legacy_variant(test.input.variant, test.input.positional); - const std::string error = read.error.empty() ? story_args_error(read.args, test.input.specs) : read.error; + std::string error = read.error; + if (error.empty()){ + for (const std::string& each : story_args_errors(read.args, test.input.specs)){ + error += (error.empty() ? "" : "; ") + each; + } + } if (read.name != test.expectation.action || read.args != test.expectation.args){ return "read \"" + read.name + "\" with other args"; } diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp index f9a87c6778..22dfc9293a 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.cpp @@ -86,7 +86,7 @@ std::string to_string(const Outcome& outcome){ } Outcome outcome_game_not_running(){ - return outcome_failed(GAME_NOT_RUNNING, "the game is not running: the Switch HOME menu is showing"); + return outcome_failed(std::string(GAME_NOT_RUNNING), "the game is not running: the Switch HOME menu is showing"); } Condition condition(std::string name, Check check){ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h index 582518b9c3..fc560ca675 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryEvents.h @@ -32,6 +32,7 @@ #include #include #include +#include #include #include "Common/Cpp/MoveOnlyFunction.h" #include "Pokemon_StoryLog.h" @@ -119,7 +120,7 @@ std::string to_string(const Outcome& outcome); // The failure of a step that found the Switch's HOME menu instead of the // game. Nothing in the story can answer it, as every press goes to the // console's menus: no recovery runs and no reload, the run stops. -constexpr const char* GAME_NOT_RUNNING = "game_not_running"; +inline constexpr std::string_view GAME_NOT_RUNNING = "game_not_running"; // GAME_NOT_RUNNING, saying so for the run log. Outcome outcome_game_not_running(); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp index 8b5b3e0dca..0a9262d2c0 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.cpp @@ -27,20 +27,18 @@ std::string event_log_line(std::string_view checkpoint, size_t number, std::stri return std::format("[Event] {}/{} {}: {}", checkpoint, number, event, status); } -std::string checkpoint_start_line(std::string_view checkpoint){ - return std::format("[Checkpoint] {}: start", checkpoint); -} - -std::string checkpoint_start_line(std::string_view checkpoint, size_t events){ - return std::format("[Checkpoint] {}: start, {} events", checkpoint, events); -} - -std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt){ - return std::format("[Checkpoint] {}: attempt {}", checkpoint, attempt); +std::string checkpoint_start_line(std::string_view checkpoint, std::optional events){ + if (!events){ + return std::format("[Checkpoint] {}: start", checkpoint); + } + return std::format("[Checkpoint] {}: start, {} events", checkpoint, *events); } -std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt, size_t attempts){ - return std::format("[Checkpoint] {}: attempt {}/{}", checkpoint, attempt, attempts); +std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt, std::optional attempts){ + if (!attempts){ + return std::format("[Checkpoint] {}: attempt {}", checkpoint, attempt); + } + return std::format("[Checkpoint] {}: attempt {}/{}", checkpoint, attempt, *attempts); } std::string checkpoint_pass_line(std::string_view checkpoint, size_t attempt, size_t in_place){ @@ -55,7 +53,7 @@ std::string checkpoint_reset_line(std::string_view checkpoint, size_t next_attem return std::format("[Checkpoint] {}: failed, resetting for attempt {}", checkpoint, next_attempt); } -std::string checkpoint_reset_line(std::string_view segment, std::string_view checkpoint, size_t next_attempt, size_t attempts){ +std::string segment_reload_line(std::string_view segment, std::string_view checkpoint, size_t next_attempt, size_t attempts){ return std::format( "[Segment] {}: {} failed, reloading for attempt {}/{}", segment, checkpoint, next_attempt, attempts diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h index 5b5df714a9..f9576e1e69 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog.h @@ -15,6 +15,7 @@ #define PokemonAutomation_Pokemon_StoryLog_H #include +#include #include #include #include "Pokemon_AutoStoryStats.h" @@ -28,19 +29,18 @@ namespace Pokemon{ // [Event] 00.04/3 talk_mom: failed: expect_failed: has_parcel std::string event_log_line(std::string_view checkpoint, size_t number, std::string_view event, std::string_view status); -// A checkpoint starts: +// A checkpoint starts; the story engine also says it has `events` events: // [Checkpoint] 00.04: start -std::string checkpoint_start_line(std::string_view checkpoint); -// The story engine starts a checkpoint of `events` events: // [Checkpoint] 00.04: start, 3 events -std::string checkpoint_start_line(std::string_view checkpoint, size_t events); +std::string checkpoint_start_line(std::string_view checkpoint, std::optional events = std::nullopt); -// An attempt at a checkpoint starts, counting from 1: +// An attempt at a checkpoint starts, counting from 1, out of the +// program's limit of `attempts` if it has one: // [Checkpoint] 00.04: attempt 2 -std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt); -// The same, out of the program's limit of `attempts`: // [Checkpoint] 00.04: attempt 2/3 -std::string checkpoint_attempt_line(std::string_view checkpoint, size_t attempt, size_t attempts); +std::string checkpoint_attempt_line( + std::string_view checkpoint, size_t attempt, std::optional attempts = std::nullopt +); // A checkpoint passed on attempt `attempt`, after `in_place` recoveries in // place: @@ -54,7 +54,7 @@ std::string checkpoint_pass_line(std::string_view checkpoint, size_t attempt, si std::string checkpoint_reset_line(std::string_view checkpoint, size_t next_attempt); // The story engine reloads within segment `segment`, out of `attempts`: // [Segment] 00: 00.02 failed, reloading for attempt 2/3 -std::string checkpoint_reset_line(std::string_view segment, std::string_view checkpoint, size_t next_attempt, size_t attempts); +std::string segment_reload_line(std::string_view segment, std::string_view checkpoint, size_t next_attempt, size_t attempts); // What a story run adds to the program's stats. diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp index 7737d2cb41..0eacf7a54b 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryLog_Tests.cpp @@ -90,7 +90,7 @@ const StoryLogCase STORY_LOG_CASES[] = { .expectation = {.line = "[Checkpoint] 00.04: failed, resetting for attempt 2"}, }, { - .input = {.name = "reload within a segment", .line = checkpoint_reset_line("00", "00.02", 2, 3)}, + .input = {.name = "reload within a segment", .line = segment_reload_line("00", "00.02", 2, 3)}, .expectation = {.line = "[Segment] 00: 00.02 failed, reloading for attempt 2/3"}, }, }; diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp index 358f606dc6..d4eec835b5 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.cpp @@ -157,7 +157,7 @@ CheckpointReport run_row( if (attempt >= policy.attempts_per_checkpoint){ return result; } - say(hooks, checkpoint_reset_line(segment.id, spec.id, attempt + 1, policy.attempts_per_checkpoint)); + say(hooks, segment_reload_line(segment.id, spec.id, attempt + 1, policy.attempts_per_checkpoint)); report.reloads++; Outcome reloaded = reload(state, hooks); if (!reloaded.succeeded()){ @@ -194,6 +194,16 @@ size_t SegmentReport::one_shot_passes() const{ return count; } +StoryRunCounts SegmentReport::story_counts() const{ + StoryRunCounts counts{.segments = 1, .checkpoints = 0, .resets = reloads}; + for (const CheckpointRun& run : runs){ + if (run.passed){ + counts.checkpoints++; + } + } + return counts; +} + SegmentReport run_segment( RunState& state, Segment& segment, const SegmentRange& range, diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h index 8e2426aee8..051e9a702a 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StorySegments.h @@ -22,6 +22,7 @@ #include #include "Common/Cpp/MoveOnlyFunction.h" #include "Pokemon_StoryEvents.h" +#include "Pokemon_StoryLog.h" namespace PokemonAutomation{ namespace Pokemon{ @@ -112,6 +113,9 @@ struct SegmentReport{ std::vector runs; size_t one_shot_passes() const; + // What the run adds to the program's stats (record_story_counts()): + // the segment, each checkpoint that passed, each reload. + StoryRunCounts story_counts() const; }; // Runs the checkpoints in `range`, in order. Logs through `hooks.log`, diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp index 49736227b5..e7d4cfeeb1 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.cpp @@ -1,14 +1,12 @@ -/* Story Steps +/* Story Step Executor * * From: https://github.com/PokemonAutomation/ * */ #include -#include #include #include -#include "Pokemon_StoryLog.h" #include "Pokemon_StoryStepExecutor.h" namespace PokemonAutomation{ @@ -22,8 +20,8 @@ Outcome unsupported_step(const StoryStep& step){ void StoryStepExecutor::begin_attempt(const StoryCheckpoint&){} -bool StoryStepExecutor::holds(const std::string& condition, RunState& state){ - return state.flags.contains(condition); +bool StoryStepExecutor::holds(std::string_view condition, RunState& state){ + return state.flags.contains(std::string(condition)); } @@ -58,19 +56,6 @@ Event from_save_event(const Pose& start){ } // namespace -Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor){ - executor.begin_attempt(data); - 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{ // `event`, for a once_per_save step known in `prepared` as `key`: skipped @@ -91,13 +76,17 @@ void run_once_per_save(Event& event, std::string key, PreparedSteps& prepared){ } // namespace -Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor, PreparedSteps& prepared){ - Checkpoint checkpoint = build_story_checkpoint(data, executor); - // After "from_save", the events are the steps in order. - const size_t first = data.start ? 1 : 0; - for (size_t c = 0; c < data.steps.size(); c++){ - if (data.steps[c].once_per_save){ - run_once_per_save(checkpoint.events[first + c], data.id + "/" + data.steps[c].name, prepared); +Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor, PreparedSteps* prepared){ + executor.begin_attempt(data); + 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)); + if (prepared != nullptr && step.once_per_save){ + run_once_per_save(checkpoint.events.back(), data.id + "/" + step.name, *prepared); } } return checkpoint; @@ -162,32 +151,6 @@ std::vector lint_story_checkpoint(const StoryCheckpoint& data){ } -void record_story_stats(AutoStoryStats& stats, const SegmentReport& report){ - StoryRunCounts counts{.segments = 1, .checkpoints = 0, .resets = report.reloads}; - for (const CheckpointRun& run : report.runs){ - if (run.passed){ - counts.checkpoints++; - } - } - record_story_counts(stats, counts); -} - - -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; -} - - } // namespace Pokemon } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h index f718276ea1..c4b1c0774c 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor.h @@ -1,4 +1,4 @@ -/* Story Steps +/* Story Step Executor * * From: https://github.com/PokemonAutomation/ * @@ -32,16 +32,12 @@ #ifndef PokemonAutomation_Pokemon_StoryStepExecutor_H #define PokemonAutomation_Pokemon_StoryStepExecutor_H -#include #include #include +#include #include -#include "Pokemon/Navigation/Pokemon_Grid.h" -#include "Pokemon_AutoStoryStats.h" #include "Pokemon_StoryEvents.h" -#include "Pokemon_StorySegments.h" #include "Pokemon_TilePose.h" -#include "Types/Pokemon_StoryCheckLevel.h" #include "Types/Pokemon_StoryCheckpoint.h" #include "Types/Pokemon_StoryStep.h" #include "Types/Pokemon_StoryStepKind.h" @@ -68,7 +64,7 @@ class StoryStepExecutor{ // 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 bool holds(std::string_view condition, RunState& state); }; // "unsupported_step: TRAIN train_in_grass": what an executor without the kind returns. @@ -79,21 +75,21 @@ Outcome unsupported_step(const StoryStep& step); // 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); - // The once_per_save steps done from the save the game is on that changed // nothing, as "/". The game keeps it across reloads and // empties it when it saves or loads another save. using PreparedSteps = std::set; -// As above, and each once_per_save step is skipped while `prepared` has -// it, and added to it when it succeeds changing nothing. `prepared` must -// outlive the checkpoint. -Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor& executor, PreparedSteps& prepared); +// 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. +// With `prepared`, each once_per_save step is skipped while `prepared` has +// it, and added to it when it succeeds changing nothing; `prepared` must +// then outlive the checkpoint. Without, once_per_save steps run every time. +Checkpoint build_story_checkpoint( + const StoryCheckpoint& data, StoryStepExecutor& executor, PreparedSteps* prepared = nullptr +); // 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 @@ -101,19 +97,6 @@ Checkpoint build_story_checkpoint(const StoryCheckpoint& data, StoryStepExecutor std::vector lint_story_checkpoint(const StoryCheckpoint& data); -// Adds a segment's run to the program's stats: the segment, each checkpoint -// that passed, each reload. -void record_story_stats(AutoStoryStats& stats, const SegmentReport& report); - - -// 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); - - } // namespace Pokemon } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp index e65e9c2375..903dca6714 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryStepExecutor_Tests.cpp @@ -19,7 +19,9 @@ #include #include #include "Common/Cpp/TestRunners/UnitTest.h" +#include "Pokemon/Options/Pokemon_GameLanguages.h" #include "Pokemon/Options/Pokemon_StoryRunOptions.h" +#include "Pokemon_AutoStorySelect.h" #include "Pokemon_StoryEvents.h" #include "Pokemon_StorySegments.h" #include "Pokemon_StoryStepExecutor.h" @@ -38,7 +40,7 @@ namespace{ class FakeExecutor : public StoryStepExecutor{ public: std::vector trace; - std::set conditions; + std::set> conditions; // Steps by name that always fail, and how. std::set failing; // Steps by name that fail this many more times, then pass. @@ -68,7 +70,7 @@ class FakeExecutor : public StoryStepExecutor{ return run_kind(state, step); } - virtual bool holds(const std::string& condition, RunState& state) override{ + virtual bool holds(std::string_view condition, RunState& state) override{ return conditions.contains(condition) || StoryStepExecutor::holds(condition, state); } @@ -369,7 +371,7 @@ UnitTestResult test_segment_of_tables(){ && 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){ + const auto has = [&lines](std::string_view line){ return std::find(lines.begin(), lines.end(), line) != lines.end(); }; if (!runs_ok @@ -424,10 +426,10 @@ struct PolicyLab{ RunState state; return run_segment(state, segment, SegmentRange{}, policy, hooks); } - bool has(const std::string& line) const{ + bool has(std::string_view line) const{ return std::find(lines.begin(), lines.end(), line) != lines.end(); } - size_t count(const std::string& item) const{ + size_t count(std::string_view item) const{ return (size_t)std::count(executor.trace.begin(), executor.trace.end(), item); } }; @@ -509,7 +511,7 @@ struct BossLab{ segment.id = "00"; segment.checkpoints.push_back(CheckpointSpec{ .id = table.id, .title = table.title, .saves = true, - .build = [this]{ return build_story_checkpoint(table, executor, prepared); }, + .build = [this]{ return build_story_checkpoint(table, executor, &prepared); }, }); return segment; } @@ -527,7 +529,7 @@ struct BossLab{ hooks.log = [this](const std::string& line){ lines.push_back(line); }; return hooks; } - size_t count(const std::string& item) const{ + size_t count(std::string_view item) const{ return (size_t)std::count(executor.trace.begin(), executor.trace.end(), item); } }; @@ -626,7 +628,7 @@ UnitTestResult test_checkpoint_dropdowns(){ ){ return "Unexpected label."; } - StoryRunOptions options(database); + StoryRunOptions options(database, Gen8ToGen9Languages_Database()); if (options.start_index() != 0 || options.end_index() != 2 || !options.check_validity().empty()){ return "Expected the whole story by default."; } @@ -648,7 +650,7 @@ UnitTestResult test_story_stats(){ CheckpointRun{.id = "00.05", .passed = false, .attempts = 1, .recoveries = 0}, }; AutoStoryStats stats; - record_story_stats(stats, report); + record_story_counts(stats, report.story_counts()); if (stats.m_segment != 1 || stats.m_checkpoint != 2 || stats.m_reset != 2){ return "Expected 1 segment, 2 checkpoints and 2 resets."; } @@ -658,13 +660,13 @@ UnitTestResult test_story_stats(){ // The program's option group: 3 attempts, no stop, Thorough, by default; // Fast when chosen. UnitTestResult test_run_options(){ - StoryRunOptions options(two_sections_database()); + StoryRunOptions options(two_sections_database(), Gen8ToGen9Languages_Database()); const SegmentPolicy policy = options.policy(); - if (policy.attempts_per_checkpoint != 3 || policy.stop_at_first_failure || options.checks() != StoryChecks::THOROUGH){ + if (policy.attempts_per_checkpoint != 3 || policy.stop_at_first_failure || options.check_level() != StoryCheckLevel::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){ + options.CHECKS.set(StoryCheckLevel::FAST); + if (options.check_level() != StoryCheckLevel::FAST){ return "Expected Fast checks once chosen."; } // Every game language can be chosen, read or not. @@ -681,7 +683,7 @@ UnitTestResult test_run_options(){ // Verify mode: off by default; 3 runs and 2 passes when on, for one // checkpoint only, never needing more passes than runs. UnitTestResult test_verify_options(){ - StoryRunOptions options(two_sections_database()); + StoryRunOptions options(two_sections_database(), Gen8ToGen9Languages_Database()); if (options.VERIFY.policy()){ return "Expected verify mode off by default."; } @@ -734,7 +736,7 @@ struct StoryStepInput{ }; struct StoryStepExpectation{ - const char* description; + std::string_view description; }; struct StoryStepTestCase{ diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h index bd8c02343a..c81e6de25c 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_StoryVariant.h @@ -65,7 +65,7 @@ struct LegacyVariant{ // if_present"): its first word is the name; the next plain words take the // keys in `positional`, in order, and plain words after those are flags; // "key=value" words are numbers when all digits, else words. It reads no -// action's specs: check the args against them (story_args_error()). +// action's specs: check the args against them (story_args_errors()). LegacyVariant split_legacy_variant(std::string_view variant, std::span positional = {}); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp index c4da5548fd..2ce9395904 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.cpp @@ -5,6 +5,8 @@ */ #include +#include +#include #include "Pokemon_StoryArgs.h" namespace PokemonAutomation{ @@ -22,7 +24,7 @@ const Type* find_arg(const StoryArgs& args, std::string_view key){ bool has_type(const StoryArgValue& value, StoryArgType type){ switch (type){ - case StoryArgType::FLAG: + case StoryArgType::BOOLEAN: return std::holds_alternative(value); case StoryArgType::NUMBER: return std::holds_alternative(value); @@ -60,22 +62,22 @@ bool story_arg_flag(const StoryArgs& args, std::string_view key){ return value != nullptr && *value; } -std::string story_args_error(const StoryArgs& args, std::span specs){ +std::vector story_args_errors(const StoryArgs& args, std::span specs){ + std::vector errors; for (const auto& [key, value] : args){ const StoryArgSpec* spec = find_spec(specs, key); if (spec == nullptr){ - return std::format("unknown argument \"{}\"", key); - } - if (!has_type(value, spec->type)){ - return std::format("argument \"{}\" has the wrong type", key); + errors.emplace_back(std::format("unknown argument \"{}\"", key)); + }else if (!has_type(value, spec->type)){ + errors.emplace_back(std::format("argument \"{}\" has the wrong type", key)); } } for (const StoryArgSpec& spec : specs){ if (spec.required && args.find(spec.key) == args.end()){ - return std::format("missing argument \"{}\"", spec.key); + errors.emplace_back(std::format("missing argument \"{}\"", spec.key)); } } - return ""; + return errors; } diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h index 96214deed7..9556c85dfe 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryArgs.h @@ -21,6 +21,7 @@ #include #include #include +#include namespace PokemonAutomation{ namespace Pokemon{ @@ -45,7 +46,7 @@ bool story_arg_flag(const StoryArgs& args, std::string_view key); // The type an action declares for an argument. enum class StoryArgType{ - FLAG, + BOOLEAN, NUMBER, TEXT, }; @@ -57,9 +58,9 @@ struct StoryArgSpec{ bool required = false; }; -// Why `args` don't fit `specs`: a key no spec names, a value of another -// type, or a required one missing. Empty when they fit. -std::string story_args_error(const StoryArgs& args, std::span specs); +// Each way `args` don't fit `specs`: a key no spec names, a value of +// another type, a required one missing. Empty when they fit. +std::vector story_args_errors(const StoryArgs& args, std::span specs); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp index a381e58c37..8502162a15 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.cpp @@ -10,11 +10,11 @@ namespace PokemonAutomation{ namespace Pokemon{ -const char* story_checks_name(StoryChecks checks){ - switch (checks){ - case StoryChecks::THOROUGH: +std::string story_check_level_name(StoryCheckLevel level){ + switch (level){ + case StoryCheckLevel::THOROUGH: return "thorough"; - case StoryChecks::FAST: + case StoryCheckLevel::FAST: return "fast"; } return "?"; diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h index 337df1d541..ddb86dfb27 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h @@ -1,4 +1,4 @@ -/* Story Checks +/* Story Check Level * * From: https://github.com/PokemonAutomation/ * @@ -11,12 +11,14 @@ #ifndef PokemonAutomation_Pokemon_StoryCheckLevel_H #define PokemonAutomation_Pokemon_StoryCheckLevel_H +#include + namespace PokemonAutomation{ namespace Pokemon{ // How much a step reads the screen to confirm what it did. -enum class StoryChecks{ +enum class StoryCheckLevel{ // Confirm on screen: purchases, the Bag, status after battles, position. THOROUGH, // In a rush: trust counts and timings. @@ -24,7 +26,7 @@ enum class StoryChecks{ }; // Returns the readable string representation of a story check level. -const char* story_checks_name(StoryChecks checks); +std::string story_check_level_name(StoryCheckLevel level); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp index 8491db4f90..28b755779b 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.cpp @@ -10,7 +10,7 @@ namespace PokemonAutomation{ namespace Pokemon{ -const char* story_step_kind_name(StoryStepKind kind){ +std::string story_step_kind_name(StoryStepKind kind){ switch (kind){ case StoryStepKind::NEW_GAME: return "NEW_GAME"; diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h index 2bc8bb948c..2e43aa8118 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Types/Pokemon_StoryStepKind.h @@ -10,6 +10,8 @@ #ifndef PokemonAutomation_Pokemon_StoryStepKind_H #define PokemonAutomation_Pokemon_StoryStepKind_H +#include + namespace PokemonAutomation{ namespace Pokemon{ @@ -34,7 +36,7 @@ enum class StoryStepKind{ }; // Returns the readable string representation of a story step kind. -const char* story_step_kind_name(StoryStepKind kind); +std::string story_step_kind_name(StoryStepKind kind); diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.cpp new file mode 100644 index 0000000000..42f0dadf08 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.cpp @@ -0,0 +1,43 @@ +/* Game Languages + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Pokemon_GameLanguages.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +const EnumDropdownDatabase& Gen8ToGen9Languages_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; +} + +const EnumDropdownDatabase& FireRedLeafGreenLanguages_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"}, + }); + return database; +} + + + +} // namespace Pokemon +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.h new file mode 100644 index 0000000000..eec094dfaf --- /dev/null +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_GameLanguages.h @@ -0,0 +1,35 @@ +/* Game Languages + * + * From: https://github.com/PokemonAutomation/ + * + * The languages each game can be played in, as dropdown databases for a + * program's language option. A program offers its game's set, so a user + * can't pick a language the game doesn't have. + * + */ + +#ifndef PokemonAutomation_Pokemon_GameLanguages_H +#define PokemonAutomation_Pokemon_GameLanguages_H + +#include "Common/Cpp/Options/EnumDropdownDatabase.h" +#include "CommonFramework/Language.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +// The nine languages of the Switch games of generations 8 and 9: Sword and +// Shield, Brilliant Diamond and Shining Pearl, Legends: Arceus, Scarlet and +// Violet, Legends: Z-A. Japanese, English, French, Italian, German, +// Spanish, Korean, Chinese (Simplified and Traditional). +const EnumDropdownDatabase& Gen8ToGen9Languages_Database(); + +// The six languages of FireRed and LeafGreen: Japanese, English, French, +// Italian, German, Spanish. +const EnumDropdownDatabase& FireRedLeafGreenLanguages_Database(); + + + +} // namespace Pokemon +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp index 1126620526..8a632cc469 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRangeOptions_Tests.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include "Common/Cpp/TestRunners/UnitTest.h" @@ -31,8 +32,8 @@ struct NamedCheckpoint{ const std::vector>& story_list(){ static const std::vector> list = []{ std::vector> ret; - for (const char* label : {"tutorial 0", "tutorial 1", "main 0", "main 1", "main 2"}){ - ret.emplace_back(std::make_unique(NamedCheckpoint{.label = label})); + for (std::string_view label : {"tutorial 0", "tutorial 1", "main 0", "main 1", "main 2"}){ + ret.emplace_back(std::make_unique(NamedCheckpoint{.label = std::string(label)})); } return ret; }(); diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp index 9f8da731a0..32696bf4d6 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.cpp @@ -10,25 +10,10 @@ 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; -} - -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)"}, +const EnumDropdownDatabase& StoryCheckLevel_Database(){ + static const EnumDropdownDatabase database({ + {StoryCheckLevel::THOROUGH, "thorough", "Thorough: confirm purchases, the Bag, status and position on screen"}, + {StoryCheckLevel::FAST, "fast", "Fast: trust counts and timings"}, }); return database; } @@ -38,6 +23,14 @@ std::string story_checkpoint_label(const StoryCheckpoint& checkpoint){ return checkpoint.title.empty() ? checkpoint.id : checkpoint.id + " " + checkpoint.title; } +StringSelectDatabase story_checkpoint_database(std::span rows){ + StringSelectDatabase database; + for (size_t c = 0; c < rows.size(); c++){ + database.add_entry(StringSelectEntry(std::to_string(c), story_checkpoint_label(rows[c]))); + } + return database; +} + StoryVerifyOptions::StoryVerifyOptions() : GroupOption( @@ -75,7 +68,7 @@ std::string StoryVerifyOptions::check_validity() const{ } -StoryRunOptions::StoryRunOptions(const StringSelectDatabase& checkpoints) +StoryRunOptions::StoryRunOptions(const StringSelectDatabase& checkpoints, const EnumDropdownDatabase& languages) : StoryRangeOptions("Story Run", checkpoints) , ATTEMPTS_PER_CHECKPOINT( "Attempts per checkpoint:
A failed checkpoint reloads its save and runs again, up to this many times in all.", @@ -89,14 +82,14 @@ StoryRunOptions::StoryRunOptions(const StringSelectDatabase& checkpoints) ) , 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(), + StoryCheckLevel_Database(), LockMode::LOCK_WHILE_RUNNING, - StoryChecks::THOROUGH + StoryCheckLevel::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(), + languages, LockMode::LOCK_WHILE_RUNNING, Language::English ) @@ -126,7 +119,7 @@ SegmentPolicy StoryRunOptions::policy() const{ return policy; } -StoryChecks StoryRunOptions::checks() const{ +StoryCheckLevel StoryRunOptions::check_level() const{ return CHECKS; } diff --git a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h index 5a63cfbe95..1fe685c5fb 100644 --- a/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h +++ b/SerialPrograms/Source/Pokemon/Options/Pokemon_StoryRunOptions.h @@ -15,17 +15,15 @@ #define PokemonAutomation_Pokemon_StoryRunOptions_H #include -#include #include +#include #include -#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 "CommonFramework/Language.h" #include "CommonTools/Options/StringSelectOption.h" -#include "Pokemon/AutoStory/Pokemon_AutoStorySelect.h" #include "Pokemon/AutoStory/Pokemon_StorySegments.h" #include "Pokemon/AutoStory/Types/Pokemon_StoryCheckLevel.h" #include "Pokemon/AutoStory/Types/Pokemon_StoryCheckpoint.h" @@ -35,36 +33,17 @@ namespace PokemonAutomation{ namespace Pokemon{ -// A checkpoint's entry in the start and end dropdowns, as SV labels its +// A checkpoint's label in the start and end dropdowns, as SV labels its // segments: "00.09 Pewter Gym: Camper Liam, then Brock". -struct StoryCheckpointEntry{ - std::string label; - - const std::string& name() const{ - return label; - } -}; - -// Formats a checkpoint identifier and description into a dropdown label. std::string story_checkpoint_label(const StoryCheckpoint& checkpoint); // The start/end dropdown database of a story's checkpoints, `rows` in -// order, slugged by their index in it (make_story_select_database()). A -// game whose story is in sections (BDSP's segments 00 and 01) passes them -// joined: a section's entry i is then story_index(true, i, first of the -// section). The program keeps it in a function-local static: the options -// hold a reference. -template -StringSelectDatabase story_checkpoint_database(const std::vector& rows); - -template -StringSelectDatabase story_checkpoint_database(const std::vector& rows){ - std::vector> entries; - for (const StoryCheckpoint& row : rows){ - entries.emplace_back(std::make_unique(StoryCheckpointEntry{.label = story_checkpoint_label(row)})); - } - return make_story_select_database(entries, 0, entries.size()); -} +// order, slugged by their index in it, as make_story_select_database() +// slugs SV's. A game whose story is in sections (BDSP's segments 00 and +// 01) passes them joined: a section's entry i is then story_index(true, +// i, first of the section). The program keeps it in a function-local +// static: the options hold a reference. +StringSelectDatabase story_checkpoint_database(std::span rows); // Verify mode, off by default (verify_checkpoint()): the start checkpoint @@ -92,7 +71,8 @@ class StoryRunOptions : public StoryRangeOptions{ public: // `checkpoints`: story_checkpoint_database() of the game's story, kept // alive by the caller; the dropdowns start on its first and last. - explicit StoryRunOptions(const StringSelectDatabase& checkpoints); + // `languages`: the ones the game has (Pokemon_GameLanguages.h). + StoryRunOptions(const StringSelectDatabase& checkpoints, const EnumDropdownDatabase& languages); // The range's check, then verify mode's: one checkpoint only. virtual std::string check_validity() const override; @@ -101,14 +81,14 @@ class StoryRunOptions : public StoryRangeOptions{ // set, not the user's. SegmentPolicy policy() const; // The checks level, for the game's steps to read. - StoryChecks checks() const; + StoryCheckLevel check_level() const; // The game's language, for the steps that read text. Language language() const; public: SimpleIntegerOption ATTEMPTS_PER_CHECKPOINT; BooleanCheckBoxOption STOP_AT_FIRST_FAILURE; - EnumDropdownOption CHECKS; + 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; diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 87a5ee7896..1e121e4ab0 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1481,6 +1481,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Options/Pokemon_BoxSortingTable.cpp Source/Pokemon/Options/Pokemon_BoxSortingTable.h Source/Pokemon/Options/Pokemon_EncounterBotOptions.h + Source/Pokemon/Options/Pokemon_GameLanguages.cpp + Source/Pokemon/Options/Pokemon_GameLanguages.h Source/Pokemon/Options/Pokemon_HomeSpriteSelectOption.cpp Source/Pokemon/Options/Pokemon_HomeSpriteSelectOption.h Source/Pokemon/Options/Pokemon_IvJudgeOption.cpp