From 0e25e72bc7f97601d19fd3b7a2cbfeb03a5b02e2 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 14:44:08 -0800 Subject: [PATCH 01/10] Load BDSP mine checkpoint from shared JSON without changing its executor --- .../workflows/cpp-ci-serial-programs-base.yml | 30 +++ SerialPrograms/CMakeLists.txt | 3 + .../PokemonBDSP_AutoStory_StoryData.cpp | 57 +++++ .../PokemonBDSP_AutoStory_StoryData.h | 25 +++ .../PokemonBDSP_AutoStory_StoryData_Tests.cpp | 95 +++++++++ .../PokemonBDSP_AutoStory_StoryData_Tests.h | 13 ++ .../PokemonBDSP_AutoStory_StoryTable.cpp | 108 +--------- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 + ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 24 +++ .../Programs/AutoStory/StoryData/README.md | 14 ++ .../StoryData/bdsp-mine-01.03.plan.json | 197 ++++++++++++++++++ SerialPrograms/cmake/SourceFiles.cmake | 7 +- 12 files changed, 469 insertions(+), 106 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.plan.json diff --git a/.github/workflows/cpp-ci-serial-programs-base.yml b/.github/workflows/cpp-ci-serial-programs-base.yml index a87f526da4..ffb7cc4da8 100644 --- a/.github/workflows/cpp-ci-serial-programs-base.yml +++ b/.github/workflows/cpp-ci-serial-programs-base.yml @@ -96,6 +96,36 @@ jobs: operation: prepare consumer-directory: Arduino-Source + - name: Checkout authored BDSP story simulator + if: startsWith(inputs.os, 'ubuntu') && inputs.run-tests + uses: actions/checkout@v7 + with: + repository: dayfine/PokemonSimulation + ref: fd2f1ab0facb116c38b3817520eeac0b3d3db8e0 + path: AuthoredStorySource + + - name: Check identical authored mine JSON and replay its scenarios + if: startsWith(inputs.os, 'ubuntu') && inputs.run-tests + env: + PYTHONPATH: AuthoredStorySource/src + run: | + python3 - <<'PYTHON' + from pathlib import Path + from pokesim.story.replay_binding import load_binding + from pokesim.story.replay_cases import read_cases, check_case + from pokesim.story.checkpoint_replay import run + source = Path('AuthoredStorySource') + folder = source / 'data/story/bdsp-mine-pilot' + native = Path('Arduino-Source/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData') + assert (native / 'bdsp-mine-01.03.plan.json').read_bytes() == (folder / 'bdsp-mine-01.03.plan.json').read_bytes() + binding = load_binding(folder / 'binding.json', source) + cases = read_cases(folder / 'cases.json') + for case in cases: + check_case(case, run(binding, case['input'])) + print(case['id'] + ': authored BDSP JSON replay passed') + assert len(cases) == 9 + PYTHON + - name: Install Qt uses: jurplel/install-qt-action@v4 with: diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index 0fe561e736..cb1055d8e3 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -240,6 +240,9 @@ pa_pokesim_map_directory(BDSP_MAP_DIRECTORY bdsp "${POKESIM_CONSUMER}" "${POKESI file(RELATIVE_PATH BDSP_MAP_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}" "${BDSP_MAP_DIRECTORY}") pa_embed_text_files(SerialProgramsLib "${BDSP_MAP_RESOURCES}" PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h PokemonAutomation::NintendoSwitch::PokemonBDSP bdsp_map_json) +pa_embed_text_files(SerialProgramsLib Source/PokemonBDSP/Programs/AutoStory/StoryData + PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h PokemonAutomation::NintendoSwitch::PokemonBDSP bdsp_checkpoint_json) + # Create binary target diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp new file mode 100644 index 0000000000..6685a8f14f --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp @@ -0,0 +1,57 @@ +/* BDSP authored story data + * From: https://github.com/PokemonAutomation/ + */ +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Pokemon/AutoStory/Pokemon_StoryStepJson.h" +#include "PokemonBDSP_AutoStory_StoryData.h" +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ +namespace{ +void exact_fields(const JsonObject& value, std::initializer_list names){ + if (value.size() != names.size()){ + throw ParseException("BDSP authored checkpoint: missing or extra fields."); + } + for (std::string_view name : names){ + value.get_value_throw(std::string(name)); + } +} +StoryFailure read_failure(const std::string& value){ + if (value == "reload_and_retry"){ + return StoryFailure::RELOAD_AND_RETRY; + } + if (value == "stop"){ + return StoryFailure::STOP; + } + throw ParseException("Unsupported BDSP failure policy: " + value); +} +} // namespace +StoryCheckpoint read_authored_checkpoint(const JsonValue& value, std::string_view source){ + const JsonObject& root = value.to_object_throw(std::string(source)); + exact_fields(root, {"format", "game", "checkpoint", "execution"}); + if (root.get_string_throw("format") != "pokesim-authored-checkpoint/1" || root.get_string_throw("game") != "bdsp"){ + throw ParseException("Unsupported BDSP authored format/game."); + } + const JsonObject& execution = root.get_object_throw("execution"); + exact_fields(execution, {"on_failure", "saves_in"}); + StoryCheckpoint result; + static_cast(result) = Pokemon::read_story_checkpoint(root.get_value_throw("checkpoint"), source); + result.on_failure = read_failure(execution.get_string_throw("on_failure")); + result.saves_in = execution.get_string_throw("saves_in"); + if ((result.on_failure == StoryFailure::RELOAD_AND_RETRY) == result.saves_in.empty()){ + throw ParseException("BDSP save-card location is required exactly when the checkpoint saves/retries."); + } + return result; +} +StoryCheckpoint load_authored_checkpoint(std::string_view file){ + for (const auto& [name, text] : bdsp_checkpoint_json()){ + if (name == file){ + return read_authored_checkpoint(parse_json(text), file); + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "BDSP authored checkpoint is not embedded: " + std::string(file)); +} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h new file mode 100644 index 0000000000..f08c58cfc7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h @@ -0,0 +1,25 @@ +/* BDSP authored story data + * From: https://github.com/PokemonAutomation/ + */ +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryData_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryData_H +#include +#include +#include +#include +#include "Common/Cpp/Json/JsonValue.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ +// Strict authored envelope and BDSP save/failure configuration; no controller IO. +// Unknown fields/values or inconsistent save policy throw ParseException. +StoryCheckpoint read_authored_checkpoint(const JsonValue& value, std::string_view source); +// The embedded authored JSON files, keyed by filename without .json. +const std::vector>& bdsp_checkpoint_json(); +// Load an embedded authored checkpoint; a missing file throws InternalProgramError. +StoryCheckpoint load_authored_checkpoint(std::string_view file); +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp new file mode 100644 index 0000000000..74683ec3be --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp @@ -0,0 +1,95 @@ +/* BDSP authored checkpoint tests + * From: https://github.com/PokemonAutomation/ + */ +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Pokemon/AutoStory/Pokemon_StoryStepJson.h" +#include "PokemonBDSP_AutoStory_StoryData.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StoryData_Tests.h" +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ +namespace{ +struct Input{ + std::string_view name; + void (*mutate)(JsonObject&) = nullptr; +}; +struct Expectation{ bool rejected = false; }; +struct Case{ Input input; Expectation expectation; }; +void extra(JsonObject& root){ root["unknown"] = true; } +void wrong_game(JsonObject& root){ root["game"] = "frlg"; } +void wrong_policy(JsonObject& root){ root.get_object_throw("execution")["on_failure"] = "guess"; } +void missing_card(JsonObject& root){ root.get_object_throw("execution")["saves_in"] = ""; } +void stop_with_save(JsonObject& root){ root.get_object_throw("execution")["on_failure"] = "stop"; } +const Case CASES[] = { + {.input = {.name = "authored mine row replaces the table entry"}, .expectation = {}}, + {.input = {.name = "extra envelope field", .mutate = extra}, .expectation = {.rejected = true}}, + {.input = {.name = "wrong game", .mutate = wrong_game}, .expectation = {.rejected = true}}, + {.input = {.name = "unknown failure policy", .mutate = wrong_policy}, .expectation = {.rejected = true}}, + {.input = {.name = "missing save-card location", .mutate = missing_card}, .expectation = {.rejected = true}}, + {.input = {.name = "stop policy must not claim a start save", .mutate = stop_with_save}, .expectation = {.rejected = true}}, +}; +JsonValue plan(){ + for (const auto& [name, text] : bdsp_checkpoint_json()){ + if (name == "bdsp-mine-01.03.plan"){ + return parse_json(text); + } + } + throw ParseException("Expected embedded bdsp-mine-01.03.plan.json."); +} +std::string check_table(const StoryCheckpoint& loaded, const JsonValue& input){ + const auto& rows = story_checkpoints(); + const auto row = std::find_if(rows.begin(), rows.end(), [](const StoryCheckpoint& x){ return x.id == "01.03"; }); + if (row == rows.end() || Pokemon::story_checkpoint_json(*row) != input.to_object_throw().get_value_throw("checkpoint")){ + return "Expected the actual 01.03 table entry to contain every authored field."; + } + if (loaded.on_failure != StoryFailure::RELOAD_AND_RETRY || loaded.saves_in != "Oreburgh City" + || row->on_failure != loaded.on_failure || row->saves_in != loaded.saves_in){ + return "Expected reload-and-retry with Oreburgh City save-card guard."; + } + const std::vector expected{ + "above_the_mine", "into_the_mine", "into_the_coal_room", "south_to_the_rock", + "to_roarks_corner", "beside_roarks_rock", "below_roark", "roark", "roark_smashes_the_rock", + "below_the_coal_rooms_warp", "up_to_the_hall", "out_of_the_mine", + "north_to_the_centers_wall", "east_below_the_centers_door", "heal_oreburgh" + }; + std::vector actual; + for (const auto& step : loaded.steps){ + actual.push_back(step.name); + } + return actual == expected ? "" : "Expected all 15 current #226 mine events in order."; +} +class DataTest : public UnitTest{ +public: + explicit DataTest(Case test) : UnitTest("PokemonBDSP::StoryData - " + std::string(test.input.name)), m_test(test){} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + JsonValue input = plan(); + if (m_test.input.mutate){ + m_test.input.mutate(input.to_object_throw()); + } + try{ + const StoryCheckpoint loaded = read_authored_checkpoint(input, m_test.input.name); + if (m_test.expectation.rejected){ + return "Expected authored data rejection."; + } + const std::string error = check_table(loaded, input); + return error.empty() ? UnitTestResult(true) : UnitTestResult(error); + }catch (const Exception& error){ + return m_test.expectation.rejected ? UnitTestResult(true) : UnitTestResult(error.to_str()); + } + } +private: + Case m_test; +}; +} // namespace +void add_tests_AutoStory_StoryData(UnitTestDatabase& database){ + for (const Case& test : CASES){ + database.add(test); + } +} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h new file mode 100644 index 0000000000..49ec762169 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h @@ -0,0 +1,13 @@ +/* Authored checkpoint tests */ +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryData_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryData_Tests_H +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ +// Register strict authored-data and actual table integration cases. +void add_tests_AutoStory_StoryData(UnitTestDatabase& database); +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp index 006a03ddb0..5ea62e4fce 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp @@ -23,6 +23,7 @@ #include #include #include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP_AutoStory_StoryData.h" #include "PokemonBDSP_AutoStory_StoryTable.h" namespace PokemonAutomation{ @@ -59,10 +60,6 @@ const char* const JUBILIFE = "JubilifeCity"; const char* const TRAINERS_SCHOOL = "TrainersSchool"; const char* const OREBURGH = "OreburghCity"; const char* const GYM = "OreburghGym"; -// The mine's entrance hall (Diamond's MAP_OREBURGH_MINE_B1F) and the coal -// room below it, Roark's. -const char* const MINE_HALL = "OreburghMineB1F"; -const char* const MINE = "OreburghMine"; const char* const GATE = "OreburghGate"; const char* const ROUTE_203 = "Route203"; const char* const ROUTE_204_SOUTH = "Route204South"; @@ -679,68 +676,6 @@ StoryCheckpoint route_203_to_oreburgh(){ }); } -// 01.03, planned on Oreburgh's map and the coal room's (Diamond's zone -// events narc_0044 and narc_0197): Barry in the Center's door says Roark -// went to the mine (Roark ignored A until then, r101 and r104); down the -// door's column to the mine's warp tiles (44..48, 59); straight down the -// entrance hall to its warps (11..13, 21); in the coal room down to -// Roark, whose scene ends with him gone (Platinum's -// scripts_oreburgh_mine_b2f.s: he smashes the rock east of him and walks -// off east); back up and out; and the Center's nurse, so 01.04 starts on -// the doorstep. The Center's door is the decomp's (47, 20): out of -// Sandgem's and Jubilife's Centers the console stood in their doors' -// columns. The map's doorstep (46, 21) is the Gym's door column plus the -// 20 taps of 01.04 (r217d, r218d), which do not say where the taps began; -// the heal lines up on the lit doorway if north from (47, 21) misses. -// -// The hall is walked on counted taps, not held: it is a cave, and a wild -// battle on a hold toward a fade would read as the fade. Its warp tiles -// are only entered on the step after, which meets no wild Pokemon. The -// warps are Platinum's (pret/pokeplatinum res/field/maps/*/events.json; -// BDSP keeps DP's layout): the city's (44..48, 59) lead to the hall's -// (11, 1), (11, 1), (12, 1), (13, 1), (13, 1), and the hall's (11..13, 1) -// back to the city's (45..47, 59), a column each. Down, from the city's -// (46, 59) we land on the hall's (12, 1); up, the coal room's warp (15, 1) -// lands us on the hall's (12, 21), not (11, 21), so the walk north up -// column 12 comes out on the city's (46, 59), east of the mine's sign -// (45, 56): the walk north to it never stopped there (lab-B 2026-11-08, -// 09:46, 10:39 and 12:03). -// -// Across the coal room to Roark's east corner (20, 28): the north wall above -// it, his rock (19, 28) to its west. A wild battle on a counted tap re-plans -// to the end of the step, and a step that ends in a corner ends that re-plan -// on wall clips. Lab-B 2026-11-08 11:55: the re-plan went only to the end of -// the interrupted leg, the open tile (15, 16); its last leg, south 5 across -// the open room, overshot a row, and the taps after ran under Roark's rocks. -std::vector legs_to_roarks_corner(){ - return { - {.direction = EAST, .tiles = 1}, - {.direction = SOUTH, .tiles = 2}, - {.direction = WEST, .tiles = 1}, - {.direction = SOUTH, .tiles = 5}, - {.direction = EAST, .tiles = 14}, - {.direction = SOUTH, .tiles = 10}, - {.direction = WEST, .tiles = 2}, - {.direction = SOUTH, .tiles = 2}, - {.direction = WEST, .tiles = 7}, - }; -} - -std::vector legs_out_of_coal_room(){ - return { - {.direction = EAST, .tiles = 9}, - {.direction = NORTH, .tiles = 3}, - {.direction = EAST, .tiles = 2}, - {.direction = NORTH, .tiles = 10}, - {.direction = WEST, .tiles = 14}, - {.direction = NORTH, .tiles = 5}, - {.direction = EAST, .tiles = 1}, - {.direction = NORTH, .tiles = 2}, - {.direction = WEST, .tiles = 1}, - {.direction = NORTH, .tiles = 7}, - }; -} - // 01.02b: the welcome leaves the party as Route 203 left it (2026-11-07 runs: // the lead paralysed, its water move out of PP, and every reload of 01.03 // replayed that). Barry's lines first, as before the split (he blocks the @@ -757,45 +692,9 @@ StoryCheckpoint oreburgh_center_heal(){ }); } +// The authored file carries the mine route and its save-card/failure policy. StoryCheckpoint oreburgh_mine_to_roark(){ - const Pose below_roark = next_to(MINE, "roark", SOUTH, NORTH); - return row("01.03", "Oreburgh Mine to Roark", out_of_oreburgh_center(), { - route("above_the_mine", at(OREBURGH, 46, 58, SOUTH), "down the door's column to above the mine's warp tiles", "town"), - step_into("into_the_mine", SOUTH, "the warp tile (46, 59)", at(MINE_HALL, 12, 1, SOUTH)), - step_into("into_the_coal_room", SOUTH, "down the entrance hall through the cave mouth", at(MINE, 15, 2, SOUTH)), - anchored(to_walls("south_to_the_rock", {{.direction = SOUTH, .tiles = 8}}, "south against the corridor rock (15, 11)"), - at(MINE, 15, 9, SOUTH)), - anchored(legs("to_roarks_corner", legs_to_roarks_corner(), "east around the rock, across the room, down column 29, west to Roark's rock"), - at(MINE, 20, 28, WEST)), - // Counted taps along the open row 29 drifted (lab-B 2026-11-07: south 3 - // moved 2): south stops at the wall under row 29, two tiles from below him. - anchored(to_walls("beside_roarks_rock", {{.direction = SOUTH, .tiles = 1}}, "south against the room's wall"), - at(MINE, 20, 29, SOUTH)), - anchored(legs("below_roark", {{.direction = WEST, .tiles = 2}}, "west 2 to below Roark"), below_roark), - talk("roark", "Roark, north of us between his rocks"), - scene("roark_smashes_the_rock", below_roark, "his lines, Rock Smash on the rock east of him, and he walks off"), - anchored(legs("below_the_coal_rooms_warp", legs_out_of_coal_room(), "east around crate, up column 29, west across row 17, and up corridor to warp doorstep"), - at(MINE, 15, 2, NORTH)), - step_into("up_to_the_hall", NORTH, "the warp tile (15, 1)", at(MINE_HALL, 12, 21, NORTH)), - // Out on the city's (46, 59), the warp tile of the hall's column 12 - // (the decomp's warp: the hall's (12, 1) to the city's (46, 59)), or - // a tile north should the walk go on past the fade. On the console - // (lab-B 2026-11-08 13:50) a tap north and a hold east to the wall - // matched the map only from column 46. - step_into("out_of_the_mine", NORTH, "up the entrance hall and out to the city", - Pokemon::tile_pose(OREBURGH, {TilePos{46, 59}, TilePos{46, 58}}, NORTH)), - // Straight north up column 46 to the wall west of the Center's door, - // (46, 20): 37 tiles from (46, 58), 38 from (46, 59). Oreburgh's - // Hiker wanders (46..48, 25..27) across it, and the walk waits for - // him. The column was the one the console stopped on at 10:39, and - // it keeps off the street east of the mine, where at 13:50 a hold - // east from (48, 57) moved 0 though the map has (49, 57) open, and - // the walk after went astray. - anchored(legs("north_to_the_centers_wall", {{.direction = NORTH, .tiles = 37}}, - "north up column 46 to the wall west of the Center's door", "counted_wall"), at(OREBURGH, 46, 21, NORTH)), - anchored(legs("east_below_the_centers_door", {{.direction = EAST, .tiles = 1}}, "a tap east below the door"), below_oreburghs_center()), - heal("heal_oreburgh", "the nurse; out onto the doorstep, where 01.04 starts", out_of_oreburgh_center()), - }); + return load_authored_checkpoint("bdsp-mine-01.03.plan"); } // Below Rowan's trigger in Jubilife: (47, 8), the east tile under the @@ -946,7 +845,6 @@ const std::map& start_save_cards(){ {"01.01", "Jubilife City"}, {"01.02", "Route 203"}, {"01.02b", "Oreburgh City"}, - {"01.03", "Oreburgh City"}, {"01.04", "Oreburgh City"}, {"01.05", "Oreburgh City"}, {"01.06", "Route 203"}, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index c16b365d7f..44a6ff8e25 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -20,6 +20,7 @@ #include "Data/PokemonBDSP_AutoStory_RoutePlanner.h" #include "Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "Actions/PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP_AutoStory_StoryData_Tests.h" #include "PokemonBDSP_AutoStory_StoryTable.h" #include "PokemonBDSP_AutoStory_StoryTable_Tests.h" @@ -273,6 +274,7 @@ const AutoStoryStoryTableCase AUTO_STORY_STORY_TABLE_CASES[] = { }; void add_tests_AutoStory_StoryTable(UnitTestDatabase& database){ + add_tests_AutoStory_StoryData(database); for (const auto& item : AUTO_STORY_STORY_TABLE_CASES){ database.add(item); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp index 27c100db13..f9843267d5 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -510,7 +510,31 @@ class Test_StorySimulation : public UnitTest{ } // namespace +// Data migration of #226 at 0738cd55b: use the actual JSON-backed table. +UnitTestResult test_authored_mine(Logger&){ + Simulation sim; + const auto report = sim.run({.from = "01.03", .to = "01.03"}); + if (!report.completed || sim.begun() != std::vector{"01.03"} + || sim.count("save in 01.03") != 1 || sim.count("card for 01.03: Oreburgh City") != 1 + || sim.count("01.03/heal_oreburgh") != 1 || sim.count("01.03/roark") != 1){ + return "Expected the JSON-authored mine checkpoint to save once, talk to Roark, heal and finish:" + joined(sim.trace) + joined(sim.lines); + } + return true; +} +UnitTestResult test_authored_mine_stops(Logger&){ + Simulation sim; + sim.game.failures["01.03/out_of_the_mine"] = {.failure = "unconfirmed_transition", .times = 3}; + const auto report = sim.run({.from = "01.03", .to = "01.03"}); + if (report.completed || report.reloads != 2 || report.failed_event != "out_of_the_mine" + || sim.count("01.03/heal_oreburgh") != 0){ + return "Expected failed mine transition to exhaust retries before healing:" + joined(sim.trace) + joined(sim.lines); + } + return true; +} + const AutoStoryStorySimulationCase AUTO_STORY_STORY_SIMULATION_CASES[] = { + {.input = {.name = "01.03 authored mine executes through the real table", .body = test_authored_mine}, .expectation = {}}, + {.input = {.name = "01.03 unconfirmed mine exit stops before healing", .body = test_authored_mine_stops}, .expectation = {}}, { .input = {"00.00 to Floaroma, in order, each row saved first, English and Japanese", test_whole_table}, .expectation = {}, diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md new file mode 100644 index 0000000000..ecf30fcc56 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md @@ -0,0 +1,14 @@ +# BDSP authored checkpoint pilot + +`bdsp-mine-01.03.plan.json` replaces `oreburgh_mine_to_roark()`'s C++ event list in the real story table. It preserves #226 revision `0738cd55b562d1dd685cdea289663a015a1cbe59`: 15 events, with Barry/healing setup already moved into `01.02b`. The same JSON is consumed by PokemonSimulation's `data/story/bdsp-mine-pilot`. + +The common `pokesim-authored-checkpoint/1` envelope contains `checkpoint.id`, `title`, `start`, `events` in that order. BDSP's `execution` extension preserves `on_failure` and `saves_in`; it does not use FRLG's save-menu field or import its supplies. The loader rejects unknown fields/policies and requires a save-card location exactly when saving/retrying. Schemas live in PokemonSimulation. + +Two gates: + +1. Native registered tests exercise the actual JSON-backed table through the existing engine/fake-game simulation, including bounded retry exhaustion before healing. Data tests check all 15 event IDs, policy/card fields and common-record round trip, and reject unsupported inputs. +2. Linux CI checks exact byte equality with a pinned PokemonSimulation revision and runs its nine modeled scenarios, including selected full ordered-action assertions. + +This changes authoring, not game executor behavior. The native fake still uses declared scene/door fallbacks; a passing integration test does not prove detector/controller correctness. Python validates pinned local terrain and bounded wall movement, but requires scripted door/dialogue/scene/heal evidence and explicit wandering-NPC clearance windows. Missing transition links, native robust planner cost parity, analog/queue timing and live recovery remain separate gaps. Python's diagnostic policy is stop/no-save; the native program retains its real start-save/reload policy. + +The source #226 comments record the mine warp-column evidence and lab failures behind the route. Its map resource lock remains unchanged at PokemonSimulation `4de7742a764d27293b4525d6ad2a6fc8a95f3e3a`. No map correction, console run, profile or save is part of this change. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.plan.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.plan.json new file mode 100644 index 0000000000..7a8261607e --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.plan.json @@ -0,0 +1,197 @@ +{ + "format": "pokesim-authored-checkpoint/1", + "game": "bdsp", + "checkpoint": { + "id": "01.03", + "title": "Oreburgh Mine to Roark", + "start": { + "map": "OreburghCity", + "certainty": "set", + "where": {"kind": "tiles", "tiles": [[47, 21], [46, 21]], "facing": "south"} + }, + "events": [ + { + "id": "above_the_mine", + "kind": "route", + "target": { + "map": "OreburghCity", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[46, 58]], "facing": "south"} + }, + "variant": "town", + "why": "down the door's column to above the mine's warp tiles" + }, + { + "id": "into_the_mine", + "kind": "step", + "direction": "south", + "declared_after": { + "map": "OreburghMineB1F", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[12, 1]], "facing": "south"} + }, + "why": "the warp tile (46, 59)" + }, + { + "id": "into_the_coal_room", + "kind": "step", + "direction": "south", + "declared_after": { + "map": "OreburghMine", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[15, 2]], "facing": "south"} + }, + "why": "down the entrance hall through the cave mouth" + }, + { + "id": "south_to_the_rock", + "kind": "legs", + "legs": [{"direction": "south", "tiles": 8}], + "declared_after": { + "map": "OreburghMine", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[15, 9]], "facing": "south"} + }, + "why": "south against the corridor rock (15, 11)", + "variant": "to_wall" + }, + { + "id": "to_roarks_corner", + "kind": "legs", + "legs": [ + {"direction": "east", "tiles": 1}, + {"direction": "south", "tiles": 2}, + {"direction": "west", "tiles": 1}, + {"direction": "south", "tiles": 5}, + {"direction": "east", "tiles": 14}, + {"direction": "south", "tiles": 10}, + {"direction": "west", "tiles": 2}, + {"direction": "south", "tiles": 2}, + {"direction": "west", "tiles": 7} + ], + "declared_after": { + "map": "OreburghMine", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[20, 28]], "facing": "west"} + }, + "why": "east around the rock, across the room, down column 29, west to Roark's rock" + }, + { + "id": "beside_roarks_rock", + "kind": "legs", + "legs": [{"direction": "south", "tiles": 1}], + "declared_after": { + "map": "OreburghMine", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[20, 29]], "facing": "south"} + }, + "why": "south against the room's wall", + "variant": "to_wall" + }, + { + "id": "below_roark", + "kind": "legs", + "legs": [{"direction": "west", "tiles": 2}], + "declared_after": { + "map": "OreburghMine", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[18, 29]], "facing": "north"} + }, + "why": "west 2 to below Roark" + }, + { + "id": "roark", + "kind": "talk", + "why": "Roark, north of us between his rocks" + }, + { + "id": "roark_smashes_the_rock", + "kind": "scene", + "declared_after": { + "map": "OreburghMine", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[18, 29]], "facing": "north"} + }, + "why": "his lines, Rock Smash on the rock east of him, and he walks off" + }, + { + "id": "below_the_coal_rooms_warp", + "kind": "legs", + "legs": [ + {"direction": "east", "tiles": 9}, + {"direction": "north", "tiles": 3}, + {"direction": "east", "tiles": 2}, + {"direction": "north", "tiles": 10}, + {"direction": "west", "tiles": 14}, + {"direction": "north", "tiles": 5}, + {"direction": "east", "tiles": 1}, + {"direction": "north", "tiles": 2}, + {"direction": "west", "tiles": 1}, + {"direction": "north", "tiles": 7} + ], + "declared_after": { + "map": "OreburghMine", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[15, 2]], "facing": "north"} + }, + "why": "east around crate, up column 29, west across row 17, and up corridor to warp doorstep" + }, + { + "id": "up_to_the_hall", + "kind": "step", + "direction": "north", + "declared_after": { + "map": "OreburghMineB1F", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[12, 21]], "facing": "north"} + }, + "why": "the warp tile (15, 1)" + }, + { + "id": "out_of_the_mine", + "kind": "step", + "direction": "north", + "declared_after": { + "map": "OreburghCity", + "certainty": "set", + "where": {"kind": "tiles", "tiles": [[46, 59], [46, 58]], "facing": "north"} + }, + "why": "up the entrance hall and out to the city" + }, + { + "id": "north_to_the_centers_wall", + "kind": "legs", + "legs": [{"direction": "north", "tiles": 37}], + "declared_after": { + "map": "OreburghCity", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[46, 21]], "facing": "north"} + }, + "why": "north up column 46 to the wall west of the Center's door", + "variant": "counted_wall" + }, + { + "id": "east_below_the_centers_door", + "kind": "legs", + "legs": [{"direction": "east", "tiles": 1}], + "declared_after": { + "map": "OreburghCity", + "certainty": "tile", + "where": {"kind": "tiles", "tiles": [[47, 21]], "facing": "north"} + }, + "why": "a tap east below the door" + }, + { + "id": "heal_oreburgh", + "kind": "heal", + "declared_after": { + "map": "OreburghCity", + "certainty": "set", + "where": {"kind": "tiles", "tiles": [[47, 21], [46, 21]], "facing": "south"} + }, + "why": "the nurse; out onto the doorstep, where 01.04 starts" + } + ] + }, + "execution": {"on_failure": "reload_and_retry", "saves_in": "Oreburgh City"} +} diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 437d83cce7..59c9fbe5d4 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1572,6 +1572,10 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -1722,7 +1726,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h From 6330fd09e74dba3de4ee0aaa865edf002836c1c7 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 14:45:48 -0800 Subject: [PATCH 02/10] Pin the BDSP replay schema correction for paired CI --- .github/workflows/cpp-ci-serial-programs-base.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cpp-ci-serial-programs-base.yml b/.github/workflows/cpp-ci-serial-programs-base.yml index ffb7cc4da8..80e03c34fb 100644 --- a/.github/workflows/cpp-ci-serial-programs-base.yml +++ b/.github/workflows/cpp-ci-serial-programs-base.yml @@ -101,7 +101,7 @@ jobs: uses: actions/checkout@v7 with: repository: dayfine/PokemonSimulation - ref: fd2f1ab0facb116c38b3817520eeac0b3d3db8e0 + ref: 678ee80802fec2ae02428429c2e603cd314f20b7 path: AuthoredStorySource - name: Check identical authored mine JSON and replay its scenarios From 4e734d9ba8f67acc0036ab11ec451e77a0f34dc1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 14:55:01 -0800 Subject: [PATCH 03/10] Compare normalized checkpoint semantics instead of optional JSON defaults --- .../AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp index 74683ec3be..bb328d2dbd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp @@ -40,10 +40,11 @@ JsonValue plan(){ } throw ParseException("Expected embedded bdsp-mine-01.03.plan.json."); } -std::string check_table(const StoryCheckpoint& loaded, const JsonValue& input){ +std::string check_table(const StoryCheckpoint& loaded){ const auto& rows = story_checkpoints(); const auto row = std::find_if(rows.begin(), rows.end(), [](const StoryCheckpoint& x){ return x.id == "01.03"; }); - if (row == rows.end() || Pokemon::story_checkpoint_json(*row) != input.to_object_throw().get_value_throw("checkpoint")){ + // The shared writer omits explicit defaults such as direction: north. + if (row == rows.end() || Pokemon::story_checkpoint_json(*row) != Pokemon::story_checkpoint_json(loaded)){ return "Expected the actual 01.03 table entry to contain every authored field."; } if (loaded.on_failure != StoryFailure::RELOAD_AND_RETRY || loaded.saves_in != "Oreburgh City" @@ -75,7 +76,7 @@ class DataTest : public UnitTest{ if (m_test.expectation.rejected){ return "Expected authored data rejection."; } - const std::string error = check_table(loaded, input); + const std::string error = check_table(loaded); return error.empty() ? UnitTestResult(true) : UnitTestResult(error); }catch (const Exception& error){ return m_test.expectation.rejected ? UnitTestResult(true) : UnitTestResult(error.to_str()); From fd5f5b10dd7cee51112aea8d14be685589968363 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 15:53:59 -0800 Subject: [PATCH 04/10] Share authored checkpoint envelopes and embedded resource loading --- .../ComputerPrograms_UnitTestRunner.cpp | 1 + .../Pokemon_AuthoredCheckpointJson.cpp | 67 +++++++++ .../Pokemon_AuthoredCheckpointJson.h | 53 +++++++ .../Pokemon_AuthoredCheckpointJson_Tests.cpp | 139 ++++++++++++++++++ .../AutoStory/Pokemon_AutoStory_Tests.h | 3 + SerialPrograms/cmake/SourceFiles.cmake | 3 + devtools/story-data-tests/CMakeLists.txt | 22 ++- devtools/story-data-tests/README.md | 2 + 8 files changed, 284 insertions(+), 6 deletions(-) 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 diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index 4792436378..df98fc4df2 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_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 1f712fc7ae..5d0bbe1172 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStory_Tests.h @@ -27,6 +27,9 @@ 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/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index dc08917bb4..7be1fec8ae 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1504,6 +1504,9 @@ 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_StoryStepJson.cpp Source/Pokemon/AutoStory/Pokemon_StoryStepJson.h Source/Pokemon/AutoStory/Pokemon_StoryStepJson_Tests.cpp diff --git a/devtools/story-data-tests/CMakeLists.txt b/devtools/story-data-tests/CMakeLists.txt index 9bfc400ae3..978db4f6d0 100644 --- a/devtools/story-data-tests/CMakeLists.txt +++ b/devtools/story-data-tests/CMakeLists.txt @@ -20,14 +20,24 @@ add_executable(StoryDataTests "${ROOT}/Common/Cpp/Filesystem/FileIO.cpp" "${ROOT}/Common/Cpp/Filesystem/FilePath.cpp" "${ROOT}/Common/Cpp/Strings/Unicode.cpp") -target_include_directories(StoryDataTests PRIVATE "${ROOT}" "${ROOT}/SerialPrograms/Source") -target_compile_features(StoryDataTests PRIVATE cxx_std_23) -target_compile_definitions(StoryDataTests PRIVATE PA_STORY_DATA_STANDALONE) +# 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(StoryDataTests PRIVATE _CRT_SECURE_NO_WARNINGS) - target_compile_options(StoryDataTests PRIVATE /W4 /WX) + 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(StoryDataTests PRIVATE -Wall -Wextra -Werror) + 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 index 2c32b00685..310cea3ea3 100644 --- a/devtools/story-data-tests/README.md +++ b/devtools/story-data-tests/README.md @@ -33,3 +33,5 @@ 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 4a4566d2f1328fd5a40b78de3145f34d79800eb0 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 15:56:14 -0800 Subject: [PATCH 05/10] Use shared authored checkpoint loading for BDSP --- .../PokemonBDSP_AutoStory_StoryData.cpp | 46 +++++++++---------- .../Programs/AutoStory/StoryData/README.md | 2 + 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp index 6685a8f14f..6d8baac372 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp @@ -1,22 +1,19 @@ /* BDSP authored story data * From: https://github.com/PokemonAutomation/ */ +#include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonObject.h" -#include "Pokemon/AutoStory/Pokemon_StoryStepJson.h" +#include "Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h" #include "PokemonBDSP_AutoStory_StoryData.h" + namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ + + namespace{ -void exact_fields(const JsonObject& value, std::initializer_list names){ - if (value.size() != names.size()){ - throw ParseException("BDSP authored checkpoint: missing or extra fields."); - } - for (std::string_view name : names){ - value.get_value_throw(std::string(name)); - } -} + StoryFailure read_failure(const std::string& value){ if (value == "reload_and_retry"){ return StoryFailure::RELOAD_AND_RETRY; @@ -26,17 +23,12 @@ StoryFailure read_failure(const std::string& value){ } throw ParseException("Unsupported BDSP failure policy: " + value); } -} // namespace -StoryCheckpoint read_authored_checkpoint(const JsonValue& value, std::string_view source){ - const JsonObject& root = value.to_object_throw(std::string(source)); - exact_fields(root, {"format", "game", "checkpoint", "execution"}); - if (root.get_string_throw("format") != "pokesim-authored-checkpoint/1" || root.get_string_throw("game") != "bdsp"){ - throw ParseException("Unsupported BDSP authored format/game."); - } - const JsonObject& execution = root.get_object_throw("execution"); - exact_fields(execution, {"on_failure", "saves_in"}); + +StoryCheckpoint apply_execution(Pokemon::AuthoredCheckpoint parsed, std::string_view source){ + const JsonObject& execution = parsed.execution; + Pokemon::require_story_json_fields(execution, {"on_failure", "saves_in"}, source); StoryCheckpoint result; - static_cast(result) = Pokemon::read_story_checkpoint(root.get_value_throw("checkpoint"), source); + static_cast(result) = std::move(parsed.checkpoint); result.on_failure = read_failure(execution.get_string_throw("on_failure")); result.saves_in = execution.get_string_throw("saves_in"); if ((result.on_failure == StoryFailure::RELOAD_AND_RETRY) == result.saves_in.empty()){ @@ -44,14 +36,18 @@ StoryCheckpoint read_authored_checkpoint(const JsonValue& value, std::string_vie } return result; } + +} // namespace + +StoryCheckpoint read_authored_checkpoint(const JsonValue& value, std::string_view source){ + return apply_execution(Pokemon::read_authored_checkpoint(value, "bdsp", source), source); +} + StoryCheckpoint load_authored_checkpoint(std::string_view file){ - for (const auto& [name, text] : bdsp_checkpoint_json()){ - if (name == file){ - return read_authored_checkpoint(parse_json(text), file); - } - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "BDSP authored checkpoint is not embedded: " + std::string(file)); + return apply_execution(Pokemon::load_authored_checkpoint(bdsp_checkpoint_json(), file, "bdsp"), file); } + + } // namespace PokemonBDSP } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md index ecf30fcc56..509a83fb1f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md @@ -12,3 +12,5 @@ Two gates: This changes authoring, not game executor behavior. The native fake still uses declared scene/door fallbacks; a passing integration test does not prove detector/controller correctness. Python validates pinned local terrain and bounded wall movement, but requires scripted door/dialogue/scene/heal evidence and explicit wandering-NPC clearance windows. Missing transition links, native robust planner cost parity, analog/queue timing and live recovery remain separate gaps. Python's diagnostic policy is stop/no-save; the native program retains its real start-save/reload policy. The source #226 comments record the mine warp-column evidence and lab failures behind the route. Its map resource lock remains unchanged at PokemonSimulation `4de7742a764d27293b4525d6ad2a6fc8a95f3e3a`. No map correction, console run, profile or save is part of this change. + +The common authored envelope, common checkpoint parsing, and embedded-resource lookup live in `Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.*`. The BDSP adapter validates only its own `on_failure` and `saves_in` execution settings and retains native bookkeeping defaults. From 146ef0d9b294cc8027037881aa2385b6feeef233 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 16:20:47 -0800 Subject: [PATCH 06/10] Make the native mine plan authoritative and address data review --- .gitattributes | 4 +- .../workflows/cpp-ci-serial-programs-base.yml | 28 +++++--- SerialPrograms/CMakeLists.txt | 11 ++- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 2 + .../PokemonBDSP_AutoStory_StoryData.cpp | 9 ++- .../PokemonBDSP_AutoStory_StoryData.h | 9 ++- .../PokemonBDSP_AutoStory_StoryData_Tests.cpp | 67 ++++++++++++++++--- .../PokemonBDSP_AutoStory_StoryData_Tests.h | 13 +++- ...PokemonBDSP_AutoStory_StoryTable_Tests.cpp | 2 - ...onBDSP_AutoStory_StorySimulation_Tests.cpp | 3 +- .../Programs/AutoStory/StoryData/README.md | 16 ++--- .../AutoStory/StoryData/bdsp-mine-01.03.md | 41 ++++++++++++ SerialPrograms/cmake/SourceFiles.cmake | 3 +- 13 files changed, 169 insertions(+), 39 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.md diff --git a/.gitattributes b/.gitattributes index 5378fe089b..2d01ba1796 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,3 @@ -* -text \ No newline at end of file +* -text +# Authored story data uses LF; native C++ retains repository CRLF conventions. +SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/*.json text eol=lf diff --git a/.github/workflows/cpp-ci-serial-programs-base.yml b/.github/workflows/cpp-ci-serial-programs-base.yml index 80e03c34fb..db1dd54b99 100644 --- a/.github/workflows/cpp-ci-serial-programs-base.yml +++ b/.github/workflows/cpp-ci-serial-programs-base.yml @@ -101,29 +101,39 @@ jobs: uses: actions/checkout@v7 with: repository: dayfine/PokemonSimulation - ref: 678ee80802fec2ae02428429c2e603cd314f20b7 + ref: cf365e1722dea03fcf40c322b910332fcc864ea4 path: AuthoredStorySource - - name: Check identical authored mine JSON and replay its scenarios + - name: Replay the authoritative Arduino mine plan if: startsWith(inputs.os, 'ubuntu') && inputs.run-tests env: PYTHONPATH: AuthoredStorySource/src run: | python3 - <<'PYTHON' from pathlib import Path + import hashlib, json, tempfile from pokesim.story.replay_binding import load_binding from pokesim.story.replay_cases import read_cases, check_case from pokesim.story.checkpoint_replay import run source = Path('AuthoredStorySource') folder = source / 'data/story/bdsp-mine-pilot' native = Path('Arduino-Source/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData') - assert (native / 'bdsp-mine-01.03.plan.json').read_bytes() == (folder / 'bdsp-mine-01.03.plan.json').read_bytes() - binding = load_binding(folder / 'binding.json', source) - cases = read_cases(folder / 'cases.json') - for case in cases: - check_case(case, run(binding, case['input'])) - print(case['id'] + ': authored BDSP JSON replay passed') - assert len(cases) == 9 + # The Arduino plan is authoritative; bind these exact bytes explicitly. + # The simulator's checked-in plan is only a frozen regression snapshot. + with tempfile.TemporaryDirectory(prefix='.native-story-', dir=source) as temporary: + stage = Path(temporary) + raw = (native / 'bdsp-mine-01.03.plan.json').read_bytes() + (stage / 'plan.json').write_bytes(raw) + profile = json.loads((folder / 'binding.json').read_text()) + profile['plan'] = {'file': str((stage / 'plan.json').relative_to(source)), + 'sha256': hashlib.sha256(raw).hexdigest()} + (stage / 'binding.json').write_text(json.dumps(profile)) + binding = load_binding(stage / 'binding.json', source) + cases = read_cases(folder / 'cases.json') + for case in cases: + check_case(case, run(binding, case['input'])) + print(case['id'] + ': current Arduino plan replay passed') + assert len(cases) == 9 PYTHON - name: Install Qt diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index cb1055d8e3..bc1398e46d 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -240,11 +240,20 @@ pa_pokesim_map_directory(BDSP_MAP_DIRECTORY bdsp "${POKESIM_CONSUMER}" "${POKESI file(RELATIVE_PATH BDSP_MAP_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}" "${BDSP_MAP_DIRECTORY}") pa_embed_text_files(SerialProgramsLib "${BDSP_MAP_RESOURCES}" PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h PokemonAutomation::NintendoSwitch::PokemonBDSP bdsp_map_json) +# Catch JSON syntax errors during a lab build, before static table initialization. +# Native tests still validate the schema, game policy and execution semantics. +file(GLOB BDSP_STORY_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/PokemonBDSP/Programs/AutoStory/StoryData/*.json") +foreach(BDSP_STORY_FILE IN LISTS BDSP_STORY_FILES) + file(READ "${BDSP_STORY_FILE}" BDSP_STORY_TEXT) + string(JSON BDSP_STORY_TYPE ERROR_VARIABLE BDSP_STORY_ERROR TYPE "${BDSP_STORY_TEXT}") + if(BDSP_STORY_ERROR OR NOT BDSP_STORY_TYPE STREQUAL "OBJECT") + message(FATAL_ERROR "Invalid authored checkpoint ${BDSP_STORY_FILE}: ${BDSP_STORY_ERROR}") + endif() +endforeach() pa_embed_text_files(SerialProgramsLib Source/PokemonBDSP/Programs/AutoStory/StoryData PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h PokemonAutomation::NintendoSwitch::PokemonBDSP bdsp_checkpoint_json) - # Create binary target if (APPLE) set(SerialPrograms_ICON ${CMAKE_CURRENT_SOURCE_DIR}/../IconResource/icon.icns) diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index 72c29a2fa1..40be88c76f 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -16,6 +16,7 @@ #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.h" #include "Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySim_Tests.h" #include "Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h" #include "PokemonBDSP_Tests.h" @@ -34,6 +35,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_StoryMaps(database); add_tests_AutoStory_Segment00(database); add_tests_AutoStory_StorySimulation(database); + add_tests_AutoStory_StoryData(database); add_tests_AutoStory_StoryTable(database); add_tests_AutoStory_Walk(database); add_tests_AutoStory_PlannedRoute(database); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp index 6d8baac372..8fecf57098 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.cpp @@ -1,6 +1,9 @@ -/* BDSP authored story data +/* BDSP Authored Checkpoint Data + * * From: https://github.com/PokemonAutomation/ + * */ + #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonObject.h" @@ -14,14 +17,14 @@ namespace PokemonBDSP{ namespace{ -StoryFailure read_failure(const std::string& value){ +StoryFailure read_failure(std::string_view value){ if (value == "reload_and_retry"){ return StoryFailure::RELOAD_AND_RETRY; } if (value == "stop"){ return StoryFailure::STOP; } - throw ParseException("Unsupported BDSP failure policy: " + value); + throw ParseException("Unsupported BDSP failure policy: " + std::string(value)); } StoryCheckpoint apply_execution(Pokemon::AuthoredCheckpoint parsed, std::string_view source){ diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h index f08c58cfc7..5ae6018f85 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h @@ -1,17 +1,24 @@ -/* BDSP authored story data +/* BDSP Authored Checkpoint Data + * * From: https://github.com/PokemonAutomation/ + * */ + #ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryData_H #define PokemonAutomation_PokemonBDSP_AutoStory_StoryData_H + #include #include #include #include #include "Common/Cpp/Json/JsonValue.h" #include "PokemonBDSP_AutoStory_StoryExecutor.h" + namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ + + // Strict authored envelope and BDSP save/failure configuration; no controller IO. // Unknown fields/values or inconsistent save policy throw ParseException. StoryCheckpoint read_authored_checkpoint(const JsonValue& value, std::string_view source); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp index bb328d2dbd..088eeaccc6 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp @@ -1,37 +1,53 @@ -/* BDSP authored checkpoint tests +/* BDSP Authored Checkpoint Tests + * * From: https://github.com/PokemonAutomation/ + * */ + #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonArray.h" #include "Common/Cpp/Json/JsonObject.h" #include "Pokemon/AutoStory/Pokemon_StoryStepJson.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" #include "PokemonBDSP_AutoStory_StoryData.h" #include "PokemonBDSP_AutoStory_StoryTable.h" #include "PokemonBDSP_AutoStory_StoryData_Tests.h" + namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ + + namespace{ + struct Input{ std::string_view name; void (*mutate)(JsonObject&) = nullptr; }; -struct Expectation{ bool rejected = false; }; -struct Case{ Input input; Expectation expectation; }; + +struct Expectation{ + bool rejected = false; +}; +struct Case{ + Input input; + Expectation expectation; +}; void extra(JsonObject& root){ root["unknown"] = true; } void wrong_game(JsonObject& root){ root["game"] = "frlg"; } void wrong_policy(JsonObject& root){ root.get_object_throw("execution")["on_failure"] = "guess"; } void missing_card(JsonObject& root){ root.get_object_throw("execution")["saves_in"] = ""; } void stop_with_save(JsonObject& root){ root.get_object_throw("execution")["on_failure"] = "stop"; } const Case CASES[] = { - {.input = {.name = "authored mine row replaces the table entry"}, .expectation = {}}, + {.input = {.name = "mine resource is registered with ordered events and save policy"}, .expectation = {}}, {.input = {.name = "extra envelope field", .mutate = extra}, .expectation = {.rejected = true}}, {.input = {.name = "wrong game", .mutate = wrong_game}, .expectation = {.rejected = true}}, {.input = {.name = "unknown failure policy", .mutate = wrong_policy}, .expectation = {.rejected = true}}, {.input = {.name = "missing save-card location", .mutate = missing_card}, .expectation = {.rejected = true}}, {.input = {.name = "stop policy must not claim a start save", .mutate = stop_with_save}, .expectation = {.rejected = true}}, }; + JsonValue plan(){ for (const auto& [name, text] : bdsp_checkpoint_json()){ if (name == "bdsp-mine-01.03.plan"){ @@ -40,12 +56,38 @@ JsonValue plan(){ } throw ParseException("Expected embedded bdsp-mine-01.03.plan.json."); } -std::string check_table(const StoryCheckpoint& loaded){ + +// Literal authored poses must stay aligned with the map-derived anchors +// used by adjacent checkpoints; a map correction must trigger plan review. +std::string check_map_anchors(const StoryCheckpoint& loaded){ + const std::vector doorstep{ + {47, 21}, map_point("OreburghCity", "pokemon_center_doorstep") + }; + if (!loaded.start || Pokemon::tiles_of(*loaded.start) != doorstep){ + return "Expected the authored start to match the Center exit alternatives."; + } + const std::vector below_roark{ + map_point("OreburghMine", "roark").next(Pokemon::Direction::SOUTH) + }; + for (const auto& step : loaded.steps){ + const bool roark = step.name == "below_roark" || step.name == "roark_smashes_the_rock"; + if (roark || step.name == "heal_oreburgh"){ + const auto& expected = roark ? below_roark : doorstep; + if (!step.pose_after || Pokemon::tiles_of(*step.pose_after) != expected){ + return "Expected map-derived arrival anchors for " + step.name; + } + } + } + return ""; +} + +std::string check_registration(const StoryCheckpoint& loaded){ const auto& rows = story_checkpoints(); const auto row = std::find_if(rows.begin(), rows.end(), [](const StoryCheckpoint& x){ return x.id == "01.03"; }); - // The shared writer omits explicit defaults such as direction: north. + // Wiring check only: both values originate in the authored resource. + // Route correctness is covered by separate scenario expectations. if (row == rows.end() || Pokemon::story_checkpoint_json(*row) != Pokemon::story_checkpoint_json(loaded)){ - return "Expected the actual 01.03 table entry to contain every authored field."; + return "Expected the embedded mine resource to be registered as checkpoint 01.03."; } if (loaded.on_failure != StoryFailure::RELOAD_AND_RETRY || loaded.saves_in != "Oreburgh City" || row->on_failure != loaded.on_failure || row->saves_in != loaded.saves_in){ @@ -61,8 +103,12 @@ std::string check_table(const StoryCheckpoint& loaded){ for (const auto& step : loaded.steps){ actual.push_back(step.name); } - return actual == expected ? "" : "Expected all 15 current #226 mine events in order."; + if (actual != expected){ + return "Expected all 15 mine event IDs in order."; + } + return check_map_anchors(loaded); } + class DataTest : public UnitTest{ public: explicit DataTest(Case test) : UnitTest("PokemonBDSP::StoryData - " + std::string(test.input.name)), m_test(test){} @@ -76,7 +122,7 @@ class DataTest : public UnitTest{ if (m_test.expectation.rejected){ return "Expected authored data rejection."; } - const std::string error = check_table(loaded); + const std::string error = check_registration(loaded); return error.empty() ? UnitTestResult(true) : UnitTestResult(error); }catch (const Exception& error){ return m_test.expectation.rejected ? UnitTestResult(true) : UnitTestResult(error.to_str()); @@ -85,12 +131,15 @@ class DataTest : public UnitTest{ private: Case m_test; }; + } // namespace + void add_tests_AutoStory_StoryData(UnitTestDatabase& database){ for (const Case& test : CASES){ database.add(test); } } + } // namespace PokemonBDSP } // namespace NintendoSwitch } // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h index 49ec762169..2b81e594e7 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.h @@ -1,11 +1,20 @@ -/* Authored checkpoint tests */ +/* BDSP Authored Checkpoint Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + #ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryData_Tests_H #define PokemonAutomation_PokemonBDSP_AutoStory_StoryData_Tests_H + #include "Common/Cpp/TestRunners/UnitTestDatabase.h" + namespace PokemonAutomation{ namespace NintendoSwitch{ namespace PokemonBDSP{ -// Register strict authored-data and actual table integration cases. + + +// Register strict authored-data validation and table-wiring cases. void add_tests_AutoStory_StoryData(UnitTestDatabase& database); } // namespace PokemonBDSP } // namespace NintendoSwitch diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp index 44a6ff8e25..c16b365d7f 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp @@ -20,7 +20,6 @@ #include "Data/PokemonBDSP_AutoStory_RoutePlanner.h" #include "Data/PokemonBDSP_AutoStory_StoryMaps.h" #include "Actions/PokemonBDSP_AutoStory_Events.h" -#include "PokemonBDSP_AutoStory_StoryData_Tests.h" #include "PokemonBDSP_AutoStory_StoryTable.h" #include "PokemonBDSP_AutoStory_StoryTable_Tests.h" @@ -274,7 +273,6 @@ const AutoStoryStoryTableCase AUTO_STORY_STORY_TABLE_CASES[] = { }; void add_tests_AutoStory_StoryTable(UnitTestDatabase& database){ - add_tests_AutoStory_StoryData(database); for (const auto& item : AUTO_STORY_STORY_TABLE_CASES){ database.add(item); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp index f9843267d5..16de02bb7c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Simulation/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp @@ -510,7 +510,7 @@ class Test_StorySimulation : public UnitTest{ } // namespace -// Data migration of #226 at 0738cd55b: use the actual JSON-backed table. +// Execute the authored mine resource through the actual table and shared engine. UnitTestResult test_authored_mine(Logger&){ Simulation sim; const auto report = sim.run({.from = "01.03", .to = "01.03"}); @@ -521,6 +521,7 @@ UnitTestResult test_authored_mine(Logger&){ } return true; } + UnitTestResult test_authored_mine_stops(Logger&){ Simulation sim; sim.game.failures["01.03/out_of_the_mine"] = {.failure = "unconfirmed_transition", .times = 3}; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md index 509a83fb1f..aba628e82a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md @@ -1,16 +1,16 @@ # BDSP authored checkpoint pilot -`bdsp-mine-01.03.plan.json` replaces `oreburgh_mine_to_roark()`'s C++ event list in the real story table. It preserves #226 revision `0738cd55b562d1dd685cdea289663a015a1cbe59`: 15 events, with Barry/healing setup already moved into `01.02b`. The same JSON is consumed by PokemonSimulation's `data/story/bdsp-mine-pilot`. +`bdsp-mine-01.03.plan.json` is the authoritative definition of the mine/Roark checkpoint. `oreburgh_mine_to_roark()` loads it into the existing story table. Edit this file here when changing the route; PokemonSimulation consumes an explicitly pinned Arduino revision for independent analysis. Its checked-in pilot plan is a frozen regression snapshot, not a second authoring source. -The common `pokesim-authored-checkpoint/1` envelope contains `checkpoint.id`, `title`, `start`, `events` in that order. BDSP's `execution` extension preserves `on_failure` and `saves_in`; it does not use FRLG's save-menu field or import its supplies. The loader rejects unknown fields/policies and requires a save-card location exactly when saving/retrying. Schemas live in PokemonSimulation. +The shared `pokesim-authored-checkpoint/1` envelope contains `checkpoint.id`, `title`, `start`, `events` in that order. BDSP validates `execution.on_failure` and `execution.saves_in`. The common envelope and resource lookup live in `Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.*`. Bookkeeping defaults and game executors are unchanged. -Two gates: +## Validation and iteration -1. Native registered tests exercise the actual JSON-backed table through the existing engine/fake-game simulation, including bounded retry exhaustion before healing. Data tests check all 15 event IDs, policy/card fields and common-record round trip, and reject unsupported inputs. -2. Linux CI checks exact byte equality with a pinned PokemonSimulation revision and runs its nine modeled scenarios, including selected full ordered-action assertions. +1. Native registered tests run the actual JSON-backed table through the existing engine/fake game, including retry exhaustion before healing. Data tests check parsing, table wiring, event IDs/order, policy/card values, and map-derived Center/Roark anchors. The table-wiring comparison is not an independent check against the removed C++ authoring list. +2. Linux CI pins a simulator revision reachable from its main branch and replays **this checkout's JSON** against nine independent scenario expectations. There is no byte-equality requirement against a second plan. A route change may still require revising a scenario expectation when its expected behavior intentionally changes; such failures are meaningful regression feedback, not a requirement to mirror JSON or repin simulator code for every edit. -This changes authoring, not game executor behavior. The native fake still uses declared scene/door fallbacks; a passing integration test does not prove detector/controller correctness. Python validates pinned local terrain and bounded wall movement, but requires scripted door/dialogue/scene/heal evidence and explicit wandering-NPC clearance windows. Missing transition links, native robust planner cost parity, analog/queue timing and live recovery remain separate gaps. Python's diagnostic policy is stop/no-save; the native program retains its real start-save/reload policy. +A lab route edit therefore touches the Arduino plan and applicable tests/evidence. The next build re-embeds it automatically. CMake rejects malformed JSON syntax before launching the app; native tests perform full reader/policy validation. Run tests before deploying: CMake's syntax preflight alone cannot validate all semantics. Authored JSON uses LF; C++ uses CRLF. Whitespace changes do not affect replay results. -The source #226 comments record the mine warp-column evidence and lab failures behind the route. Its map resource lock remains unchanged at PokemonSimulation `4de7742a764d27293b4525d6ad2a6fc8a95f3e3a`. No map correction, console run, profile or save is part of this change. +Coordinates remain literal data tied to the pinned map. The map-anchor test catches drift in the Center exit and Roark approach; it does not validate every tile or replace map review. See [the checkpoint evidence](bdsp-mine-01.03.md) for the warp columns, counted hall movement, wall choices and unresolved observations. -The common authored envelope, common checkpoint parsing, and embedded-resource lookup live in `Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.*`. The BDSP adapter validates only its own `on_failure` and `saves_in` execution settings and retains native bookkeeping defaults. +The native fake uses declared scene/door fallbacks and is not a detector/timing oracle. Python uses scripted transition/dialogue/scene/heal evidence and NPC-clearance windows. Missing warp links, native planner-cost parity, queued movement and live recovery remain gaps. Python's diagnostic policy is stop/no-save; the native program retains its start-save/reload policy. No map correction, console, profile or save is part of this authoring migration. diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.md b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.md new file mode 100644 index 0000000000..efe2adc205 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/bdsp-mine-01.03.md @@ -0,0 +1,41 @@ +# Mine/Roark checkpoint: route evidence + +This sidecar preserves the reasoning that previously accompanied the C++ row. The JSON defines current actions; these observations explain why they were chosen. They are historical evidence, not new console verification. Entry assumes the preceding Barry conversation and Center heal have completed. + +## Entry and Center alignment + +The Center door is `(47,20)`, so its approach is `(47,21)` facing north. The published `pokemon_center_doorstep` is `(46,21)`: a route there hit the wall west of the door and healing found no door (lab-B, 2026-11-07 20:18). Both `(47,21)` and `(46,21)` facing south remain exit alternatives. Earlier Gym alignment runs r217d/r218d inferred column 46 from twenty west taps; they did not independently establish where those taps began. + +`above_the_mine` goes to `(46,58)` facing south. The city's five warp tiles `(44..48,59)` map into the hall at `(11,1),(11,1),(12,1),(13,1),(13,1)`. Thus column 46 enters B1F at `(12,1)`. The mine hall is Diamond's B1F; the coal room below is the room with Roark. Diamond zone-event sources were narc_0044/narc_0197; Platinum's event files provide the corresponding warp layout, used as evidence for BDSP's retained DP layout. + +## Into the coal room + +`into_the_mine` and `into_the_coal_room` cross the cave mouths. Hall movement uses counted taps rather than an open hold toward a fade: a wild battle on a hold can be mistaken for the transition. Warp entry itself is expected on the subsequent step. These are executor assumptions that still need detector/interruption coverage. + +The hall's coal-room mouth is `(11..13,21)`; column 12 enters the coal room at `(15,2)`. `south_to_the_rock` clips the corridor rock before the grouped inbound legs. The script's anchor is `(15,9)` facing south; the rock was described at `(15,11)`. + +## Roark approach and talk + +`to_roarks_corner` groups all nine inbound legs through `(20,28)` facing west, rather than ending recovery at an open intermediate tile. Lab-B, 2026-11-08 11:55: recovery replanned only to `(15,16)`, a subsequent south-five leg overshot a row, and later taps ran under Roark's rocks. Grouping through a corner allows a replanned path to finish with wall clips. The offline interrupted-leg case deliberately stops with uncertainty; it does not prove live recovery/rejoin. + +`beside_roarks_rock` clips the room's south wall to `(20,29)`, then `below_roark` moves west two to `(18,29)` facing north. Lab-B, 2026-11-07: an open counted south-three leg moved only two, motivating the wall anchor. Roark's map point is `(18,28)`; native tests check the authored below-Roark pose against that map point. + +`roark` initiates the conversation. Barry's earlier message that Roark went to the mine is a prerequisite: Roark ignored A without it in r101/r104. `roark_smashes_the_rock` represents the lines, Rock Smash east of him, and his departure east, consistent with Platinum's mine B2F script. The declared return pose is an executor fallback, not an observed receipt proving the scene completed. + +## Back through the hall + +`below_the_coal_rooms_warp` contains the ten outbound legs: east around the crate, north along column 29, west across row 17, then up the corridor to `(15,2)` facing north. Entering `(15,1)` returns to the hall at `(12,21)`, not `(11,21)`. + +`up_to_the_hall` and `out_of_the_mine` continue up column 12. The hall's `(12,1)` exit leads to city `(46,59)`. The city belief also permits `(46,58)` if movement continued past the fade. Lab-B, 2026-11-08 09:46, 10:39 and 12:03: the exit was east of the sign `(45,56)` and northward movement did not stop there. At 13:50, a north tap and east wall hold matched the map only from column 46. + +## Return to the nurse + +`north_to_the_centers_wall` clips the wall at `(46,20)` from either exit tile: 37 tiles from row 58 or 38 from row 59. The Hiker wanders through columns 46..48 and rows 25..27; the live walker must wait or recover safely, and the offline model uses an explicit synthetic clearance window. + +This column avoids the street east of the mine. At 13:50, an east hold from `(48,57)` moved zero although the map showed `(49,57)` open; later movement went astray. That discrepancy remains unresolved and is not grounds to change terrain silently. + +`east_below_the_centers_door` moves one tile east to `(47,21)` facing north. `heal_oreburgh` enters, heals and exits to the two-tile doorstep belief. The next Gym checkpoint starts there. The checkpoint keeps save-at-start/reload-and-retry guarded by the Oreburgh City save card. + +## Updating this route + +Edit the authoritative JSON, update relevant native/scenario expectations, and preserve new run evidence here. A changed map pin requires anchor review. A claimed transition or recovery fix needs an independent observation/regression case; do not turn `declared_after` into proof of success. The initial migration was checked against the pre-migration row and the reported successful lab-B sequence of 2026-11-08 14:17–14:23; later changes must record their own evidence. diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index ab4d91f5fc..804d80b04e 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1729,8 +1729,7 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.cpp Source/PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_MenuCursorDetector.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h - Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.cpp + Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.cpp Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h From dbdfbcc8f1f7bdf7eeabb333805bccf79a4195de Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 16:23:47 -0800 Subject: [PATCH 07/10] Space BDSP data declarations and test mutation helpers --- .../PokemonBDSP_AutoStory_StoryData.h | 2 ++ .../PokemonBDSP_AutoStory_StoryData_Tests.cpp | 25 +++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h index 5ae6018f85..ef56b77bca 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData.h @@ -22,8 +22,10 @@ namespace PokemonBDSP{ // Strict authored envelope and BDSP save/failure configuration; no controller IO. // Unknown fields/values or inconsistent save policy throw ParseException. StoryCheckpoint read_authored_checkpoint(const JsonValue& value, std::string_view source); + // The embedded authored JSON files, keyed by filename without .json. const std::vector>& bdsp_checkpoint_json(); + // Load an embedded authored checkpoint; a missing file throws InternalProgramError. StoryCheckpoint load_authored_checkpoint(std::string_view file); } // namespace PokemonBDSP diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp index 088eeaccc6..17be60c7b2 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp @@ -34,11 +34,26 @@ struct Case{ Input input; Expectation expectation; }; -void extra(JsonObject& root){ root["unknown"] = true; } -void wrong_game(JsonObject& root){ root["game"] = "frlg"; } -void wrong_policy(JsonObject& root){ root.get_object_throw("execution")["on_failure"] = "guess"; } -void missing_card(JsonObject& root){ root.get_object_throw("execution")["saves_in"] = ""; } -void stop_with_save(JsonObject& root){ root.get_object_throw("execution")["on_failure"] = "stop"; } +void extra(JsonObject& root){ + root["unknown"] = true; +} + +void wrong_game(JsonObject& root){ + root["game"] = "frlg"; +} + +void wrong_policy(JsonObject& root){ + root.get_object_throw("execution")["on_failure"] = "guess"; +} + +void missing_card(JsonObject& root){ + root.get_object_throw("execution")["saves_in"] = ""; +} + +void stop_with_save(JsonObject& root){ + root.get_object_throw("execution")["on_failure"] = "stop"; +} + const Case CASES[] = { {.input = {.name = "mine resource is registered with ordered events and save policy"}, .expectation = {}}, {.input = {.name = "extra envelope field", .mutate = extra}, .expectation = {.rejected = true}}, From 973d6edc4d72120b59d278512a03026f3a6b8495 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 16:24:46 -0800 Subject: [PATCH 08/10] Resolve simulator staging root across Python versions --- .github/workflows/cpp-ci-serial-programs-base.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cpp-ci-serial-programs-base.yml b/.github/workflows/cpp-ci-serial-programs-base.yml index db1dd54b99..40452d1067 100644 --- a/.github/workflows/cpp-ci-serial-programs-base.yml +++ b/.github/workflows/cpp-ci-serial-programs-base.yml @@ -115,7 +115,7 @@ jobs: from pokesim.story.replay_binding import load_binding from pokesim.story.replay_cases import read_cases, check_case from pokesim.story.checkpoint_replay import run - source = Path('AuthoredStorySource') + source = Path('AuthoredStorySource').resolve() folder = source / 'data/story/bdsp-mine-pilot' native = Path('Arduino-Source/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData') # The Arduino plan is authoritative; bind these exact bytes explicitly. From 3aad41001ad0cf74c63325674a57028d84d7fc62 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 16:25:45 -0800 Subject: [PATCH 09/10] Keep existing Git normalization rules for authored story data --- .gitattributes | 4 +--- .../Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.gitattributes b/.gitattributes index 2d01ba1796..5378fe089b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1 @@ -* -text -# Authored story data uses LF; native C++ retains repository CRLF conventions. -SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/*.json text eol=lf +* -text \ No newline at end of file diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md index aba628e82a..cf77edffe0 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/StoryData/README.md @@ -9,7 +9,7 @@ The shared `pokesim-authored-checkpoint/1` envelope contains `checkpoint.id`, `t 1. Native registered tests run the actual JSON-backed table through the existing engine/fake game, including retry exhaustion before healing. Data tests check parsing, table wiring, event IDs/order, policy/card values, and map-derived Center/Roark anchors. The table-wiring comparison is not an independent check against the removed C++ authoring list. 2. Linux CI pins a simulator revision reachable from its main branch and replays **this checkout's JSON** against nine independent scenario expectations. There is no byte-equality requirement against a second plan. A route change may still require revising a scenario expectation when its expected behavior intentionally changes; such failures are meaningful regression feedback, not a requirement to mirror JSON or repin simulator code for every edit. -A lab route edit therefore touches the Arduino plan and applicable tests/evidence. The next build re-embeds it automatically. CMake rejects malformed JSON syntax before launching the app; native tests perform full reader/policy validation. Run tests before deploying: CMake's syntax preflight alone cannot validate all semantics. Authored JSON uses LF; C++ uses CRLF. Whitespace changes do not affect replay results. +A lab route edit therefore touches the Arduino plan and applicable tests/evidence. The next build re-embeds it automatically. CMake rejects malformed JSON syntax before launching the app; native tests perform full reader/policy validation. Run tests before deploying: CMake's syntax preflight alone cannot validate all semantics. The shared formatter writes authored JSON; Git normalization rules are unchanged. Whitespace and line-ending changes do not affect replay results. C++ retains the repository CRLF convention. Coordinates remain literal data tied to the pinned map. The map-anchor test catches drift in the Center exit and Roark approach; it does not validate every tile or replace map review. See [the checkpoint evidence](bdsp-mine-01.03.md) for the warp columns, counted hall movement, wall choices and unresolved observations. From e866b5b87a34f5b1aa0790573d3eed8d85230555 Mon Sep 17 00:00:00 2001 From: dayfine Date: Sun, 8 Nov 2026 16:34:06 -0800 Subject: [PATCH 10/10] Report actual BDSP story data values in test failures --- SerialPrograms/CMakeLists.txt | 2 + .../PokemonBDSP_AutoStory_StoryData_Tests.cpp | 51 ++++++++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/CMakeLists.txt b/SerialPrograms/CMakeLists.txt index bc1398e46d..bc51b4daf2 100644 --- a/SerialPrograms/CMakeLists.txt +++ b/SerialPrograms/CMakeLists.txt @@ -240,6 +240,8 @@ pa_pokesim_map_directory(BDSP_MAP_DIRECTORY bdsp "${POKESIM_CONSUMER}" "${POKESI file(RELATIVE_PATH BDSP_MAP_RESOURCES "${CMAKE_CURRENT_SOURCE_DIR}" "${BDSP_MAP_DIRECTORY}") pa_embed_text_files(SerialProgramsLib "${BDSP_MAP_RESOURCES}" PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_Maps.h PokemonAutomation::NintendoSwitch::PokemonBDSP bdsp_map_json) +# TODO: Move JSON syntax preflight into pa_embed_text_files so map and story +# resources share it. Keep game/schema/semantic checks in their native tests. # Catch JSON syntax errors during a lab build, before static table initialization. # Native tests still validate the schema, game policy and execution semantics. file(GLOB BDSP_STORY_FILES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/PokemonBDSP/Programs/AutoStory/StoryData/*.json") diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp index 17be60c7b2..98c70e6f7a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp @@ -5,6 +5,7 @@ */ #include +#include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/Json/JsonArray.h" #include "Common/Cpp/Json/JsonObject.h" @@ -63,13 +64,36 @@ const Case CASES[] = { {.input = {.name = "stop policy must not claim a start save", .mutate = stop_with_save}, .expectation = {.rejected = true}}, }; +std::string string_list(const std::vector& values){ + JsonArray result; + for (const std::string& value : values){ + result.push_back(value); + } + return result.dump(-1); +} + +std::string tile_list(const std::vector& tiles){ + std::vector values; + for (const auto& tile : tiles){ + values.push_back(Pokemon::to_string(tile)); + } + return string_list(values); +} + +std::string policy(const StoryCheckpoint& row){ + return std::format("{}; saves_in=\"{}\"", + row.on_failure == StoryFailure::RELOAD_AND_RETRY ? "reload_and_retry" : "stop", row.saves_in); +} + JsonValue plan(){ + std::vector names; for (const auto& [name, text] : bdsp_checkpoint_json()){ + names.push_back(name); if (name == "bdsp-mine-01.03.plan"){ return parse_json(text); } } - throw ParseException("Expected embedded bdsp-mine-01.03.plan.json."); + throw ParseException("Expected embedded bdsp-mine-01.03.plan.json; read resource names " + string_list(names)); } // Literal authored poses must stay aligned with the map-derived anchors @@ -79,7 +103,8 @@ std::string check_map_anchors(const StoryCheckpoint& loaded){ {47, 21}, map_point("OreburghCity", "pokemon_center_doorstep") }; if (!loaded.start || Pokemon::tiles_of(*loaded.start) != doorstep){ - return "Expected the authored start to match the Center exit alternatives."; + return "Expected Center exit tiles " + tile_list(doorstep) + "; read " + + (loaded.start ? Pokemon::story_pose_json(*loaded.start).dump(-1) : "absent start"); } const std::vector below_roark{ map_point("OreburghMine", "roark").next(Pokemon::Direction::SOUTH) @@ -89,7 +114,8 @@ std::string check_map_anchors(const StoryCheckpoint& loaded){ if (roark || step.name == "heal_oreburgh"){ const auto& expected = roark ? below_roark : doorstep; if (!step.pose_after || Pokemon::tiles_of(*step.pose_after) != expected){ - return "Expected map-derived arrival anchors for " + step.name; + return "Expected " + step.name + " arrival tiles " + tile_list(expected) + "; read " + + (step.pose_after ? Pokemon::story_pose_json(*step.pose_after).dump(-1) : "absent arrival"); } } } @@ -101,12 +127,21 @@ std::string check_registration(const StoryCheckpoint& loaded){ const auto row = std::find_if(rows.begin(), rows.end(), [](const StoryCheckpoint& x){ return x.id == "01.03"; }); // Wiring check only: both values originate in the authored resource. // Route correctness is covered by separate scenario expectations. - if (row == rows.end() || Pokemon::story_checkpoint_json(*row) != Pokemon::story_checkpoint_json(loaded)){ - return "Expected the embedded mine resource to be registered as checkpoint 01.03."; + if (row == rows.end()){ + std::vector ids; + for (const auto& entry : rows){ + ids.push_back(entry.id); + } + return "Expected registered checkpoint 01.03; read checkpoint IDs " + string_list(ids); + } + if (Pokemon::story_checkpoint_json(*row) != Pokemon::story_checkpoint_json(loaded)){ + return "Expected registered resource " + Pokemon::story_checkpoint_json(loaded).dump(-1) + + "; read table record " + Pokemon::story_checkpoint_json(*row).dump(-1); } if (loaded.on_failure != StoryFailure::RELOAD_AND_RETRY || loaded.saves_in != "Oreburgh City" || row->on_failure != loaded.on_failure || row->saves_in != loaded.saves_in){ - return "Expected reload-and-retry with Oreburgh City save-card guard."; + return "Expected reload_and_retry; saves_in=\"Oreburgh City\" for resource and table; read resource " + + policy(loaded) + "; table " + policy(*row); } const std::vector expected{ "above_the_mine", "into_the_mine", "into_the_coal_room", "south_to_the_rock", @@ -119,7 +154,7 @@ std::string check_registration(const StoryCheckpoint& loaded){ actual.push_back(step.name); } if (actual != expected){ - return "Expected all 15 mine event IDs in order."; + return "Expected event IDs " + string_list(expected) + "; read " + string_list(actual); } return check_map_anchors(loaded); } @@ -135,7 +170,7 @@ class DataTest : public UnitTest{ try{ const StoryCheckpoint loaded = read_authored_checkpoint(input, m_test.input.name); if (m_test.expectation.rejected){ - return "Expected authored data rejection."; + return "Expected authored data rejection; read accepted record " + Pokemon::story_checkpoint_json(loaded).dump(-1); } const std::string error = check_registration(loaded); return error.empty() ? UnitTestResult(true) : UnitTestResult(error);