Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
426de16
PokemonFRLG: Load an exported checkpoint record back into a story row
dayfine Nov 8, 2026
d27f17b
Merge branch 'frlg/c1-story-loader' into frlg/checkpoint-replay
dayfine Nov 8, 2026
6c9f710
Merge remote-tracking branch 'origin/frlg/stage7-celadon' into frlg/c…
dayfine Nov 8, 2026
eb8ed7e
Move unchanged overworld route formatting out of the planner implemen…
dayfine Nov 8, 2026
4f2629d
Replay shared FRLG Parcel scenarios through the native checkpoint eng…
dayfine Nov 8, 2026
26256c7
Document Parcel JSON equivalence checks and migration lifetime
dayfine Nov 8, 2026
4a3da90
Merge remote-tracking branch 'origin/frlg/checkpoint-json' into frlg/…
dayfine Nov 8, 2026
9458752
Order Parcel JSON for readers and distinguish snapshot from requirements
dayfine Nov 8, 2026
9fd7971
Merge remote-tracking branch 'origin/frlg/checkpoint-json' into frlg/…
dayfine Nov 8, 2026
f530278
Refresh replay preview pins after readable Parcel JSON formatting
dayfine Nov 8, 2026
f5fb083
Load Parcel story table entry from authored JSON and replay it in bot…
dayfine Nov 8, 2026
41ab2a2
Merge remote-tracking branch 'origin/frlg/checkpoint-json' into frlg/…
dayfine Nov 8, 2026
518b449
Merge Parcel JSON loader and shared codec into FRLG replay diagnostic
dayfine Nov 9, 2026
9d875ff
Merge frlg/stage7-celadon into frlg/checkpoint-replay
dayfine Nov 9, 2026
b319e0d
Common: test that an empty JSON object survives dump and parse
dayfine Nov 9, 2026
e321063
Common: dump an empty JsonObject as {} instead of null
dayfine Nov 9, 2026
2c47614
Merge fix/json-empty-object-dump into frlg/checkpoint-replay
dayfine Nov 9, 2026
bca87ad
FRLG replay: report a failed entry check as entry_requirement_failed
dayfine Nov 9, 2026
6ebd110
Common: table the JSON round-trip test cases
dayfine Nov 9, 2026
2edfd9e
Merge fix/json-empty-object-dump into frlg/checkpoint-replay
dayfine Nov 9, 2026
0e7f70c
Merge frlg/stage7-celadon (story action table) into frlg/checkpoint-r…
dayfine Nov 9, 2026
f335b40
FRLG StoryReplay: run ReplayExecutor through the story action table
dayfine Nov 9, 2026
a262eaf
Merge frlg/stage7-celadon into frlg/checkpoint-replay
dayfine Nov 9, 2026
18c5bb9
FRLG StoryReplay: a ReplayExecutor can't be copied
dayfine Nov 9, 2026
049fbc5
Merge frlg/stage7-celadon: check the save stands at the checkpoint's …
dayfine Nov 9, 2026
b996daf
Merge frlg/stage7-celadon: 00.27 finds its place on walls on the way …
dayfine Nov 9, 2026
3a4fc81
Merge frlg/stage7-celadon: place a wild battle seen after an open leg…
dayfine Nov 9, 2026
7b8b2f7
Merge frlg/stage7-celadon: run the sim through the action table
dayfine Nov 9, 2026
7a29dae
Merge frlg/stage7-celadon: check a loaded save's start against a kept…
dayfine Nov 9, 2026
fe9ebf8
Merge eb2dfc828 (#122 via parent) into frlg/checkpoint-replay
dayfine Nov 9, 2026
2bbd354
Merge FRLG between-step interrupts (cf8535ef0) into #258
dayfine Nov 9, 2026
ff6ed6b
Merge FRLG between-step interrupts (3052e09a4) into #258
dayfine Nov 9, 2026
0e94586
Merge FRLG sim interrupts (802099ee9) into #258
dayfine Nov 9, 2026
1e49850
FRLG story replay: clear interrupts between steps
dayfine Nov 9, 2026
ff007a3
Merge FRLG interrupt actions move (9eab4d76f) into #258
dayfine Nov 9, 2026
8930848
FRLG story replay: a flatter interrupt screen table
dayfine Nov 9, 2026
44d0fcf
Merge frlg/stage7-celadon into frlg/checkpoint-replay (hold watchers …
dayfine Nov 9, 2026
8cabfdf
Merge frlg/stage7-celadon into frlg/checkpoint-replay
dayfine Nov 9, 2026
740398a
Merge frlg/stage7-celadon into frlg/checkpoint-replay (wait for a bat…
dayfine Nov 9, 2026
806a540
Merge frlg/stage7-celadon into frlg/checkpoint-replay (scene ends aft…
dayfine Nov 9, 2026
583905b
Merge frlg/stage7-celadon into frlg/checkpoint-replay: a step on the …
dayfine Nov 9, 2026
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
89 changes: 88 additions & 1 deletion .github/workflows/cpp-ci-serial-programs-base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,50 @@ jobs:
operation: prepare
consumer-directory: Arduino-Source

- name: Checkout preview replay fixture producer (not a resource adoption)
if: startsWith(inputs.os, 'ubuntu') && inputs.run-tests
uses: actions/checkout@v7
with:
repository: dayfine/PokemonSimulation
ref: 9cb12c52f5f055299e32c2a7cf43e7103c101e3e
path: StoryReplaySource

- name: Verify replay preview bytes and independently regenerate its route input
if: startsWith(inputs.os, 'ubuntu') && inputs.run-tests
env:
PYTHONPATH: StoryReplaySource/src
run: |
python3 - <<'PYTHON'
import hashlib, json, subprocess
from pathlib import Path
from pokesim.story.replay_binding import load_binding
from pokesim.story.checkpoint_replay import run
source = Path('StoryReplaySource')
native = Path('Arduino-Source/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory')
folder = native / 'ReplayData'
pin = json.loads((folder / 'pin.json').read_text())
revision = subprocess.check_output(['git', '-C', str(source), 'rev-parse', 'HEAD']).decode().strip()
assert pin['status'] == 'preview-unmerged-PR-73' and revision == pin['revision']
for name, entry in pin['files'].items():
published = (source / entry['path']).read_bytes()
copied = (native / 'StoryData/frlg-parcel-00.05.json' if name == 'capture' else folder / (name + '.json')).read_bytes()
assert hashlib.sha256(published).hexdigest() == entry['sha256'], name
assert copied == published, name
binding = load_binding(source / pin['files']['binding']['path'], source)
cases = json.loads((source / pin['files']['cases']['path']).read_text())['cases']
scenario = next(c['input'] for c in cases if c['id'] == 'parcel-all-events')
actual = run(binding, scenario) # No expectations are consulted to generate the candidate.
assert actual['status'] == 'model_complete'
candidate = {'format': 'pokesim-route-candidate/1', 'binding_sha256': binding.profile_sha256,
'checkpoint': binding.checkpoint['id'], 'routes': {
e['id']: [a for a in actual['actions'] if a['event'] == e['id']]
for e in binding.checkpoint['events'] if e['kind'] == 'route'}}
raw = (folder / 'route-candidate.json').read_bytes()
assert hashlib.sha256(raw).hexdigest() == pin['candidate']['sha256']
assert json.loads(raw) == candidate
print('Preview source bytes and independently generated candidate match.')
PYTHON

- name: Checkout authored checkpoint simulator
if: startsWith(inputs.os, 'ubuntu') && inputs.run-tests
uses: actions/checkout@v7
Expand Down Expand Up @@ -253,7 +297,7 @@ jobs:
cd Arduino-Source/SerialPrograms
mkdir bin
cd bin
cmake .. -DQT_MAJOR:STRING=6 -DPA_FRLG_STORY_EXPORT=ON ${{env.CMAKE_ADDITIONAL_FLAGS}}
cmake .. -DQT_MAJOR:STRING=6 -DPA_FRLG_STORY_EXPORT=ON -DPA_FRLG_STORY_REPLAY=ON ${{env.CMAKE_ADDITIONAL_FLAGS}}
# By default Ninja reads each compile's .d dependency file into its own database, then deletes it.
# To keep them for the downstream clang-query cache (see .github/scripts/clang-query-cached.py), add
# -d keepdepfile
Expand All @@ -279,6 +323,49 @@ jobs:
name: frlg-checkpoint-${{inputs.os}}-${{inputs.compiler}}
path: Arduino-Source/SerialPrograms/bin/frlg-parcel.native.json

- name: Run compiled native replay and compare shared semantic scenarios (Ubuntu)
if: startsWith(inputs.os, 'ubuntu') && inputs.run-tests
env:
PYTHONPATH: StoryReplaySource/src
run: |
Arduino-Source/SerialPrograms/bin/StoryCheckpointReplay > Arduino-Source/SerialPrograms/bin/frlg-parcel.replay.json
python3 - <<'PYTHON'
import json, subprocess
from pathlib import Path
from pokesim.story.replay_binding import load_binding
from pokesim.story.checkpoint_replay import run
from pokesim.story.replay_cases import read_cases, check_case
source = Path('StoryReplaySource')
data = json.loads(Path('Arduino-Source/SerialPrograms/bin/frlg-parcel.replay.json').read_text())
revision = subprocess.check_output(['git', '-C', 'Arduino-Source', 'rev-parse', 'HEAD']).decode().strip()
assert data['native_execution_revision'] == revision
cases = read_cases(source / 'data/story/parcel-pilot/cases.json')
reports = {item['id']: item['report'] for item in data['reports']}
assert len(reports) == len(data['reports']) == len(cases)
assert set(reports) == {case['id'] for case in cases}
binding = load_binding(source / 'data/story/parcel-pilot/binding.json', source)
for case in cases:
native = reports[case['id']]
python = run(binding, case['input'])
check_case(case, native)
check_case(case, python)
for field in case['expectation']:
assert native[field] == python[field], (case['id'], field)
if case['id'] == 'parcel-all-events':
control = native['native_timing_controls']['below_mart_door']
assert control['status'] == 'complete', control
assert any('from_save' in line for line in native['engine_log'])
print(case['id'] + ': shared diagnostic outcomes/actions match; live executor parity remains unclaimed.')
PYTHON

- name: Upload native replay trace
if: always() && startsWith(inputs.os, 'ubuntu') && inputs.run-tests
uses: actions/upload-artifact@v7
with:
name: frlg-native-replay-${{inputs.os}}-${{inputs.compiler}}
path: Arduino-Source/SerialPrograms/bin/frlg-parcel.replay.json
if-no-files-found: ignore

- name: Prepare upload build
if: inputs.upload-build
shell: bash
Expand Down
2 changes: 1 addition & 1 deletion Common/Cpp/Json/JsonTools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ nlohmann::json to_nlohmann(const JsonValue& json){
return ret;
}
if (json.is_object()){
nlohmann::json ret;
nlohmann::json::object_t ret;
for (const auto& item : *json.to_object()){
ret[item.first] = to_nlohmann(item.second);
}
Expand Down
77 changes: 77 additions & 0 deletions Common/Cpp/Json/JsonTools_Tests.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* JSON Tools Tests
*
* From: https://github.com/PokemonAutomation/
*
*/

#include <format>
#include <string>
#include <string_view>
#include "Common/Cpp/TestRunners/UnitTestDatabase.h"
#include "JsonValue.h"
#include "JsonTools_Tests.h"

namespace PokemonAutomation{
namespace{



struct DumpInput{
std::string_view name;
std::string_view text;
};
struct DumpExpectation{
std::string_view dumped;
};
struct DumpCase{
DumpInput input;
DumpExpectation expectation;
};

// Empty containers must keep their type through dump() and parse_json().
const DumpCase CASES[] = {
{.input = {.name = "Empty object", .text = "{}"}, .expectation = {.dumped = "{}"}},
{.input = {.name = "Nested empty object", .text = R"({"object":{}})"}, .expectation = {.dumped = R"({"object":{}})"}},
{.input = {.name = "Nested empty array", .text = R"({"array":[]})"}, .expectation = {.dumped = R"({"array":[]})"}},
};



class Test_JsonTools_DumpRoundTrip : public UnitTest{
public:
explicit Test_JsonTools_DumpRoundTrip(DumpCase test)
: UnitTest(std::format("Common::JsonTools - {} round trip", test.input.name))
, m_test(test)
{}

virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{
const std::string dumped = parse_json(std::string(m_test.input.text)).dump(-1);
if (dumped != m_test.expectation.dumped){
return UnitTestResult(std::format("dump: expected {}, got {}", m_test.expectation.dumped, dumped));
}
const std::string again = parse_json(dumped).dump(-1);
if (again != dumped){
return UnitTestResult(std::format("dump after reparse: expected {}, got {}", dumped, again));
}
return true;
}

private:
DumpCase m_test;
};



} // namespace



void add_tests_JsonTools(UnitTestDatabase& database){
for (const DumpCase& test : CASES){
database.add<Test_JsonTools_DumpRoundTrip>(test);
}
}



} // namespace PokemonAutomation
22 changes: 22 additions & 0 deletions Common/Cpp/Json/JsonTools_Tests.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* JSON Tools Tests
*
* From: https://github.com/PokemonAutomation/
*
*/

#ifndef PokemonAutomation_JsonTools_Tests_H
#define PokemonAutomation_JsonTools_Tests_H

#include "Common/Cpp/TestRunners/UnitTestDatabase.h"

namespace PokemonAutomation{



// Register the JSON dump/parse round-trip tests.
void add_tests_JsonTools(UnitTestDatabase& database);



} // namespace PokemonAutomation
#endif
4 changes: 4 additions & 0 deletions SerialPrograms/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ pa_embed_text_files(SerialProgramsLib Source/VideoGames/PokemonFRLG/Programs/Aut
VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData.h PokemonAutomation::NintendoSwitch::PokemonFRLG autostory_battle_json)
pa_embed_text_files(SerialProgramsLib Source/VideoGames/PokemonFRLG/Programs/AutoStory/StoryData
VideoGames/PokemonFRLG/Programs/AutoStory/Data/JsonParser.h PokemonAutomation::NintendoSwitch::PokemonFRLG autostory_checkpoint_json)
pa_embed_text_files(SerialProgramsLib Source/VideoGames/PokemonFRLG/Programs/AutoStory/ReplayData
VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_StoryReplay.h PokemonAutomation::NintendoSwitch::PokemonFRLG story_replay_fixture_json)


# Create binary target
Expand Down Expand Up @@ -930,3 +932,5 @@ if(PA_PYTHON_BINDINGS)
# Python bindings (GUI-free) from subdirectory
include(Source/PythonBindings/PythonBindings.cmake)
endif()

include(../devtools/story-replay/FRLG.cmake)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*
*/

#include "Common/Cpp/Json/JsonTools_Tests.h"
#include "Common/Cpp/ListenerSet.h"
#include "Common/Cpp/MoveOnlyFunction_Tests.h"
#include "Common/Cpp/ScopeExit.h"
Expand Down Expand Up @@ -53,13 +54,14 @@



UnitTestDatabase make_UNIT_TESTS_ALL(){

Check warning on line 57 in SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

PokemonAutomation::ComputerPrograms::make_UNIT_TESTS_ALL(): function_lines=50 (advisory above 25, limit 50)
UnitTestDatabase ret;

add_tests_QtEventThreadPool(ret, GlobalThreadPools::computation_normal());
add_tests_Sockets(ret, GlobalThreadPools::unlimited_normal());
add_tests_BlackBorderDetector(ret);
add_tests_ListenerSet(ret);
add_tests_JsonTools(ret);
add_tests_MoveOnlyFunction(ret);
SysbotBase::add_tests_SysbotBasePing(ret);
SysbotBase::add_tests_SysbotBaseReceiveLine(ret);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* Overworld Route

Check warning on line 1 in SerialPrograms/Source/VideoGames/PokemonFRLG/Overworld/PokemonFRLG_OverworldRoute.cpp

View workflow job for this annotation

GitHub Actions / code-health

Code health (advisory)

<file>: file_lines=995 (advisory above 500, limit 1000)
*
* From: https://github.com/PokemonAutomation/
*
Expand All @@ -20,34 +20,6 @@
namespace PokemonFRLG{


std::string to_string(const MapPosition& position){
return position.map + " " + to_string(position.point);
}

std::string to_string(const RouteStep& step){
std::string text;
switch (step.action){
case RouteAction::WALK:
text = std::format("walk {} {}", direction_name(step.direction), step.tiles);
if (step.warps){
text += " (warp)";
}
if (step.to_wall){
text += " (to wall)";
}
break;
case RouteAction::SURF:
text = "surf " + std::string(direction_name(step.direction));
break;
case RouteAction::FACE:
text = "face " + std::string(direction_name(step.direction));
break;
}
return text + " -> " + to_string(step.end);
}



const RouteCosts& default_route_costs(){
static const RouteCosts COSTS{
{RouteCost::TILE, 1},
Expand Down Expand Up @@ -372,6 +344,29 @@



std::optional<OverworldPress> predict_overworld_press(
const std::vector<MapData>& maps, const OverworldPose& from, Direction direction
){
const int map = find_map(maps, from.position.map);
if (map < 0){
return std::nullopt;
}
const Located start{map, {from.position.point, from.elevation, from.surfing}};
const auto next = press(maps, start, direction);
if (!next){
return std::nullopt;
}
return OverworldPress{
.pose = {
.position = {maps[next->next.map].name, next->next.state.position},
.elevation = next->next.state.elevation,
.surfing = next->next.state.surfing,
.facing = next->walked_out ? Direction::SOUTH : direction,
},
.warped = next->warped,
};
}

std::optional<std::vector<RouteStep>> plan_overworld_route(
const std::vector<MapData>& maps,
const MapPosition& start,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,23 @@ enum class RouteAction{
FACE, // turn to `direction` without moving
};

// A stationary prediction in the map's runtime-local tile/elevation frame.
struct OverworldPose{
MapPosition position;
uint8_t elevation = ELEVATION_GROUND;
bool surfing = false;
Direction facing = Direction::NORTH;
};
// One transition from the production planner's own collision/warp implementation.
struct OverworldPress{
OverworldPose pose;
bool warped = false;
};
// A model prediction only; nullopt means no known move, never confirmed localization.
std::optional<OverworldPress> predict_overworld_press(
const std::vector<MapData>& maps, const OverworldPose& from, Direction direction
);

// A single step along an overworld path.
struct RouteStep{
RouteAction action = RouteAction::WALK;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* Overworld Route Formatting
* From: https://github.com/PokemonAutomation/
*/
#include <format>
#include "PokemonFRLG_OverworldRoute.h"
namespace PokemonAutomation{
namespace NintendoSwitch{
namespace PokemonFRLG{
std::string to_string(const MapPosition& position){
return position.map + " " + to_string(position.point);
}

std::string to_string(const RouteStep& step){
std::string text;
switch (step.action){
case RouteAction::WALK:
text = std::format("walk {} {}", direction_name(step.direction), step.tiles);
if (step.warps){
text += " (warp)";
}
if (step.to_wall){
text += " (to wall)";
}
break;
case RouteAction::SURF:
text = "surf " + std::string(direction_name(step.direction));
break;
case RouteAction::FACE:
text = "face " + std::string(direction_name(step.direction));
break;
}
return text + " -> " + to_string(step.end);
}



} // namespace PokemonFRLG
} // namespace NintendoSwitch
} // namespace PokemonAutomation
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "JsonParser.h"
#include "JsonWriter.h"
#include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h"
#include "VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_StoryReplay.h"
#include "Json_Tests.h"

namespace PokemonAutomation{
Expand Down Expand Up @@ -269,6 +270,7 @@ class LoadTest : public UnitTest{
} // namespace

void add_tests_AutoStoryJson(UnitTestDatabase& database){
add_tests_StoryReplay(database);
for (const ExportCase& test : CASES){
database.add<ExportTest>(test);
}
Expand Down
Loading
Loading