Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/cpp-ci-serial-programs-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,46 @@ 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: cf365e1722dea03fcf40c322b910332fcc864ea4
path: AuthoredStorySource

- 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').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.
# 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
uses: jurplel/install-qt-action@v4
with:
Expand Down
14 changes: 14 additions & 0 deletions SerialPrograms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +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)
# 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")
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
Expand Down
2 changes: 2 additions & 0 deletions SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* BDSP Recovery Walk Tests

Check warning on line 1 in SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_RecoveryWalk_Tests.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

<file>: file_lines=679 (advisory above 500, limit 1000)
*
* From: https://github.com/PokemonAutomation/
*
Expand All @@ -11,6 +11,7 @@
#include <format>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "Common/Cpp/Exceptions.h"
#include "Pokemon/AutoStory/Pokemon_TilePose.h"
Expand Down Expand Up @@ -217,15 +218,15 @@
GridSim& m_sim;
};

const Pokemon::StoryStep& story_step(const std::string& row, const std::string& name){
const Pokemon::StoryStep& story_step(std::string_view row, std::string_view name){
for (const Pokemon::StoryCheckpoint& checkpoint : story_checkpoints()){
for (const Pokemon::StoryStep& step : checkpoint.steps){
if (checkpoint.id == row && step.name == name){
return step;
}
}
}
throw GridSimFailure{"no step " + name + " in " + row};
throw GridSimFailure{std::format("no step {} in {}", name, row)};
}

// lab-B 2026-11-08 11:55: a wild battle on 01.03's counted taps across
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* BDSP Authored Checkpoint Data
*
* From: https://github.com/PokemonAutomation/
*
*/

#include <utility>
#include "Common/Cpp/Exceptions.h"
#include "Common/Cpp/Json/JsonObject.h"
#include "Pokemon/AutoStory/Pokemon_AuthoredCheckpointJson.h"
#include "PokemonBDSP_AutoStory_StoryData.h"

namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonBDSP{


namespace{

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: " + std::string(value));
}

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<Pokemon::StoryCheckpoint&>(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()){
throw ParseException("BDSP save-card location is required exactly when the checkpoint saves/retries.");
}
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){
return apply_execution(Pokemon::load_authored_checkpoint(bdsp_checkpoint_json(), file, "bdsp"), file);
}


} // namespace PokemonBDSP
} // namespace NintendoSwitch
} // namespace PokemonAutomation
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* BDSP Authored Checkpoint Data
*
* From: https://github.com/PokemonAutomation/
*
*/

#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryData_H
#define PokemonAutomation_PokemonBDSP_AutoStory_StoryData_H

#include <string>
#include <string_view>
#include <utility>
#include <vector>
#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<std::pair<std::string, std::string>>& 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
/* BDSP Authored Checkpoint Tests
*
* From: https://github.com/PokemonAutomation/
*
*/

#include <algorithm>
#include <format>
#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;
};
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}},
{.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}},
};

std::string string_list(const std::vector<std::string>& values){
JsonArray result;
for (const std::string& value : values){
result.push_back(value);
}
return result.dump(-1);
}

std::string tile_list(const std::vector<Pokemon::TilePos>& tiles){
std::vector<std::string> 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<std::string> 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; read resource names " + string_list(names));
}

// 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<Pokemon::TilePos> doorstep{
{47, 21}, map_point("OreburghCity", "pokemon_center_doorstep")
};
if (!loaded.start || Pokemon::tiles_of(*loaded.start) != doorstep){
return "Expected Center exit tiles " + tile_list(doorstep) + "; read "
+ (loaded.start ? Pokemon::story_pose_json(*loaded.start).dump(-1) : "absent start");
}
const std::vector<Pokemon::TilePos> 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 " + step.name + " arrival tiles " + tile_list(expected) + "; read "
+ (step.pose_after ? Pokemon::story_pose_json(*step.pose_after).dump(-1) : "absent arrival");
}
}
}
return "";
}

std::string check_registration(const StoryCheckpoint& loaded){

Check warning on line 125 in SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryData_Tests.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::NintendoSwitch::PokemonBDSP::check_registration( const StoryCheckpoint & loaded): function_lines=36 (advisory above 25, limit 50)
const auto& rows = story_checkpoints();
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()){
std::vector<std::string> 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; saves_in=\"Oreburgh City\" for resource and table; read resource "
+ policy(loaded) + "; table " + policy(*row);
}
const std::vector<std::string> 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<std::string> actual;
for (const auto& step : loaded.steps){
actual.push_back(step.name);
}
if (actual != expected){
return "Expected event IDs " + string_list(expected) + "; read " + string_list(actual);
}
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){}
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; read accepted record " + Pokemon::story_checkpoint_json(loaded).dump(-1);
}
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());
}
}
private:
Case m_test;
};

} // namespace

void add_tests_AutoStory_StoryData(UnitTestDatabase& database){
for (const Case& test : CASES){
database.add<DataTest>(test);
}
}

} // namespace PokemonBDSP
} // namespace NintendoSwitch
} // namespace PokemonAutomation
Loading
Loading