From 445e9530038b5f805b6c3d6cf39a3bbea55e3093 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 02:03:16 -0800 Subject: [PATCH 1/6] BDSP AutoStory sim: a grid simulation of the walker, and story rows on it GridSim moves a player on the story maps as measured on the console: short D-pad taps only turn, 160 ms steps a tile, holds walk to the wall, a stick tap moves part of a tile and the D-pad keeps that offset, doors warp only from their column, people block and trainers' sight lines stop the walk. GridSimRouteIO runs the route walker on it. The executor's walking steps (ROUTE, LEGS, STEP, TALK, MASH) move into StoryWalkSteps on a StoryStepIO, so the story sim tests run rows 00.04 to 00.05c with the program's own steps on the simulation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Source/PokemonBDSP/PokemonBDSP_Tests.cpp | 4 + .../PokemonBDSP_AutoStory_GridSim_Tests.cpp | 418 ++++++++++++++++++ .../PokemonBDSP_AutoStory_GridSim_Tests.h | 28 ++ .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 274 +----------- .../PokemonBDSP_AutoStory_StoryExecutor.h | 15 +- .../PokemonBDSP_AutoStory_StorySim_Tests.cpp | 392 ++++++++++++++++ .../PokemonBDSP_AutoStory_StorySim_Tests.h | 28 ++ .../PokemonBDSP_AutoStory_StoryStepIO.cpp | 146 ++++++ .../PokemonBDSP_AutoStory_StoryStepIO.h | 78 ++++ .../PokemonBDSP_AutoStory_StoryWalkSteps.cpp | 250 +++++++++++ .../PokemonBDSP_AutoStory_StoryWalkSteps.h | 71 +++ .../Utils/PokemonBDSP_AutoStory_GridSim.cpp | 361 +++++++++++++++ .../Utils/PokemonBDSP_AutoStory_GridSim.h | 181 ++++++++ SerialPrograms/cmake/SourceFiles.cmake | 10 + 14 files changed, 1984 insertions(+), 272 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h diff --git a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp index c4f3752ab1..621d4421ef 100644 --- a/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/PokemonBDSP_Tests.cpp @@ -6,12 +6,14 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Actions_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Segment00_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryWorld_Tests.h" +#include "Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h" #include "Programs/AutoStory/PokemonBDSP_AutoStory_Walk_Tests.h" @@ -34,6 +36,8 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory_StoryTable(database); add_tests_AutoStory_Walk(database); add_tests_AutoStory_PlannedRoute(database); + add_tests_AutoStory_GridSim(database); + add_tests_AutoStory_StorySim(database); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp new file mode 100644 index 0000000000..8d5d49b517 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp @@ -0,0 +1,418 @@ +/* BDSP AutoStory Grid Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The grid simulation's moves against the console's measurements, then the + * route walker (walk_route()) on it: every route of the story table from + * where the table has the player, and the routes to doors that a turn on + * the stick missed on the console (r49, r57). + * + */ + +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Data/PokemonBDSP_AutoStory_PlanCosts.h" +#include "Utils/PokemonBDSP_AutoStory_GridSim.h" +#include "Utils/PokemonBDSP_AutoStory_PlannedRoute.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_GridSim_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Pose; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +StoryWorld make_world(const std::vector& rows){ + Pokemon::MapData map; + map.name = "Test"; + map.tiles = Pokemon::Grid{(int)rows[0].size(), (int)rows.size(), {}}; + for (const std::string& row : rows){ + for (char glyph : row){ + map.tiles.cells.push_back(*Pokemon::tile_from_glyph(glyph)); + } + } + StoryWorld world; + world.add("Test", map, {0, 0}); + return world; +} + +SimPos at_tile(int x, int y, int part_x = 0, int part_y = 0){ + return {x * SIM_TILE + part_x, y * SIM_TILE + part_y}; +} + + +// One input to the simulation. +enum class SimInputKind{ + DPAD, + STICK, + HOLD, +}; +struct SimInput{ + SimInputKind kind; + Direction direction; + Milliseconds time; +}; + +// Inputs from `start`, and where the player must end and how the last +// input ended. +struct MoveCase{ + std::string name; + TilePos start; + std::vector inputs; + SimPos end; + SimStop stop; +}; + +SimMove apply(GridSim& sim, const SimInput& input){ + switch (input.kind){ + case SimInputKind::DPAD: + return sim.tap(input.direction, input.time, false); + case SimInputKind::STICK: + return sim.tap(input.direction, input.time, true); + case SimInputKind::HOLD: + return sim.hold(input.direction, input.time, SimPace::WALK); + } + return {}; +} + +// A room with a door in row 3 at x 3, a ledge south at x 7, someone +// standing at (5, 1), and a trainer east of row 2 who looks west 3 tiles. +UnitTestResult test_moves(Logger&){ + StoryWorld world = make_world({ + "#########", + "#.......#", + "#.......#", + "###D###v#", + "#.......#", + "#########", + }); + world.block({5, 1}); + const SimWorld setup{.world = &world, .trainers = {{.name = "watcher", .pos = {8, 2}, .facing = Direction::WEST, .range = 3}}}; + constexpr Direction EAST = Direction::EAST; + constexpr Direction SOUTH = Direction::SOUTH; + constexpr Direction WEST = Direction::WEST; + const std::vector cases{ + {.name = "a 50 ms D-pad tap only turns", .start = {2, 1}, .inputs = {{SimInputKind::DPAD, EAST, 50ms}}, .end = at_tile(2, 1), .stop = SimStop::DONE}, + {.name = "a 160 ms D-pad tap steps a tile", .start = {2, 1}, .inputs = {{SimInputKind::DPAD, EAST, 160ms}}, .end = at_tile(3, 1), .stop = SimStop::DONE}, + {.name = "a 100 ms stick tap steps 0.6 tile across", .start = {2, 1}, .inputs = {{SimInputKind::STICK, EAST, 100ms}}, .end = at_tile(2, 1, 625), .stop = SimStop::DONE}, + {.name = "a 100 ms stick tap steps 0.4 tile down", .start = {2, 1}, .inputs = {{SimInputKind::STICK, SOUTH, 100ms}}, .end = at_tile(2, 1, 0, 416), .stop = SimStop::DONE}, + {.name = "the D-pad keeps a stick tap's offset", .start = {2, 1}, + .inputs = {{SimInputKind::STICK, EAST, 100ms}, {SimInputKind::DPAD, EAST, 160ms}}, .end = at_tile(3, 1, 625), .stop = SimStop::DONE}, + {.name = "a held walk stops at someone standing", .start = {2, 1}, .inputs = {{SimInputKind::HOLD, EAST, 5000ms}}, .end = at_tile(4, 1), .stop = SimStop::BLOCKED}, + {.name = "a door warps from its column", .start = {3, 2}, .inputs = {{SimInputKind::HOLD, SOUTH, 3000ms}}, .end = at_tile(3, 3), .stop = SimStop::WARP}, + {.name = "part of a tile off the door's column, the frame stops us", .start = {3, 2}, + .inputs = {{SimInputKind::STICK, WEST, 100ms}, {SimInputKind::HOLD, SOUTH, 3000ms}}, .end = at_tile(2, 2, 375), .stop = SimStop::BLOCKED}, + {.name = "a trainer's sight stops a walk", .start = {1, 2}, .inputs = {{SimInputKind::HOLD, EAST, 5000ms}}, .end = at_tile(5, 2), .stop = SimStop::DIALOG}, + {.name = "a ledge hops two tiles", .start = {7, 2}, .inputs = {{SimInputKind::DPAD, SOUTH, 160ms}}, .end = at_tile(7, 4), .stop = SimStop::DONE}, + }; + for (const MoveCase& item : cases){ + GridSim sim(setup, item.start, Direction::NORTH, false); + SimMove last; + for (const SimInput& input : item.inputs){ + last = apply(sim, input); + } + if (sim.player().pos != item.end || last.stop != item.stop){ + return std::format("{}: expected {} ({}), got {} ({}).", item.name, to_string(item.end), sim_stop_name(item.stop), + to_string(sim.player().pos), sim_stop_name(last.stop)); + } + } + return true; +} + + +// A turn on the stick, as before r49: each turn tap of the D-pad becomes a +// 100 ms stick tap, which moves the player part of a tile. +class StickTurnIO : public RouteIO{ +public: + explicit StickTurnIO(RouteIO& io) + : m_io(io) + {} + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) override{ + return m_io.walk_to_wall(walk); + } + virtual WalkResult walk_taps(Direction direction, size_t tiles, const std::string& label) override{ + return m_io.walk_taps(direction, tiles, label); + } + virtual void press(const RouteInput& input) override{ + if (input.kind != RouteInputKind::DPAD || input.hold >= 100ms){ + m_io.press(input); + return; + } + RouteInput stick = input; + stick.kind = RouteInputKind::STICK; + stick.hold = 100ms; + m_io.press(stick); + } + virtual bool dialog_within(Milliseconds wait, Color color) override{ + return m_io.dialog_within(wait, color); + } + virtual bool holding_against_wall(Direction direction, const std::string& label) override{ + return m_io.holding_against_wall(direction, label); + } + virtual void look(const std::function& at) override{ + m_io.look(at); + } + virtual void note(const std::string& text, RouteNote kind) override{ + m_io.note(text, kind); + } + +private: + RouteIO& m_io; +}; + + +// A route walked on the simulation: where the walker believes it ended, +// where the player is, and why it failed. +struct SimWalk{ + std::vector believed; + SimPos actual; + std::string failure; + std::vector notes; +}; + +// One route to walk: on `world`, from `from` (the walker's belief) with the +// player on `start`, to any of `targets`. +struct SimRoute{ + std::string label; + const StoryWorld* world; + std::vector from; + TilePos start; + std::vector targets; + RobustPlanCosts costs; + bool can_run; +}; + +SimWalk walk_on_sim(const SimRoute& route, bool stick_turns){ + SimWalk out; + GridSim sim({.world = route.world, .trainers = story_trainers(*route.world)}, route.start, Direction::NORTH, route.can_run); + GridSimRouteIO sim_io(sim, out.notes); + StickTurnIO stick_io(sim_io); + RouteIO& io = stick_turns ? static_cast(stick_io) : sim_io; + RouteTrainers trainers{story_trainers(*route.world), [&sim](const std::string&){ sim.end_dialogue(); }}; + const std::vector& targets = route.targets; + try{ + out.believed = walk_route(io, *route.world, route.from, trainers, { + .goal = [&targets](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, + .label = route.label, + .avoid = {}, + .costs = route.costs, + .speed = route.can_run ? MoveSpeed::RUN : MoveSpeed::WALK, + }); + }catch (const GridSimFailure& e){ + out.failure = e.message; + } + out.actual = sim.player().pos; + return out; +} + +// Why a walk did not end where it should: on the grid, on one of the +// targets, and on a tile the walker believes. Empty if it did. +std::string walk_error(const SimRoute& route, const SimWalk& walk){ + const auto has = [](const std::vector& tiles, const TilePos& pos){ + return std::find(tiles.begin(), tiles.end(), pos) != tiles.end(); + }; + const TilePos tile = nearest_tile(walk.actual); + if (!walk.failure.empty()){ + return std::format("{} from {}: failed at {}: {}", route.label, describe(*route.world, route.start), + to_string(walk.actual), walk.failure); + } + if (!on_grid(walk.actual) || !has(route.targets, tile) || !has(walk.believed, tile)){ + return std::format("{} from {}: ended at {} ({}), off the grid or off the target.", route.label, + describe(*route.world, route.start), to_string(walk.actual), describe(*route.world, tile)); + } + return ""; +} + + +std::vector world_tiles(const StoryWorld& world, const Pose& pose){ + std::vector tiles; + for (const TilePos& pos : Pokemon::tiles_of(pose)){ + tiles.push_back(story_to_world(world, pose.map, pos)); + } + return tiles; +} + +// Where the table has the player after `step`, from `at`: a route's +// target, a walk's last tile, a step's next tile; a scene or an event's +// end is the table's pose_after, or unknown. +std::optional pose_after_step(const std::optional& at, const StoryStep& step){ + std::optional next; + if (step.kind == StoryStepKind::ROUTE){ + next = step.target; + }else if (at && step.kind == StoryStepKind::STEP){ + std::vector tiles; + for (const TilePos& pos : Pokemon::tiles_of(*at)){ + tiles.push_back(pos.next(step.direction)); + } + next = Pokemon::tile_pose(at->map, tiles, step.direction); + }else if (at && step.kind == StoryStepKind::LEGS && step.variant.empty()){ + std::vector tiles = Pokemon::tiles_of(*at); + for (const Pokemon::StoryLeg& leg : step.legs){ + for (TilePos& pos : tiles){ + for (int i = 0; i < leg.tiles; i++){ + pos = pos.next(leg.direction); + } + } + } + next = Pokemon::tile_pose(at->map, tiles, step.legs.back().direction); + }else if (step.kind != StoryStepKind::GAME && step.kind != StoryStepKind::SHOP && + step.kind != StoryStepKind::MASH && step.kind != StoryStepKind::LEGS + ){ + next = at; + } + return step.pose_after ? step.pose_after : next; +} + +// Every ROUTE step of the table that starts from a known pose, from its +// first and its last tile, at the pace StoryExecutor walks it. +std::vector table_routes(){ + std::vector routes; + for (const StoryCheckpoint& row : story_checkpoints()){ + std::optional at = row.start; + bool shoes = row.id > std::string(RUNNING_SHOES_ROW); + for (const StoryStep& step : row.steps){ + const StoryWorld* world = at && step.kind == StoryStepKind::ROUTE ? story_plan_world(at->map, step.target->map) : nullptr; + const std::vector from = world ? world_tiles(*world, *at) : std::vector{}; + std::vector starts; + if (!from.empty()){ + starts.push_back(from.front()); + } + if (from.size() > 1){ + starts.push_back(from.back()); + } + for (const TilePos& start : starts){ + routes.push_back({.label = row.id + " " + step.name, .world = world, .from = from, .start = start, + .targets = world_tiles(*world, *step.target), .costs = *story_route_costs(step.variant), .can_run = shoes}); + } + at = pose_after_step(at, step); + shoes = shoes || (row.id == RUNNING_SHOES_ROW && step.name == RUNNING_SHOES_STEP); + } + } + return routes; +} + +UnitTestResult test_table_routes(Logger& logger){ + std::string errors; + size_t walked = 0; + for (const SimRoute& route : table_routes()){ + const SimWalk walk = walk_on_sim(route, false); + walked++; + const std::string error = walk_error(route, walk); + logger.log(std::format("{} from {}: {}", route.label, describe(*route.world, route.start), error.empty() ? "on target" : error)); + if (!error.empty()){ + errors += "\n " + error; + } + } + if (walked < 10){ + return std::format("Expected the table's routes; found {}.", walked); + } + return errors.empty() ? UnitTestResult(true) : UnitTestResult("Routes that did not end on their target:" + errors); +} + + +// A route to the tile below a door, then a walk north into it. +struct DoorRoute{ + std::string name; + std::string map; + TilePos from; + std::string door; +}; + +// Whether the route ends below the door and a walk north goes in. +std::string door_route_error(const DoorRoute& item, bool stick_turns){ + const StoryWorld& world = *story_plan_world(item.map, item.map); + const TilePos from = story_to_world(world, item.map, item.from); + const TilePos below = story_to_world(world, item.map, map_point(item.map, item.door).next(Direction::SOUTH)); + const SimRoute route{.label = item.name, .world = &world, .from = {from}, .start = from, .targets = {below}, + .costs = town_plan_costs(), .can_run = true}; + const SimWalk walk = walk_on_sim(route, stick_turns); + const std::string error = walk_error(route, walk); + if (!error.empty()){ + return error; + } + GridSim sim({.world = &world}, below, Direction::NORTH, true); + if (sim.hold(Direction::NORTH, 3000ms, SimPace::RUN).stop != SimStop::WARP){ + return item.name + ": the walk north did not go through the door."; + } + return ""; +} + +// r49 put us a tile west of Sandgem's Center door after a stick "turn", +// and r57 walked the Jubilife School's row a column short: a turn tap on +// the stick moves part of a tile, and every D-pad tap after keeps it. With +// D-pad turns both routes end below their doors; with stick turns they +// must miss, or the simulation would not have caught it. +UnitTestResult test_door_routes(Logger& logger){ + const std::vector routes{ + {.name = "Sandgem: below the Center's door", .map = "SandgemTown", .from = map_point("SandgemTown", "front_row_east_end"), + .door = "pokemon_center_door"}, + {.name = "Jubilife: below the School's door", .map = "JubilifeCity", .from = map_point("JubilifeCity", "pokemon_center_door").next(Direction::SOUTH), + .door = "trainers_school_door"}, + }; + for (const DoorRoute& item : routes){ + const std::string dpad = door_route_error(item, false); + if (!dpad.empty()){ + return "D-pad turns: " + dpad; + } + const std::string stick = door_route_error(item, true); + logger.log(item.name + ", stick turns: " + stick); + if (stick.empty()){ + return item.name + ": with turns on the stick, expected the route to miss the door; it went in."; + } + } + return true; +} + + +class Test_GridSim : public UnitTest{ +public: + Test_GridSim(std::string name, std::function body) + : UnitTest("PokemonBDSP::AutoStory - Grid sim: " + std::move(name)) + , m_body(std::move(body)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::function m_body; +}; + +} + + +void add_tests_AutoStory_GridSim(UnitTestDatabase& database){ + database.add("taps, holds, stick offsets, doors, people and trainers as measured", test_moves); + database.add("the table's routes end on their targets", test_table_routes); + database.add("a stick-tap turn misses the Sandgem Center and Jubilife School doors", test_door_routes); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h new file mode 100644 index 0000000000..16c74d7cce --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h @@ -0,0 +1,28 @@ +/* BDSP AutoStory Grid Simulation Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_GridSim_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_GridSim_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// The grid simulation (Utils/PokemonBDSP_AutoStory_GridSim.h): the moves +// measured on the console, the story table's routes walked by the route +// walker on it, and walks that missed doors on the console missing them here. +void add_tests_AutoStory_GridSim(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index d3cdd08b85..cdaa6f0865 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -96,24 +96,19 @@ void save_failure_screenshot(VideoStream& stream, size_t attempt){ StoryExecutor::StoryExecutor(StoryEnv& env) : m_env(env) + , m_io(env.env.console, env.context) + , m_walks(m_io) {} void StoryExecutor::begin_attempt(const Pokemon::StoryCheckpoint& data){ m_checkpoint = data.id; - // A row starts from its save: after the shoes' row, the save has them. - m_can_run = data.id > std::string(RUNNING_SHOES_ROW); - const StoryWorld* south = story_plan_world("Route202", "Route202"); - m_trainers = south ? story_trainers(*south) : std::vector{}; + m_walks.begin_attempt(data.id); } Outcome StoryExecutor::run(RunState& state, const StoryStep& step){ stream().log(std::format("[{}] {} {}: {}", m_checkpoint, Pokemon::story_step_kind_name(step.kind), step.name, step.why), COLOR_BLUE); try{ - Outcome outcome = run_guarded(state, step); - if (outcome.succeeded() && m_checkpoint == RUNNING_SHOES_ROW && step.name == RUNNING_SHOES_STEP){ - m_can_run = true; - } - return outcome; + return run_guarded(state, step); }catch (OperationFailedException& e){ if (m_stop_rows.contains(m_checkpoint)){ throw; @@ -135,49 +130,6 @@ Outcome StoryExecutor::run_guarded(RunState& state, const StoryStep& step){ return Pokemon::StoryStepExecutor::run(state, step); } -Pose StoryExecutor::walk_to(const Pose& from, const Pose& goal, const StoryStep& step){ - const StoryWorld* maps = story_plan_world(from.map, goal.map); - if (maps == nullptr){ - fail_step(stream(), std::format("{}: no maps join {} and {}.", step.name, from.map, goal.map)); - } - const std::optional costs = story_route_costs(step.variant); - if (!costs){ - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown route costs: " + step.variant); - } - std::vector start; - for (const TilePos& pos : Pokemon::tiles_of(from)){ - start.push_back(story_to_world(*maps, from.map, pos)); - } - std::vector targets; - for (const TilePos& pos : Pokemon::tiles_of(goal)){ - targets.push_back(story_to_world(*maps, goal.map, pos)); - } - // Only Route 202's trainers are known; the ones fought stay off - // m_trainers until the next attempt. - const bool south = maps == story_plan_world("Route202", "Route202"); - RouteTrainers trainers{ - south ? m_trainers : std::vector{}, - [this](const std::string& trainer){ fight_route_trainer(stream(), context(), trainer); } - }; - const RouteWalkPlan plan{ - [&](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, - label_of(step), {}, *costs, m_can_run ? MoveSpeed::RUN : MoveSpeed::WALK - }; - const std::vector at = walk_route(stream(), context(), run_from_wild_battle, *maps, start, trainers, plan); - if (south){ - m_trainers = std::move(trainers.sights); - } - std::vector local; - for (const TilePos& pos : at){ - const std::optional tile = story_to_local(*maps, goal.map, pos); - if (!tile){ - fail_step(stream(), std::format("{}: the walk ended off {}.", step.name, goal.map)); - } - local.push_back(*tile); - } - return Pokemon::tile_pose(goal.map, local, Pokemon::facing_of(goal)); -} - Outcome StoryExecutor::new_game(RunState&, const StoryStep&){ play_new_game_intro(m_env.env, context(), m_env.player_name); return Pokemon::outcome_ok(); @@ -190,213 +142,19 @@ Outcome StoryExecutor::options(RunState&, const StoryStep&){ } Outcome StoryExecutor::route(RunState& state, const StoryStep& step){ - if (Pokemon::tiles_of(state.pose).empty()){ - return Pokemon::outcome_failed("no_pose", Pokemon::to_string(state.pose)); - } - return Pokemon::outcome_ok(walk_to(state.pose, *step.target, step)); -} - -namespace{ - -// A LEGS step "to_dialogue": taps its one leg until a dialogue starts on the -// way, wherever it starts (Dawn's at Sandgem's edge, r35/r36: an exact tile -// and banner raced her scene). The scene's lines are the next step's. -Outcome leg_to_dialogue(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ - const Pokemon::StoryLeg& leg = step.legs.front(); - route_turn(stream, context, leg.direction); - const auto [x, y] = dpad_of(leg.direction); - const WalkResult walk = walk_tiles_dpad(stream, context, run_from_wild_battle, {x, y}, (size_t)leg.tiles, step.name); - stream.log(std::format("[{}] {}: taps {} to a dialogue: {} tiles, {}.", checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, walk_stop_name(walk.stop))); - if (walk.stop == WalkStop::DIALOG){ - return Pokemon::outcome_ok(); - } - // A scene can lock us before its first line: Dawn walks over first (r37). - ShortDialogWatcher dialog(COLOR_CYAN); - if (walk.stop == WalkStop::BLOCKED && wait_until(stream, context, 10000ms, {{dialog}}) == 0){ - return Pokemon::outcome_ok(); - } - return Pokemon::outcome_failed("no_dialogue", step.name); -} - -// A LEGS step "to_map": runs its one leg until the location banner of -// pose_after's map shows, wherever it shows. A straight run along an open -// row needs no tile count (r41, r42: Sandgem's row 11 took us into the lab). -Outcome leg_to_map(VideoStream& stream, ProControllerContext& context, const StoryStep& step, const std::string& checkpoint){ - const Pokemon::StoryLeg& leg = step.legs.front(); - const std::string map = step.pose_after ? step.pose_after->map : ""; - route_turn(stream, context, leg.direction); - const auto [x, y] = dpad_of(leg.direction); - const WalkResult walk = walk_until_dpad(stream, context, run_from_wild_battle, {x, y}, - Milliseconds(250) * leg.tiles + 2000ms, step.name, { - .arrived = [&](const ImageViewRGB32& screen){ return location_banner_matches(read_location_banner(screen), map); }, - .pace = DpadPace::RUN, - }); - stream.log(std::format("[{}] {}: ran {} to {}'s banner: {}.", checkpoint, step.name, direction_name(leg.direction), map, walk_stop_name(walk.stop))); - return walk.stop == WalkStop::ARRIVED ? Pokemon::outcome_ok() : Pokemon::outcome_failed("no_banner", map); -} - + return m_walks.route(state, step); } Outcome StoryExecutor::legs(RunState& state, const StoryStep& step){ - if (step.variant == "to_wall"){ - for (const Pokemon::StoryLeg& leg : step.legs){ - walk_until_stopped(stream(), context(), leg.direction, (size_t)leg.tiles, label_of(step)); - } - return Pokemon::outcome_ok(); - } - if (step.variant == "to_dialogue"){ - return leg_to_dialogue(stream(), context(), step, m_checkpoint); - } - if (step.variant == "to_map"){ - return leg_to_map(stream(), context(), step, m_checkpoint); - } - const bool one_short_ok = step.variant == "one_short_ok"; - if (!one_short_ok && !step.variant.empty()){ - return Pokemon::outcome_failed("unknown_variant", step.variant); - } - std::vector at = Pokemon::tiles_of(state.pose); - for (const Pokemon::StoryLeg& leg : step.legs){ - // Facing another way, a tap only turns (r256): turn first. - route_turn(stream(), context(), leg.direction); - const auto [x, y] = dpad_of(leg.direction); - const size_t tiles = (size_t)leg.tiles; - const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, tiles, step.name); - stream().log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_checkpoint, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); - if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ - return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); - } - // Where we may be: the end, or a tile short of it. - std::vector next; - for (const TilePos& start : at){ - TilePos end = start; - for (size_t i = 0; i < tiles; i++){ - if (one_short_ok && i + 1 == tiles){ - next.push_back(end); - } - end = end.next(leg.direction); - } - next.push_back(end); - } - at = std::move(next); - } - return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.legs.back().direction)); -} - -namespace{ - -// One rule for scenes and map changes: v2's long B mashes, with an end -// check. Press through the text, and keep pressing B while the screen is -// quiet: a scene can pause while someone walks off (Dawn and Barry after -// Rowan leaves, r23), fade dimly, not to black, to another map (r25), or -// wait on a box the detector misses (r26). B never starts a talk on the -// overworld. Done once the overworld has shown, with no text and a still -// screen, for 5 s of presses: after r31's fade home a scripted walk-in ran -// 5 s with no text before Mom spoke. -// -// A step or scene that ends on another map (`fade`) must fade on the way, -// and only the quiet after the fade counts: r30's scene went quiet for 5 s -// before the fade home, and r28 "went out" a tile beside the door and -// walked a whole route in the house. No fade is a map mismatch. -constexpr double FADE_DARKNESS = 40; // brightness 0 to 255; r24's dim fade home goes darker -void settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label, bool fade){ - const WallClock none = WallClock::max(); - const WallClock deadline = current_time() + 120s; - ShortDialogDetector dialog; - OverworldDetector overworld; - WallClock quiet_since = none; - double darkest = 255; - VideoSnapshot previous; - while (current_time() < deadline){ - context.wait_for_all_requests(); - const auto frame = stream.video().snapshot(); - const WallClock now = current_time(); - if (frame){ - darkest = std::min(darkest, image_average(*frame.frame).sum() / 3); - } - const bool talking = frame && dialog.detect(frame); - const bool still = frame && previous && walk_screen_change(*previous.frame, *frame.frame) < WALK_TAP_BLOCKED_CHANGE; - previous = frame; - if (!talking && still && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS)){ - quiet_since = quiet_since == none ? now : quiet_since; - if (now - quiet_since >= 5000ms){ - stream.log(std::format("[Story] {}: settled; darkest frame {:.0f}.", label, darkest)); - return; - } - }else{ - quiet_since = none; - } - pbf_press_button(context, talking ? button : BUTTON_B, 80ms, 1200ms); - } - OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, - fade && darkest >= FADE_DARKNESS - ? label + ": no fade within 120 s, so we are not where the step goes (map mismatch)." - : label + ": not back on a quiet overworld within 120 s.", - stream); -} - -} - -namespace{ - -// Holds the D-pad toward a door (with B after the shoes) until the screen goes -// dark or a line shows, up to 12 s. Returns whether it went dark. A line -// stops it: holding on under Mom's "Oh, Kosmos!" wasted 12 s (r45). The walker's black-screen watch -// missed the lab's fade into Sandgem (r44, "dialog"), and settle's samples -// 1.3 s apart can fall either side of it, so watch at settle's own darkness. -bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction, bool run){ - const DpadPosition dpad = - direction == Direction::NORTH ? DPAD_UP : - direction == Direction::SOUTH ? DPAD_DOWN : - direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; - BlackScreenWatcher dark(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 3 * FADE_DARKNESS, 60); - ShortDialogWatcher dialog(COLOR_YELLOW); - const int ret = run_until(stream, context, [&](ProControllerContext& context){ - if (run){ - ssf_press_button(context, BUTTON_B, 0ms, 12000ms, 0ms); - } - pbf_press_dpad(context, dpad, 12000ms, 0ms); - }, {{dark}, {dialog}}); - return ret == 0; -} - + return m_walks.legs(state, step); } Outcome StoryExecutor::step(RunState& state, const StoryStep& step){ - route_turn(stream(), context(), step.direction); - // A step through a door: walk (or run) on until the screen goes dark, - // from next to the door or from anywhere in its line (r43: tapping 8 tiles - // down the lab took 7 s), then settle for any scene on arrival (Mom's, - // 00.01; Dawn's tour, 00.04). Without the fade, settle must see it itself - // (r33 was outside, "no fade"). - if (step.pose_after && step.pose_after->map != state.pose.map){ - const bool faded = run_into_door(stream(), context(), step.direction, m_can_run); - stream().log(std::format("[{}] {}: {} through the door: {}.", m_checkpoint, step.name, direction_name(step.direction), faded ? "faded" : "no fade yet")); - settle(stream(), context(), BUTTON_B, label_of(step), !faded); - return Pokemon::outcome_ok(); - } - const auto [x, y] = dpad_of(step.direction); - const WalkResult walk = walk_tiles_dpad(stream(), context(), run_from_wild_battle, {x, y}, 1, step.name); - stream().log(std::format("[{}] {}: a step {}, {}.", m_checkpoint, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); - std::vector at; - for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ - at.push_back(pos.next(step.direction)); - } - return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.direction)); + return m_walks.step(state, step); } Outcome StoryExecutor::talk(RunState& state, const StoryStep& step){ - // Face whom we talk to: a route ends facing its last leg, not its goal - // (r40 reached Mom facing north). They block the tile, so the tap only turns. - if (const std::optional facing = Pokemon::facing_of(state.pose)){ - route_turn(stream(), context(), *facing); - } - pbf_press_button(context(), BUTTON_A, 80ms, 200ms); - context().wait_for_all_requests(); - ShortDialogWatcher dialog(COLOR_YELLOW); - if (wait_until(stream(), context(), 3000ms, {{dialog}}) != 0){ - fail_step(stream(), label_of(step) + ": nobody answered."); - } - return Pokemon::outcome_ok(); + return m_walks.talk(state, step); } Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ @@ -407,15 +165,7 @@ Outcome StoryExecutor::answer(RunState&, const StoryStep& step){ } Outcome StoryExecutor::mash(RunState& state, const StoryStep& step){ - // The step before may have played the scene already (a door step settles - // its arrival scene), so its first line is not required. - ShortDialogWatcher dialog(COLOR_CYAN); - if (wait_until(stream(), context(), 15000ms, {{dialog}}) != 0){ - stream().log(label_of(step) + ": no line within 15 s; the step before played it.", COLOR_ORANGE); - } - settle(stream(), context(), step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step), - step.pose_after && step.pose_after->map != state.pose.map); - return Pokemon::outcome_ok(); + return m_walks.mash(state, step); } Outcome StoryExecutor::battle(RunState&, const StoryStep& step){ @@ -444,7 +194,7 @@ Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ // way can leave the route unsure of the column (r59, Jubilife). Below // Sandgem's door the lineup read 0.7 tile off and stepped us off it (r48). route_turn(stream(), context(), Direction::NORTH); - const bool in = run_into_door(stream(), context(), Direction::NORTH, m_can_run); + const bool in = m_io.run_into_door(Direction::NORTH, m_walks.can_run()); stream().log(std::format("[{}] {}: north into the Center: {}.", m_checkpoint, step.name, in ? "faded" : "no door; lining up by sight")); if (in){ pbf_wait(context(), 1500ms); @@ -466,9 +216,9 @@ Outcome StoryExecutor::heal(RunState&, const StoryStep& step){ // Out the same way: south until the fade. The walk out's indoor check // read "not a room" a step from the counter and stopped inside (r50, r59). route_turn(stream(), context(), Direction::SOUTH); - const bool out = run_into_door(stream(), context(), Direction::SOUTH, m_can_run); + const bool out = m_io.run_into_door(Direction::SOUTH, m_walks.can_run()); stream().log(std::format("[{}] {}: south out of the Center: {}.", m_checkpoint, step.name, out ? "faded" : "no fade yet")); - settle(stream(), context(), BUTTON_B, label_of(step), !out); + m_io.settle(BUTTON_B, label_of(step), !out); // r143a's heal said it had walked out while still inside. context().wait_for_all_requests(); VideoSnapshot screen = stream().video().snapshot(); diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h index 88dbb00c09..f56ddbdb19 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h @@ -34,6 +34,8 @@ #include "Pokemon/AutoStory/Pokemon_StorySegments.h" #include "Pokemon/AutoStory/Pokemon_StorySteps.h" #include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP_AutoStory_StoryStepIO.h" +#include "PokemonBDSP_AutoStory_StoryWalkSteps.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -104,11 +106,6 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ const std::string& checkpoint() const{ return m_checkpoint; } - // Walks from the tiles of `from` on its map to any of `goal`'s tiles, - // fighting the trainers who see us this attempt. Returns where it left - // us, as a pose on `goal`'s map. - Pokemon::Pose walk_to(const Pokemon::Pose& from, const Pokemon::Pose& goal, const Pokemon::StoryStep& step); - // The intro of a new game, from the title screen to the bedroom. virtual Pokemon::Outcome new_game(Pokemon::RunState& state, const Pokemon::StoryStep& step) override; // verify_tutorial_settings(): fast text, Autosave off. @@ -148,12 +145,10 @@ class StoryExecutor : public Pokemon::StoryStepExecutor{ Pokemon::Outcome run_guarded(Pokemon::RunState& state, const Pokemon::StoryStep& step); StoryEnv& m_env; + ConsoleStoryStepIO m_io; + // The steps that walk and talk (ROUTE, LEGS, STEP, TALK, MASH). + StoryWalkSteps m_walks; std::string m_checkpoint; - // Whether the player has the Running Shoes: the pace routes are timed at. - bool m_can_run = false; - // Route 202's trainers who can still see us, in story_plan_world()'s - // world tiles; all of them again at each attempt. - std::vector m_trainers; std::map m_failures; std::map m_run_counts; std::set m_stop_rows; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp new file mode 100644 index 0000000000..a10d802ffe --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp @@ -0,0 +1,392 @@ +/* BDSP AutoStory Story Sim Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Rows of the story table run as the program runs them (the segment + * runner, make_story_segment()), with the program's own walking steps + * (StoryWalkSteps: ROUTE, LEGS, STEP, TALK, MASH) on the grid simulation. + * After each walking step the simulated player must stand, on the grid, + * on a tile the table gives; a step through a door must have warped. + * + * Events, battles, shops and saves are stubs: a dialogue they open is + * closed, and the table's pose_after puts the player where they end. A + * heal checks that a walk north from where we stand enters a door. A scene + * starts when the player steps onto its trigger (SimTrigger). Rooms with no + * map (the Trainers' School) are passed over. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "Utils/PokemonBDSP_AutoStory_GridSim.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryStepIO.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StoryWalkSteps.h" +#include "PokemonBDSP_AutoStory_StorySim_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::RunState; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +// StoryStepIO on the simulation. +class SimStoryStepIO : public StoryStepIO{ +public: + SimStoryStepIO(GridSim& sim, std::vector& notes) + : m_sim(sim) + , m_route(sim, notes) + {} + + virtual RouteIO& route() override{ + return m_route; + } + virtual void fight(const std::string&) override{ + m_sim.end_dialogue(); + } + virtual void walk_until_stopped(Direction direction, size_t tiles, const std::string& label) override{ + m_sim.tap(direction, ROUTE_TURN_TAP, false); + const Milliseconds limit = ROUTE_START_DELAY + 800ms + route_ms_per_tile(direction) * (int64_t)(2 * tiles); + const SimMove move = m_sim.hold(direction, limit, SimPace::WALK); + if (move.stop == SimStop::DIALOG || move.stop == SimStop::WARP){ + m_route.note(label + ": " + sim_stop_name(move.stop) + " instead of a stop.", RouteNote::FAILURE); + } + } + virtual bool run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&) override{ + for (Milliseconds ran = 0ms; ran < limit; ran += ROUTE_RUN_MS_PER_TILE_ACROSS){ + const SimMove move = m_sim.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); + if (story_to_local(m_sim.world(), map, nearest_tile(m_sim.player().pos))){ + return true; + } + if (move.distance == 0 || move.stop != SimStop::DONE){ + return false; + } + } + return false; + } + virtual bool run_into_door(Direction direction, bool run) override{ + return m_sim.hold(direction, 12000ms, run ? SimPace::RUN : SimPace::WALK).stop == SimStop::WARP; + } + // A scene may take us to another map (Mom walks us out, 00.04): with a + // dialogue open the stub scene plays it. Otherwise only a door fades. + virtual void settle(Button, const std::string& label, bool fade) override{ + if (fade && !m_sim.player().warped && !m_sim.player().dialog){ + m_route.note(label + ": no fade, so we are not where the step goes (map mismatch).", RouteNote::FAILURE); + } + m_sim.end_dialogue(); + } + +private: + GridSim& m_sim; + GridSimRouteIO m_route; +}; + + +// A scene's trigger the simulation fires from row `row` on, once the step +// `after` has run ("" from the row's start), on `map`'s `tiles`. +struct RowTrigger{ + std::string row; + std::string after; + std::string map; + SimTrigger trigger; +}; + +std::vector column(int x, int y0, int y1){ + std::vector tiles; + for (int y = y0; y <= y1; y++){ + tiles.push_back({x, y}); + } + return tiles; +} +std::vector row_of(int y, int x0, int x1){ + std::vector tiles; + for (int x = x0; x <= x1; x++){ + tiles.push_back({x, y}); + } + return tiles; +} + +// The coord events of 00.04 to 00.05c, from the decomp as the table cites +// them: Mom's call above the door after she heals us (r45, r46), Dawn's +// catching tutorial on Route 202, and the Poketch president on Jubilife's +// row 40. +std::vector story_triggers(){ + const auto trigger = [](std::string row, std::string after, std::string map, std::string name, std::vector tiles){ + return RowTrigger{.row = std::move(row), .after = std::move(after), .map = std::move(map), + .trigger = SimTrigger{.name = std::move(name), .tiles = std::move(tiles)}}; + }; + return { + trigger("00.04", "mom_heals_us", "PlayerHouse1F", "moms_call", {TilePos{6, 9}}), + trigger("00.04", "", "Route202", "catch_tutorial", column(21, 25, 29)), + trigger("00.05b", "", "JubilifeCity", "president", row_of(40, 44, 48)), + }; +} + + +// The program's walking steps on the simulation, the rest stubbed. +class SimExecutor : public Pokemon::StoryStepExecutor{ +public: + explicit SimExecutor(Logger& logger) + : m_logger(logger) + , m_sim({}, {0, 0}, Direction::NORTH, true) + , m_io(m_sim, m_notes) + , m_walks(m_io) + { + // Mom stands a row above the decomp's (7, 8) from 00.04 on (r43). + m_house = *story_plan_world("PlayerHouse1F", "PlayerHouse1F"); + m_house.block({7, 7}); + } + + const std::string& error() const{ + return m_error; + } + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ + m_row = data.id; + m_walks.begin_attempt(data.id); + arm(""); + } + virtual Outcome run(RunState& state, const StoryStep& step) override; + +protected: + virtual Outcome route(RunState& state, const StoryStep& step) override{ return m_walks.route(state, step); } + virtual Outcome legs(RunState& state, const StoryStep& step) override{ return m_walks.legs(state, step); } + virtual Outcome step(RunState& state, const StoryStep& step) override{ return m_walks.step(state, step); } + virtual Outcome talk(RunState& state, const StoryStep& step) override{ return m_walks.talk(state, step); } + virtual Outcome mash(RunState& state, const StoryStep& step) override{ return m_walks.mash(state, step); } + virtual Outcome heal(RunState& state, const StoryStep& step) override; + virtual Outcome new_game(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome options(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome answer(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome battle(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome train(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome shop(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome save(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome game(RunState&, const StoryStep&) override{ return stub(); } + +private: + // A step the simulation does not play: its dialogue opened and closed. + Outcome stub(){ + m_sim.end_dialogue(); + return Pokemon::outcome_ok(); + } + const StoryWorld* sim_world(const std::string& map) const; + SimWorld setup(const StoryWorld* world) const; + void arm(const std::string& after); + bool sync(const Pose& pose); + std::string check(const StoryStep& step, const Outcome& outcome) const; + Outcome run_step(RunState& state, const StoryStep& step); + + Logger& m_logger; + std::vector m_notes; + GridSim m_sim; + SimStoryStepIO m_io; + StoryWalkSteps m_walks; + StoryWorld m_house; + std::string m_row; + std::vector m_armed; + // Whether the player has been put on a map yet. + bool m_placed = false; + // Whether the step running now is on a room with no map. + bool m_passed_over = false; + std::string m_error; +}; + +// The world the player walks on for `map`: the planner's, with what the +// console showed that the maps lack. Nothing for a room with no map. +const StoryWorld* SimExecutor::sim_world(const std::string& map) const{ + const StoryWorld* world = story_plan_world(map, map); + if (world == story_plan_world("PlayerHouse1F", "PlayerHouse1F") && m_row >= "00.04"){ + return &m_house; + } + return world; +} + +SimWorld SimExecutor::setup(const StoryWorld* world) const{ + SimWorld out{.world = world, .trainers = story_trainers(*world)}; + for (const RowTrigger& armed : m_armed){ + if (sim_world(armed.map) != world){ + continue; + } + SimTrigger trigger{.name = armed.trigger.name}; + for (const TilePos& pos : armed.trigger.tiles){ + trigger.tiles.push_back(story_to_world(*world, armed.map, pos)); + } + out.triggers.push_back(std::move(trigger)); + } + return out; +} + +// Arms the triggers of this row that start after `after`. +void SimExecutor::arm(const std::string& after){ + size_t armed = 0; + for (const RowTrigger& trigger : story_triggers()){ + if (trigger.row == m_row && trigger.after == after){ + m_armed.push_back(trigger); + armed++; + } + } + const SimPlayer& player = m_sim.player(); + if (armed > 0 && m_placed && on_grid(player.pos) && !player.dialog){ + m_sim.place(setup(&m_sim.world()), nearest_tile(player.pos), player.facing); + } +} + +// Puts the player on `pose` unless they stand on one of its tiles already. +// Returns whether the pose is on a map the simulation has. +bool SimExecutor::sync(const Pose& pose){ + const std::vector tiles = Pokemon::tiles_of(pose); + const StoryWorld* world = pose.map.empty() ? nullptr : sim_world(pose.map); + if (world == nullptr || tiles.empty()){ + return false; + } + const SimPlayer& player = m_sim.player(); + const std::optional local = !m_placed || &m_sim.world() != world + ? std::nullopt : story_to_local(*world, pose.map, nearest_tile(player.pos)); + if (local && on_grid(player.pos) && std::find(tiles.begin(), tiles.end(), *local) != tiles.end()){ + return true; + } + m_sim.place(setup(world), story_to_world(*world, pose.map, tiles.front()), Pokemon::facing_of(pose)); + m_placed = true; + return true; +} + +Outcome SimExecutor::heal(RunState& state, const StoryStep& step){ + if (sync(state.pose) && m_sim.hold(Direction::NORTH, 3000ms, SimPace::RUN).stop != SimStop::WARP){ + return Pokemon::outcome_failed("no_door", step.name + ": no door north of " + to_string(m_sim.player().pos)); + } + return Pokemon::outcome_ok(); +} + +// Why a walking step did not end where the table says, or "". +std::string SimExecutor::check(const StoryStep& step, const Outcome& outcome) const{ + const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || step.kind == StoryStepKind::STEP; + const std::optional expected = step.pose_after ? step.pose_after : outcome.pose; + if (!walks || !expected || Pokemon::tiles_of(*expected).empty() || !m_placed){ + return ""; + } + const SimPlayer& player = m_sim.player(); + const std::optional local = story_to_local(m_sim.world(), expected->map, nearest_tile(player.pos)); + if (!local){ + return player.warped ? "" : std::format("ended on {} at {}, not on {}.", describe(m_sim.world(), nearest_tile(player.pos)), + to_string(player.pos), expected->map); + } + const std::vector tiles = Pokemon::tiles_of(*expected); + if (!on_grid(player.pos) || std::find(tiles.begin(), tiles.end(), *local) == tiles.end()){ + return std::format("ended at {} ({}), not on {}.", to_string(player.pos), describe(m_sim.world(), nearest_tile(player.pos)), + Pokemon::to_string(*expected)); + } + return ""; +} + +Outcome SimExecutor::run_step(RunState& state, const StoryStep& step){ + if (story_events().contains(step.variant)){ + return stub(); + } + // A scene goes on from where the step before left us. + const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || + step.kind == StoryStepKind::STEP || step.kind == StoryStepKind::TALK; + if (walks && !sync(state.pose)){ + m_logger.log(std::format("[{}] {}: no map for {}; passed over.", m_row, step.name, Pokemon::to_string(state.pose))); + m_passed_over = true; + return Pokemon::outcome_ok(); + } + return Pokemon::StoryStepExecutor::run(state, step); +} + +Outcome SimExecutor::run(RunState& state, const StoryStep& step){ + Outcome outcome; + m_passed_over = false; + try{ + outcome = run_step(state, step); + }catch (const GridSimFailure& e){ + outcome = Pokemon::outcome_failed("sim_failure", e.message); + } + const std::string mismatch = outcome.succeeded() && !m_passed_over ? check(step, outcome) : ""; + if (!mismatch.empty()){ + outcome = Pokemon::outcome_failed("sim_mismatch", step.name + ": " + mismatch); + } + m_logger.log(std::format("[{}] {}: {}, the player at {}.", m_row, step.name, Pokemon::to_string(outcome), to_string(m_sim.player().pos))); + if (!outcome.succeeded() && m_error.empty()){ + m_error = m_row + " " + Pokemon::to_string(outcome); + } + arm(step.name); + return outcome; +} + + +// Runs `range` of the table on the simulation, stopping at the first failure. +UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ + SimExecutor executor(logger); + std::set saved; + Pokemon::Segment segment = make_story_segment(story_checkpoints(), executor, saved); + Pokemon::SegmentPolicy policy; + policy.stop_at_first_failure = true; + Pokemon::SegmentHooks hooks; + hooks.reload = [](RunState&){ return Pokemon::outcome_ok(); }; + hooks.log = [&logger](const std::string& line){ logger.log(line); }; + RunState state; + const Pokemon::SegmentReport report = Pokemon::run_segment(state, segment, range, policy, hooks); + if (!report.completed){ + return std::format("stopped at {} {}: {}", report.failed_checkpoint, report.failed_event, executor.error()); + } + return true; +} + + +class Test_StorySim : public UnitTest{ +public: + Test_StorySim(std::string name, std::string from, std::string to) + : UnitTest("PokemonBDSP::AutoStory - Story sim: " + std::move(name)) + , m_from(std::move(from)) + , m_to(std::move(to)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return run_rows(logger, {.from = m_from, .to = m_to}); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + std::string m_from; + std::string m_to; +}; + +} + + +void add_tests_AutoStory_StorySim(UnitTestDatabase& database){ + database.add("00.04 to 00.04d: home, the catch, Sandgem's Center, Route 202", "00.04", "00.04d"); + database.add("00.05 to 00.05c: Jubilife's Center, School, clowns and Poketch", "00.05", "00.05c"); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h new file mode 100644 index 0000000000..dfa12e07be --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h @@ -0,0 +1,28 @@ +/* BDSP AutoStory Story Sim Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StorySim_Tests_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StorySim_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + + +// The story table's rows run by the program's own walking steps +// (StoryWalkSteps) on the grid simulation: each walk must end where the +// table says the player is. +void add_tests_AutoStory_StorySim(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp new file mode 100644 index 0000000000..e999b15bb3 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp @@ -0,0 +1,146 @@ +/* BDSP AutoStory - Story step IO + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/ImageTools/ImageStats.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "CommonTools/VisualDetectors/BlackScreenDetector.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_Superscalar.h" +#include "PokemonBDSP/Inference/PokemonBDSP_DialogDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Detect/PokemonBDSP_AutoStory_OverworldDetector.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Talks.h" +#include "PokemonBDSP_AutoStory_Walks.h" +#include "PokemonBDSP_AutoStory_WildBattle.h" +#include "PokemonBDSP_AutoStory_StoryStepIO.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; + + +namespace{ + +// One rule for scenes and map changes: v2's long B mashes, with an end +// check. Press through the text, and keep pressing B while the screen is +// quiet: a scene can pause while someone walks off (Dawn and Barry after +// Rowan leaves, r23), fade dimly, not to black, to another map (r25), or +// wait on a box the detector misses (r26). B never starts a talk on the +// overworld. Done once the overworld has shown, with no text and a still +// screen, for 5 s of presses: after r31's fade home a scripted walk-in ran +// 5 s with no text before Mom spoke. +// +// A step or scene that ends on another map (`fade`) must fade on the way, +// and only the quiet after the fade counts: r30's scene went quiet for 5 s +// before the fade home, and r28 "went out" a tile beside the door and +// walked a whole route in the house. No fade is a map mismatch. +constexpr double FADE_DARKNESS = 40; // brightness 0 to 255; r24's dim fade home goes darker +void settle(VideoStream& stream, ProControllerContext& context, Button button, const std::string& label, bool fade){ + const WallClock none = WallClock::max(); + const WallClock deadline = current_time() + 120s; + ShortDialogDetector dialog; + OverworldDetector overworld; + WallClock quiet_since = none; + double darkest = 255; + VideoSnapshot previous; + while (current_time() < deadline){ + context.wait_for_all_requests(); + const auto frame = stream.video().snapshot(); + const WallClock now = current_time(); + if (frame){ + darkest = std::min(darkest, image_average(*frame.frame).sum() / 3); + } + const bool talking = frame && dialog.detect(frame); + const bool still = frame && previous && walk_screen_change(*previous.frame, *frame.frame) < WALK_TAP_BLOCKED_CHANGE; + previous = frame; + if (!talking && still && overworld.detect(frame) && (!fade || darkest < FADE_DARKNESS)){ + quiet_since = quiet_since == none ? now : quiet_since; + if (now - quiet_since >= 5000ms){ + stream.log(std::format("[Story] {}: settled; darkest frame {:.0f}.", label, darkest)); + return; + } + }else{ + quiet_since = none; + } + pbf_press_button(context, talking ? button : BUTTON_B, 80ms, 1200ms); + } + OperationFailedExceptionWithScreenshot::fire(ErrorReportMode::SEND_ERROR_REPORT, + fade && darkest >= FADE_DARKNESS + ? label + ": no fade within 120 s, so we are not where the step goes (map mismatch)." + : label + ": not back on a quiet overworld within 120 s.", + stream); +} + +// Holds the D-pad toward a door (with B after the shoes) until the screen goes +// dark or a line shows, up to 12 s. Returns whether it went dark. A line +// stops it: holding on under Mom's "Oh, Kosmos!" wasted 12 s (r45). The walker's black-screen watch +// missed the lab's fade into Sandgem (r44, "dialog"), and settle's samples +// 1.3 s apart can fall either side of it, so watch at settle's own darkness. +bool run_into_door(VideoStream& stream, ProControllerContext& context, Direction direction, bool run){ + const DpadPosition dpad = + direction == Direction::NORTH ? DPAD_UP : + direction == Direction::SOUTH ? DPAD_DOWN : + direction == Direction::WEST ? DPAD_LEFT : DPAD_RIGHT; + BlackScreenWatcher dark(COLOR_RED, {0.1, 0.1, 0.8, 0.8}, 3 * FADE_DARKNESS, 60); + ShortDialogWatcher dialog(COLOR_YELLOW); + const int ret = run_until(stream, context, [&](ProControllerContext& context){ + if (run){ + ssf_press_button(context, BUTTON_B, 0ms, 12000ms, 0ms); + } + pbf_press_dpad(context, dpad, 12000ms, 0ms); + }, {{dark}, {dialog}}); + return ret == 0; +} + +} + + +ConsoleStoryStepIO::ConsoleStoryStepIO(VideoStream& stream, ProControllerContext& context) + : m_stream(stream) + , m_context(context) + , m_escape(run_from_wild_battle) + , m_route(stream, context, m_escape) +{} + +void ConsoleStoryStepIO::fight(const std::string& trainer){ + fight_route_trainer(m_stream, m_context, trainer); +} + +void ConsoleStoryStepIO::walk_until_stopped(Direction direction, size_t tiles, const std::string& label){ + PokemonBDSP::walk_until_stopped(m_stream, m_context, direction, tiles, label); +} + +bool ConsoleStoryStepIO::run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string& label){ + const auto [x, y] = dpad_of(direction); + const WalkResult walk = walk_until_dpad(m_stream, m_context, run_from_wild_battle, {x, y}, limit, label, { + .arrived = [&](const ImageViewRGB32& screen){ return location_banner_matches(read_location_banner(screen), map); }, + .pace = DpadPace::RUN, + }); + m_stream.log(std::format("[Story] {}: ran {} to {}'s banner: {}.", label, direction_name(direction), map, walk_stop_name(walk.stop))); + return walk.stop == WalkStop::ARRIVED; +} + +bool ConsoleStoryStepIO::run_into_door(Direction direction, bool run){ + return PokemonBDSP::run_into_door(m_stream, m_context, direction, run); +} + +void ConsoleStoryStepIO::settle(Button button, const std::string& label, bool fade){ + PokemonBDSP::settle(m_stream, m_context, button, label, fade); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h new file mode 100644 index 0000000000..34e6826931 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h @@ -0,0 +1,78 @@ +/* BDSP AutoStory - Story step IO + * + * From: https://github.com/PokemonAutomation/ + * + * What the story's walking steps (StoryWalkSteps) do on the console beyond + * the route walker's RouteIO: fights, walks until stopped, runs into doors + * and the settle after a scene. Tests run the steps on a simulation; + * ConsoleStoryStepIO is the console. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryStepIO_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryStepIO_H + +#include +#include "Common/Cpp/Time.h" +#include "CommonFramework/Tools/VideoStream.h" +#include "NintendoSwitch/Controllers/NintendoSwitch_ControllerButtons.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteIO.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Walk.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class StoryStepIO{ +public: + virtual ~StoryStepIO() = default; + + // The route walker's console: walks, presses, dialogue watches, the log. + virtual RouteIO& route() = 0; + // Fights a trainer whose dialogue has opened (fight_route_trainer()). + virtual void fight(const std::string& trainer) = 0; + // walk_until_stopped(): `direction` until something stops us, for at + // most `tiles` tiles' time. Fails on a door or a dialogue. + virtual void walk_until_stopped(Pokemon::Direction direction, size_t tiles, const std::string& label) = 0; + // Runs `direction` on the D-pad until the location banner of `map` (a + // JSON map's name) shows, for at most `limit`. Returns whether it showed. + virtual bool run_until_map(Pokemon::Direction direction, Milliseconds limit, const std::string& map, const std::string& label) = 0; + // Holds the D-pad toward a door (running with `run`) until the screen + // goes dark or a line shows. Returns whether it went dark. + virtual bool run_into_door(Pokemon::Direction direction, bool run) = 0; + // Presses through any text until the overworld is quiet; with `fade`, a + // fade must come first (a map change). Fails otherwise. + virtual void settle(Button button, const std::string& label, bool fade) = 0; +}; + + +// The console. +class ConsoleStoryStepIO : public StoryStepIO{ +public: + ConsoleStoryStepIO(VideoStream& stream, ProControllerContext& context); + + virtual RouteIO& route() override{ + return m_route; + } + virtual void fight(const std::string& trainer) override; + virtual void walk_until_stopped(Pokemon::Direction direction, size_t tiles, const std::string& label) override; + virtual bool run_until_map(Pokemon::Direction direction, Milliseconds limit, const std::string& map, const std::string& label) override; + virtual bool run_into_door(Pokemon::Direction direction, bool run) override; + virtual void settle(Button button, const std::string& label, bool fade) override; + +private: + VideoStream& m_stream; + ProControllerContext& m_context; + WildBattleEscape m_escape; + ConsoleRouteIO m_route; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp new file mode 100644 index 0000000000..f7eb637af7 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp @@ -0,0 +1,250 @@ +/* BDSP AutoStory - Story walk steps + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Color.h" +#include "Common/Cpp/Exceptions.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_PlannedRoute.h" +#include "PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StoryWalkSteps.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::RunState; +using Pokemon::StoryStep; +using Pokemon::TilePos; + + +namespace{ + +// "to_catch_grass" as "to catch grass", for the logs. +std::string label_of(const StoryStep& step){ + std::string label = step.name; + std::replace(label.begin(), label.end(), '_', ' '); + return label; +} + +// Fails the step (fail_step() on the console). +[[noreturn]] void fail(RouteIO& io, const std::string& message){ + io.note(message, RouteNote::FAILURE); + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "RouteIO::note() returned from a failure: " + message); +} + +} + + +void StoryWalkSteps::begin_attempt(const std::string& row){ + m_row = row; + // A row starts from its save: after the shoes' row, the save has them. + m_can_run = row > std::string(RUNNING_SHOES_ROW); + const StoryWorld* south = story_plan_world("Route202", "Route202"); + m_trainers = south ? story_trainers(*south) : std::vector{}; +} + +void StoryWalkSteps::turn(Direction direction){ + m_io.route().note(std::string("[Route] Turning ") + direction_name(direction) + ".", RouteNote::LOG); + m_io.route().press(route_turn_tap(direction)); +} + +void StoryWalkSteps::log(const std::string& text){ + m_io.route().note(text, RouteNote::LOG); +} + +Pose StoryWalkSteps::walk_to(const Pose& from, const Pose& goal, const StoryStep& step){ + const StoryWorld* maps = story_plan_world(from.map, goal.map); + if (maps == nullptr){ + fail(m_io.route(), std::format("{}: no maps join {} and {}.", step.name, from.map, goal.map)); + } + const std::optional costs = story_route_costs(step.variant); + if (!costs){ + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown route costs: " + step.variant); + } + std::vector start; + for (const TilePos& pos : Pokemon::tiles_of(from)){ + start.push_back(story_to_world(*maps, from.map, pos)); + } + std::vector targets; + for (const TilePos& pos : Pokemon::tiles_of(goal)){ + targets.push_back(story_to_world(*maps, goal.map, pos)); + } + // Only Route 202's trainers are known; the ones fought stay off + // m_trainers until the next attempt. + const bool south = maps == story_plan_world("Route202", "Route202"); + RouteTrainers trainers{ + south ? m_trainers : std::vector{}, + [this](const std::string& trainer){ m_io.fight(trainer); } + }; + const RouteWalkPlan plan{ + [&](const TilePos& pos){ return std::find(targets.begin(), targets.end(), pos) != targets.end(); }, + label_of(step), {}, *costs, m_can_run ? MoveSpeed::RUN : MoveSpeed::WALK + }; + const std::vector at = walk_route(m_io.route(), *maps, start, trainers, plan); + if (south){ + m_trainers = std::move(trainers.sights); + } + std::vector local; + for (const TilePos& pos : at){ + const std::optional tile = story_to_local(*maps, goal.map, pos); + if (!tile){ + fail(m_io.route(), std::format("{}: the walk ended off {}.", step.name, goal.map)); + } + local.push_back(*tile); + } + return Pokemon::tile_pose(goal.map, local, Pokemon::facing_of(goal)); +} + +Outcome StoryWalkSteps::route(RunState& state, const StoryStep& step){ + if (Pokemon::tiles_of(state.pose).empty()){ + return Pokemon::outcome_failed("no_pose", Pokemon::to_string(state.pose)); + } + return Pokemon::outcome_ok(walk_to(state.pose, *step.target, step)); +} + +// A LEGS step "to_dialogue": taps its one leg until a dialogue starts on the +// way, wherever it starts (Dawn's at Sandgem's edge, r35/r36: an exact tile +// and banner raced her scene). The scene's lines are the next step's. +Outcome StoryWalkSteps::leg_to_dialogue(const StoryStep& step){ + const Pokemon::StoryLeg& leg = step.legs.front(); + turn(leg.direction); + const WalkResult walk = m_io.route().walk_taps(leg.direction, (size_t)leg.tiles, step.name); + log(std::format("[{}] {}: taps {} to a dialogue: {} tiles, {}.", m_row, step.name, direction_name(leg.direction), walk.moved_chunks, walk_stop_name(walk.stop))); + if (walk.stop == WalkStop::DIALOG){ + return Pokemon::outcome_ok(); + } + // A scene can lock us before its first line: Dawn walks over first (r37). + if (walk.stop == WalkStop::BLOCKED && m_io.route().dialog_within(10000ms, COLOR_CYAN)){ + return Pokemon::outcome_ok(); + } + return Pokemon::outcome_failed("no_dialogue", step.name); +} + +// A LEGS step "to_map": runs its one leg until the location banner of +// pose_after's map shows, wherever it shows. A straight run along an open +// row needs no tile count (r41, r42: Sandgem's row 11 took us into the lab). +Outcome StoryWalkSteps::leg_to_map(const StoryStep& step){ + const Pokemon::StoryLeg& leg = step.legs.front(); + const std::string map = step.pose_after ? step.pose_after->map : ""; + turn(leg.direction); + const bool arrived = m_io.run_until_map(leg.direction, Milliseconds(250) * leg.tiles + 2000ms, map, step.name); + return arrived ? Pokemon::outcome_ok() : Pokemon::outcome_failed("no_banner", map); +} + +// Each leg in D-pad taps, turning first; "one_short_ok" lets a leg end a +// tile short. +Outcome StoryWalkSteps::tap_legs(const Pose& from, const StoryStep& step){ + const bool one_short_ok = step.variant == "one_short_ok"; + std::vector at = Pokemon::tiles_of(from); + for (const Pokemon::StoryLeg& leg : step.legs){ + // Facing another way, a tap only turns (r256): turn first. + turn(leg.direction); + const size_t tiles = (size_t)leg.tiles; + const WalkResult walk = m_io.route().walk_taps(leg.direction, tiles, step.name); + log(std::format("[{}] {}: taps {} moved {} of {} tiles.", m_row, step.name, direction_name(leg.direction), walk.moved_chunks, tiles)); + if (walk.moved_chunks + (one_short_ok ? 1 : 0) < tiles){ + return Pokemon::outcome_failed("short_leg", std::format("{} {} moved {}", direction_name(leg.direction), tiles, walk.moved_chunks)); + } + // Where we may be: the end, or a tile short of it. + std::vector next; + for (const TilePos& start : at){ + TilePos end = start; + for (size_t i = 0; i < tiles; i++){ + if (one_short_ok && i + 1 == tiles){ + next.push_back(end); + } + end = end.next(leg.direction); + } + next.push_back(end); + } + at = std::move(next); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(from.map, at, step.legs.back().direction)); +} + +Outcome StoryWalkSteps::legs(RunState& state, const StoryStep& step){ + if (step.variant == "to_wall"){ + for (const Pokemon::StoryLeg& leg : step.legs){ + m_io.walk_until_stopped(leg.direction, (size_t)leg.tiles, label_of(step)); + } + return Pokemon::outcome_ok(); + } + if (step.variant == "to_dialogue"){ + return leg_to_dialogue(step); + } + if (step.variant == "to_map"){ + return leg_to_map(step); + } + if (step.variant != "one_short_ok" && !step.variant.empty()){ + return Pokemon::outcome_failed("unknown_variant", step.variant); + } + return tap_legs(state.pose, step); +} + +Outcome StoryWalkSteps::step(RunState& state, const StoryStep& step){ + turn(step.direction); + // A step through a door: walk (or run) on until the screen goes dark, + // from next to the door or from anywhere in its line (r43: tapping 8 tiles + // down the lab took 7 s), then settle for any scene on arrival (Mom's, + // 00.01; Dawn's tour, 00.04). Without the fade, settle must see it itself + // (r33 was outside, "no fade"). + if (step.pose_after && step.pose_after->map != state.pose.map){ + const bool faded = m_io.run_into_door(step.direction, m_can_run); + log(std::format("[{}] {}: {} through the door: {}.", m_row, step.name, direction_name(step.direction), faded ? "faded" : "no fade yet")); + m_io.settle(BUTTON_B, label_of(step), !faded); + return Pokemon::outcome_ok(); + } + const WalkResult walk = m_io.route().walk_taps(step.direction, 1, step.name); + log(std::format("[{}] {}: a step {}, {}.", m_row, step.name, direction_name(step.direction), walk_stop_name(walk.stop))); + std::vector at; + for (const TilePos& pos : Pokemon::tiles_of(state.pose)){ + at.push_back(pos.next(step.direction)); + } + return Pokemon::outcome_ok(Pokemon::tile_pose(state.pose.map, at, step.direction)); +} + +Outcome StoryWalkSteps::talk(RunState& state, const StoryStep& step){ + // Face whom we talk to: a route ends facing its last leg, not its goal + // (r40 reached Mom facing north). They block the tile, so the tap only turns. + if (const std::optional facing = Pokemon::facing_of(state.pose)){ + turn(*facing); + } + m_io.route().press({.kind = RouteInputKind::BUTTON_A, .direction = Direction::NORTH, .hold = 80ms, .release = 200ms, + .count = 1, .end = RouteInputEnd::DONE}); + if (!m_io.route().dialog_within(3000ms, COLOR_YELLOW)){ + fail(m_io.route(), label_of(step) + ": nobody answered."); + } + return Pokemon::outcome_ok(); +} + +Outcome StoryWalkSteps::mash(RunState& state, const StoryStep& step){ + // The step before may have played the scene already (a door step settles + // its arrival scene), so its first line is not required. + if (!m_io.route().dialog_within(15000ms, COLOR_CYAN)){ + m_io.route().note(label_of(step) + ": no line within 15 s; the step before played it.", RouteNote::WARNING); + } + m_io.settle(step.variant == "a" ? BUTTON_A : BUTTON_B, label_of(step), + step.pose_after && step.pose_after->map != state.pose.map); + if (m_row == RUNNING_SHOES_ROW && step.name == RUNNING_SHOES_STEP){ + m_can_run = true; + } + return Pokemon::outcome_ok(); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h new file mode 100644 index 0000000000..b883eeae01 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h @@ -0,0 +1,71 @@ +/* BDSP AutoStory - Story walk steps + * + * From: https://github.com/PokemonAutomation/ + * + * The story steps that walk and talk (ROUTE, LEGS, STEP, TALK, MASH), on a + * StoryStepIO: StoryExecutor runs them on the console, the story's + * simulation tests on a simulated player. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_StoryWalkSteps_H +#define PokemonAutomation_PokemonBDSP_AutoStory_StoryWalkSteps_H + +#include +#include +#include "Pokemon/AutoStory/Pokemon_StorySteps.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP_AutoStory_StoryStepIO.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +class StoryWalkSteps{ +public: + explicit StoryWalkSteps(StoryStepIO& io) + : m_io(io) + {} + + // A new attempt of the row `row`: every trainer can see us again, and + // the pace is the row's save's (a run after the Running Shoes). + void begin_attempt(const std::string& row); + // Whether the player has the Running Shoes: the pace routes are timed at. + bool can_run() const{ + return m_can_run; + } + + // Walks the planned route to `step.target`. Variant: the plan's costs. + Pokemon::Outcome route(Pokemon::RunState& state, const Pokemon::StoryStep& step); + // See StoryExecutor::legs(). + Pokemon::Outcome legs(Pokemon::RunState& state, const Pokemon::StoryStep& step); + // One D-pad step, or a run through a door when the step changes map. + Pokemon::Outcome step(Pokemon::RunState& state, const Pokemon::StoryStep& step); + // A at what we face; a dialogue must open. + Pokemon::Outcome talk(Pokemon::RunState& state, const Pokemon::StoryStep& step); + // Text to a quiet overworld; the Running Shoes' scene gives the shoes. + Pokemon::Outcome mash(Pokemon::RunState& state, const Pokemon::StoryStep& step); + +private: + Pokemon::Pose walk_to(const Pokemon::Pose& from, const Pokemon::Pose& goal, const Pokemon::StoryStep& step); + Pokemon::Outcome tap_legs(const Pokemon::Pose& from, const Pokemon::StoryStep& step); + Pokemon::Outcome leg_to_dialogue(const Pokemon::StoryStep& step); + Pokemon::Outcome leg_to_map(const Pokemon::StoryStep& step); + void turn(Pokemon::Direction direction); + void log(const std::string& text); + + StoryStepIO& m_io; + std::string m_row; + bool m_can_run = false; + // Route 202's trainers who can still see us, in story_plan_world()'s + // world tiles; all of them again at each attempt. + std::vector m_trainers; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp new file mode 100644 index 0000000000..302b175634 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp @@ -0,0 +1,361 @@ +/* BDSP AutoStory - Grid simulation + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_GridSim.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::TileKind; +using Pokemon::TilePos; + + +namespace{ + +int floor_div(int value, int by){ + return value >= 0 ? value / by : -((-value + by - 1) / by); +} + +bool across(Direction direction){ + return direction == Direction::EAST || direction == Direction::WEST; +} +// +1 toward the east or south, -1 toward the west or north. +int sign_of(Direction direction){ + return direction == Direction::EAST || direction == Direction::SOUTH ? 1 : -1; +} +int& along(SimPos& pos, Direction direction){ + return across(direction) ? pos.x : pos.y; +} +int along(const SimPos& pos, Direction direction){ + return across(direction) ? pos.x : pos.y; +} +int sideways(const SimPos& pos, Direction direction){ + return across(direction) ? pos.y : pos.x; +} +TilePos tile_at(Direction direction, int along_tile, int side_tile){ + return across(direction) ? TilePos{along_tile, side_tile} : TilePos{side_tile, along_tile}; +} + +// The tiles a player at `pos` moves into with their next tile toward +// `direction`: one on the grid, two off it. `pos` is on the grid along +// `direction`. +std::vector tiles_ahead(const SimPos& pos, Direction direction){ + const int next = along(pos, direction) / SIM_TILE + sign_of(direction); + const int side = sideways(pos, direction); + std::vector tiles{tile_at(direction, next, floor_div(side, SIM_TILE))}; + if (side % SIM_TILE != 0){ + tiles.push_back(tile_at(direction, next, floor_div(side, SIM_TILE) + 1)); + } + return tiles; +} + +bool is_door(const StoryWorld& world, const TilePos& pos){ + const std::optional tile = world.tile(pos); + return tile && *tile == TileKind::DOOR && !world.occupied(pos); +} + +Milliseconds per_tile(Direction direction, SimPace pace, bool can_run){ + return route_ms_per_tile(direction, pace == SimPace::RUN && can_run ? MoveSpeed::RUN : MoveSpeed::WALK); +} + +} + + +SimPos sim_pos(const TilePos& tile){ + return {tile.x * SIM_TILE, tile.y * SIM_TILE}; +} +bool on_grid(const SimPos& pos){ + return pos.x % SIM_TILE == 0 && pos.y % SIM_TILE == 0; +} +TilePos nearest_tile(const SimPos& pos){ + return {floor_div(pos.x + SIM_TILE / 2, SIM_TILE), floor_div(pos.y + SIM_TILE / 2, SIM_TILE)}; +} +std::string to_string(const SimPos& pos){ + return std::format("({:g}, {:g})", pos.x / (double)SIM_TILE, pos.y / (double)SIM_TILE); +} + +const char* sim_stop_name(SimStop stop){ + switch (stop){ + case SimStop::DONE: + return "done"; + case SimStop::BLOCKED: + return "blocked"; + case SimStop::DIALOG: + return "dialog"; + case SimStop::WARP: + return "warp"; + case SimStop::LIMIT: + return "limit"; + } + return "?"; +} + + +GridSim::GridSim(SimWorld world, const TilePos& start, Direction facing, bool can_run) + : m_world(std::move(world)) +{ + m_player.pos = sim_pos(start); + m_player.facing = facing; + m_player.can_run = can_run; +} + +void GridSim::place(SimWorld world, const TilePos& start, std::optional facing){ + m_world = std::move(world); + m_player.pos = sim_pos(start); + if (facing){ + m_player.facing = *facing; + } + m_player.dialog = false; + m_player.trainer.reset(); + m_player.warped.reset(); +} + +// Moves into the next tile toward `direction`, `part` of the way (a whole +// tile or less), from a position on the grid along it; adds what it moved +// to `moved`. A ledge hops two whole tiles and a door warps, however far +// the press goes. Nothing if the player may go on, else why the move ended. +std::optional GridSim::enter_next(Direction direction, int part, int& moved){ + const StoryWorld& world = *m_world.world; + const std::vector ahead = tiles_ahead(m_player.pos, direction); + if (ahead.size() == 1){ + const TilePos from = nearest_tile(m_player.pos); + const std::optional landing = PokemonBDSP::step(world, from, direction); + if (landing && *landing != ahead[0]){ + part = 2 * SIM_TILE; + }else if (!landing && is_door(world, ahead[0])){ + m_player.pos = sim_pos(ahead[0]); + m_player.warped = ahead[0]; + moved += SIM_TILE; + return SimStop::WARP; + }else if (!landing){ + return SimStop::BLOCKED; + } + } + // Off the grid, both tiles must be open: a door frame or a corner stops us. + for (const TilePos& pos : ahead){ + if (ahead.size() > 1 && !standable(world, pos)){ + return SimStop::BLOCKED; + } + } + along(m_player.pos, direction) += sign_of(direction) * part; + moved += part; + return std::nullopt; +} + +// On reaching a whole tile: a scene's trigger or a trainer's sight stops us. +std::optional GridSim::arrive(){ + const TilePos tile = nearest_tile(m_player.pos); + for (const SimTrigger& trigger : m_world.triggers){ + const bool fired = std::find(m_player.fired.begin(), m_player.fired.end(), trigger.name) != m_player.fired.end(); + if (!fired && std::find(trigger.tiles.begin(), trigger.tiles.end(), tile) != trigger.tiles.end()){ + m_player.fired.push_back(trigger.name); + m_player.dialog = true; + return SimStop::DIALOG; + } + } + for (const TrainerSight& trainer : m_world.trainers){ + const bool fought = std::find(m_player.fought.begin(), m_player.fought.end(), trainer.name) != m_player.fought.end(); + const std::vector sight = sight_tiles(*m_world.world, trainer); + if (!fought && std::find(sight.begin(), sight.end(), tile) != sight.end()){ + m_player.dialog = true; + m_player.trainer = trainer.name; + return SimStop::DIALOG; + } + } + return std::nullopt; +} + +// Moves up to `distance` toward `direction`. +SimMove GridSim::advance(Direction direction, int distance){ + m_player.facing = direction; + if (m_player.dialog){ + return {.stop = SimStop::DIALOG}; + } + int moved = 0; + while (moved < distance){ + const int offset = along(m_player.pos, direction) % SIM_TILE; + if (offset != 0){ + // Part of a tile to the next whole one: no new tile is entered. + const int to_whole = sign_of(direction) > 0 ? SIM_TILE - offset : offset; + const int part = std::min(to_whole, distance - moved); + along(m_player.pos, direction) += sign_of(direction) * part; + moved += part; + }else if (const std::optional stop = enter_next(direction, std::min(SIM_TILE, distance - moved), moved)){ + return {.stop = *stop, .distance = moved}; + } + if (along(m_player.pos, direction) % SIM_TILE == 0){ + if (const std::optional stop = arrive()){ + return {.stop = *stop, .distance = moved}; + } + } + } + return {.stop = SimStop::DONE, .distance = moved}; +} + +SimMove GridSim::tap(Direction direction, Milliseconds hold, bool stick){ + const Milliseconds tile_time = route_ms_per_tile(direction, MoveSpeed::WALK); + if (!stick && hold < 100ms){ + m_player.facing = direction; + return {.stop = m_player.dialog ? SimStop::DIALOG : SimStop::DONE, .time = hold}; + } + // The stick moves as far as it is held; the D-pad whole tiles. + int distance = (int)(hold.count() * SIM_TILE / tile_time.count()); + if (!stick){ + distance = std::max(1, (int)((hold + tile_time / 2) / tile_time)) * SIM_TILE; + } + SimMove move = advance(direction, distance); + move.time = hold; + return move; +} + +SimMove GridSim::hold(Direction direction, Milliseconds limit, SimPace pace){ + const Milliseconds tile_time = per_tile(direction, pace, m_player.can_run); + const int64_t tiles = limit < ROUTE_START_DELAY ? 0 : (limit - ROUTE_START_DELAY) / tile_time; + int moved = 0; + SimStop stop = SimStop::LIMIT; + while (moved < tiles * SIM_TILE){ + const SimMove step = advance(direction, SIM_TILE); + moved += step.distance; + if (step.stop != SimStop::DONE){ + stop = step.stop; + break; + } + } + if (stop == SimStop::LIMIT){ + return {.stop = stop, .distance = moved, .time = limit}; + } + // A stop is seen once the screen has held still a while. + const Milliseconds seen = stop == SimStop::BLOCKED ? 800ms : 0ms; + return {.stop = stop, .distance = moved, .time = ROUTE_START_DELAY + tile_time * moved / SIM_TILE + seen}; +} + +bool GridSim::press_a(){ + if (m_player.dialog){ + return true; + } + const StoryWorld& world = *m_world.world; + const TilePos faced = nearest_tile(m_player.pos).next(m_player.facing); + const std::optional tile = world.tile(faced); + m_player.dialog = world.occupied(faced) != StoryWorld::NOBODY || (tile && *tile == TileKind::SIGN); + for (const TrainerSight& trainer : m_world.trainers){ + if (m_player.dialog && trainer.pos == faced){ + m_player.trainer = trainer.name; + } + } + return m_player.dialog; +} + +void GridSim::end_dialogue(){ + if (m_player.trainer){ + m_player.fought.push_back(*m_player.trainer); + } + m_player.trainer.reset(); + m_player.dialog = false; +} + + +bool sim_can_step(const GridSim& sim, Direction direction){ + GridSim probe = sim; + const SimMove move = probe.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); + return move.distance > 0; +} + + +namespace{ + +WalkStop walk_stop(SimStop stop){ + switch (stop){ + case SimStop::BLOCKED: + return WalkStop::BLOCKED; + case SimStop::DIALOG: + return WalkStop::DIALOG; + case SimStop::WARP: + return WalkStop::TRANSITION; + case SimStop::DONE: + case SimStop::LIMIT: + return WalkStop::LIMIT; + } + return WalkStop::LIMIT; +} + +} + +WalkResult GridSimRouteIO::walk_to_wall(const RouteWallWalk& walk){ + const SimPace pace = walk.options.pace == DpadPace::RUN ? SimPace::RUN : SimPace::WALK; + const SimMove move = m_sim.hold(walk.direction, walk.limit, pace); + m_notes.push_back(std::format("[Sim] {}: held {} to {} at {}.", walk.label, direction_name(walk.direction), + sim_stop_name(move.stop), to_string(m_sim.player().pos))); + return {.stop = walk_stop(move.stop), .walked = move.time, .wild_battles = 0, .moved_chunks = (size_t)(move.distance / SIM_TILE)}; +} + +// A tap a tile, as walk_tiles_dpad() taps: two taps in a row that move +// nothing end the walk. +WalkResult GridSimRouteIO::walk_taps(Direction direction, size_t tiles, const std::string& label){ + WalkResult result{.stop = WalkStop::LIMIT, .walked = 0ms, .wild_battles = 0, .moved_chunks = 0}; + size_t still = 0; + for (size_t tap = 0; tap < tiles && result.stop == WalkStop::LIMIT; tap++){ + const SimMove move = m_sim.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); + result.walked += 800ms; + still = move.distance > 0 ? 0 : still + 1; + result.moved_chunks += move.distance > 0 ? 1 : 0; + if (move.stop == SimStop::DIALOG || move.stop == SimStop::WARP){ + result.stop = walk_stop(move.stop); + }else if (still >= 2){ + result.stop = WalkStop::BLOCKED; + } + } + m_notes.push_back(std::format("[Sim] {}: {} taps {}, {} moved, {} at {}.", label, tiles, direction_name(direction), + result.moved_chunks, walk_stop_name(result.stop), to_string(m_sim.player().pos))); + return result; +} + +void GridSimRouteIO::press(const RouteInput& input){ + for (size_t press = 0; press < input.count; press++){ + switch (input.kind){ + case RouteInputKind::DPAD: + m_sim.tap(input.direction, input.hold, false); + break; + case RouteInputKind::STICK: + m_sim.tap(input.direction, input.hold, true); + break; + case RouteInputKind::BUTTON_A: + m_sim.press_a(); + break; + case RouteInputKind::WAIT: + case RouteInputKind::PAUSE: + break; + } + } +} + +bool GridSimRouteIO::dialog_within(Milliseconds, Color){ + return m_sim.player().dialog; +} + +bool GridSimRouteIO::holding_against_wall(Direction direction, const std::string&){ + return !sim_can_step(m_sim, direction); +} + +void GridSimRouteIO::look(const std::function&){} + +void GridSimRouteIO::note(const std::string& text, RouteNote kind){ + m_notes.push_back(text); + if (kind == RouteNote::FAILURE){ + throw GridSimFailure{text}; + } +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h new file mode 100644 index 0000000000..4cb6b329fc --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h @@ -0,0 +1,181 @@ +/* BDSP AutoStory - Grid simulation + * + * From: https://github.com/PokemonAutomation/ + * + * A player walking on a story world, by the movement measured on the + * console, so walks that go wrong only on the Switch go wrong in tests: + * + * - A D-pad tap of 40 to 80 ms only turns; 160 ms steps a whole tile. + * - The D-pad held walks whole tiles until something stops it. + * - A stick tap moves part of a tile (100 ms about 0.6 tile across), and + * later D-pad moves keep that offset: the player is off the tile grid. + * - Walls, signs and people on their tiles stop the player. Off the grid + * the player covers two tiles, and both must be open: a door frame or + * a tree corner stops a player a part of a tile off. + * - Walking onto a door warps, but only from its exact column or row. + * - A trainer whose sight line the player steps into stops the walk with + * their dialogue; so does a scene's trigger tile. + * + * Positions are in thousandths of a tile, so parts of tiles add up exactly. + * GridSimRouteIO is the walker's RouteIO on a GridSim. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_GridSim_H +#define PokemonAutomation_PokemonBDSP_AutoStory_GridSim_H + +#include +#include +#include +#include "Common/Cpp/Time.h" +#include "Pokemon/Navigation/Pokemon_Grid.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.h" +#include "PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_StoryWorld.h" +#include "PokemonBDSP_AutoStory_RouteIO.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +// A tile, in the simulation's units. +inline constexpr int SIM_TILE = 1000; + +// A world position in thousandths of a tile: the player covers the square +// from (x, y) to (x + SIM_TILE, y + SIM_TILE). +struct SimPos{ + int x = 0; + int y = 0; + bool operator==(const SimPos& other) const = default; +}; +SimPos sim_pos(const Pokemon::TilePos& tile); +// Whether the player stands exactly on a tile. +bool on_grid(const SimPos& pos); +// The tile the player mostly stands on. +Pokemon::TilePos nearest_tile(const SimPos& pos); +// "(12.375, 5)". +std::string to_string(const SimPos& pos); + +// A scene that starts when the player steps onto one of `tiles` (a coord +// event): its dialogue opens and the player stops. It fires once. +struct SimTrigger{ + std::string name; + std::vector tiles; +}; + +// Where the simulated player walks: the world (which must outlive the +// simulation), its trainers and its scene triggers, in world tiles. +struct SimWorld{ + const StoryWorld* world = nullptr; + std::vector trainers = {}; + std::vector triggers = {}; +}; + +enum class SimStop{ + DONE, // the input finished + BLOCKED, // stopped by a wall, a person, or a door off its column + DIALOG, // a trainer saw us or a scene started; the player is frozen + WARP, // stepped onto a door + LIMIT, // a held walk ran out of time +}; +const char* sim_stop_name(SimStop stop); + +// What one input did. +struct SimMove{ + SimStop stop = SimStop::DONE; + int distance = 0; // thousandths of a tile + Milliseconds time = Milliseconds(0); +}; + +// The player as the simulation has them. +struct SimPlayer{ + SimPos pos; + Pokemon::Direction facing = Pokemon::Direction::NORTH; + // Whether B runs: the Running Shoes (00.03). + bool can_run = false; + // A dialogue is open: a trainer's, a scene's or a talk's. + bool dialog = false; + // The trainer whose dialogue is open, if one is. + std::optional trainer; + // The door the last warp stepped onto. + std::optional warped; + std::vector fought = {}; + std::vector fired = {}; +}; + +// How long a held walk takes per tile in `direction`. +enum class SimPace{ + WALK, + RUN, // B held: a run once the player has the Running Shoes +}; + +class GridSim{ +public: + GridSim(SimWorld world, const Pokemon::TilePos& start, Pokemon::Direction facing, bool can_run); + + // One press toward `direction`: the D-pad or the stick, held for `hold`. + SimMove tap(Pokemon::Direction direction, Milliseconds hold, bool stick); + // Holds toward `direction` until something stops the player or `limit` + // runs out. Moves whole tiles, from wherever the player stands. + SimMove hold(Pokemon::Direction direction, Milliseconds limit, SimPace pace); + // A: talks to a person or a sign on the faced tile. Returns whether a + // dialogue is open. + bool press_a(); + // The open dialogue ends: a trainer's battle is won and they see us no more. + void end_dialogue(); + // Puts the player on `start` of `world`: after a warp or a scene. + void place(SimWorld world, const Pokemon::TilePos& start, std::optional facing); + + const SimPlayer& player() const{ + return m_player; + } + const StoryWorld& world() const{ + return *m_world.world; + } + +private: + SimMove advance(Pokemon::Direction direction, int distance); + std::optional enter_next(Pokemon::Direction direction, int part, int& moved); + std::optional arrive(); + + SimWorld m_world; + SimPlayer m_player; +}; + +// Whether the player could take a step toward `direction` from where they stand. +bool sim_can_step(const GridSim& sim, Pokemon::Direction direction); + + +// The walker's RouteIO on a GridSim. A failure throws GridSimFailure. +class GridSimRouteIO : public RouteIO{ +public: + // Every line noted goes to `notes`, which must outlive this. + GridSimRouteIO(GridSim& sim, std::vector& notes) + : m_sim(sim) + , m_notes(notes) + {} + + virtual WalkResult walk_to_wall(const RouteWallWalk& walk) override; + virtual WalkResult walk_taps(Pokemon::Direction direction, size_t tiles, const std::string& label) override; + virtual void press(const RouteInput& input) override; + virtual bool dialog_within(Milliseconds wait, Color color) override; + virtual bool holding_against_wall(Pokemon::Direction direction, const std::string& label) override; + virtual void look(const std::function& at) override; + virtual void note(const std::string& text, RouteNote kind) override; + +private: + GridSim& m_sim; + std::vector& m_notes; +}; + +// What GridSimRouteIO throws for RouteNote::FAILURE. +struct GridSimFailure{ + std::string message; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index e0425f314a..c781e6b2c0 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1558,6 +1558,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySimulation_Tests.h + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp + Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.cpp Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Indoors.h Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_Location.cpp @@ -1682,8 +1684,14 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Settings.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryPlaces.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryTable_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Talks.cpp @@ -1702,6 +1710,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Maps_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_PlannedRoute_Tests.h + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp + Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_RoutePlanner_Tests.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryMaps_Tests.cpp From fd349b3ebbcab1d2ceeaec3d4517969e2827131a Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 02:13:14 -0800 Subject: [PATCH 2/6] BDSP AutoStory sim: scenes from the maps' coord events The story maps list their coord events ("coord_events": the decomp's zone_event triggers, from pret/pokeplatinum's events files, and Mom's call as measured on the console). story_coord_events() reads them, and the story sim arms each while its story variable holds, so a step onto a trigger tile starts its scene in the simulation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_CoordEvents.cpp | 109 ++++++++++++++++++ .../Data/PokemonBDSP_AutoStory_CoordEvents.h | 50 ++++++++ .../Programs/AutoStory/Maps/JubilifeCity.json | 4 + .../AutoStory/Maps/PlayerHouse1F.json | 4 + .../Programs/AutoStory/Maps/Route202.json | 3 + .../Programs/AutoStory/Maps/SandgemTown.json | 3 + .../PokemonBDSP_AutoStory_StorySim_Tests.cpp | 98 ++++++++++++---- SerialPrograms/cmake/SourceFiles.cmake | 2 + 8 files changed, 248 insertions(+), 25 deletions(-) create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp create mode 100644 SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp new file mode 100644 index 0000000000..3a500b0a54 --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp @@ -0,0 +1,109 @@ +/* BDSP AutoStory - Coord events + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Json/JsonArray.h" +#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Json/JsonValue.h" +#include "PokemonBDSP_AutoStory_Maps.h" +#include "PokemonBDSP_AutoStory_CoordEvents.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + +using Pokemon::TilePos; + + +namespace{ + +int read_int(const JsonObject& object, const std::string& key, const std::string& where){ + const JsonValue* value = object.get_value(key); + if (value == nullptr || !value->is_integer()){ + throw ParseException(where + key + " is missing or not an integer."); + } + return (int)value->to_integer_default(); +} + +std::string read_string(const JsonObject& object, const std::string& key, const std::string& where){ + const JsonValue* value = object.get_value(key); + const std::string* text = value == nullptr ? nullptr : value->to_string(); + if (text == nullptr){ + throw ParseException(where + key + " is missing or not a string."); + } + return *text; +} + +CoordEvent read_event(const JsonObject& object, const std::string& where){ + CoordEvent event{ + .name = read_string(object, "name", where), + .tiles = {}, + .var = read_string(object, "var", where), + .value = read_int(object, "value", where), + .source = read_string(object, "source", where), + }; + const int x = read_int(object, "x", where); + const int y = read_int(object, "y", where); + const int width = read_int(object, "width", where); + const int length = read_int(object, "length", where); + if (width < 1 || length < 1){ + throw ParseException(where + "width and length must be at least 1."); + } + for (int row = y; row < y + length; row++){ + for (int col = x; col < x + width; col++){ + event.tiles.push_back({col, row}); + } + } + return event; +} + +std::vector read_events(const std::string& name, const std::string& json){ + std::vector events; + const JsonValue root = parse_json(json); + const JsonObject* object = root.to_object(); + const JsonValue* list = object == nullptr ? nullptr : object->get_value("coord_events"); + if (list == nullptr){ + return events; + } + const JsonArray* array = list->to_array(); + if (array == nullptr){ + throw ParseException(name + ".json: coord_events is not an array."); + } + for (size_t c = 0; c < array->size(); c++){ + const std::string where = std::format("{}.json: coord_events[{}].", name, c); + const JsonObject* item = (*array)[c].to_object(); + if (item == nullptr){ + throw ParseException(where + " is not an object."); + } + events.push_back(read_event(*item, where)); + } + return events; +} + +} + + +const std::vector& story_coord_events(std::string_view map){ + static const std::map, std::less<>> by_map = []{ + std::map, std::less<>> out; + for (const auto& [name, json] : bdsp_map_json()){ + out.emplace(name, read_events(name, json)); + } + return out; + }(); + if (auto iter = by_map.find(map); iter != by_map.end()){ + return iter->second; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, std::format("No BDSP story map named \"{}\".", map)); +} + + + +} +} +} diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h new file mode 100644 index 0000000000..2c38f3c9df --- /dev/null +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h @@ -0,0 +1,50 @@ +/* BDSP AutoStory - Coord events + * + * From: https://github.com/PokemonAutomation/ + * + * The scene triggers of the story maps: stepping onto one of an event's + * tiles while its story variable holds `value` starts its script. They are + * the decomp's coord events (zone_event), read from a map's + * "coord_events", which the shared map schema leaves to the game: + * + * "coord_events": [{"name": "catch_tutorial", "x": 21, "y": 25, "width": 1, "length": 5, + * "var": "VAR_ROUTE_202_STATE", "value": 0, "source": "..."}, ...] + * + * A trigger seen on the console that the decomp does not have names the run + * in `source`. + * + */ + +#ifndef PokemonAutomation_PokemonBDSP_AutoStory_CoordEvents_H +#define PokemonAutomation_PokemonBDSP_AutoStory_CoordEvents_H + +#include +#include +#include +#include "Pokemon/Navigation/Pokemon_Grid.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +struct CoordEvent{ + std::string name; + // The map's own tiles: `width` columns from x, `length` rows from y. + std::vector tiles; + std::string var; + int value = 0; + std::string source; +}; + +// The coord events of the map `map` (a JSON map's name), none if it lists +// none. Throws InternalProgramError for an unknown map, and ParseException +// for an event that does not parse. +const std::vector& story_coord_events(std::string_view map); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/JubilifeCity.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/JubilifeCity.json index 71aed1d875..9b7952fa7c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/JubilifeCity.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/JubilifeCity.json @@ -135,5 +135,9 @@ {"name": "clown_tv_station", "kind": "npc", "x": 36, "y": 16}, {"name": "kid_with_nds_29", "kind": "npc", "x": 52, "y": 46}, {"name": "kid_with_nds_30", "kind": "npc", "x": 54, "y": 46} + ], + "coord_events": [ + {"name": "dawn_at_the_steps", "x": 45, "y": 60, "width": 4, "length": 1, "var": "VAR_JUBILIFE_CITY_STATE", "value": 0, "source": "pret/pokeplatinum res/field/events/events_jubilife_city.json coord_events[0] (173, 796), script 2"}, + {"name": "poketch_president", "x": 44, "y": 40, "width": 5, "length": 1, "var": "VAR_POKETCH_CAMPAIGN_STATE", "value": 1, "source": "pret/pokeplatinum res/field/events/events_jubilife_city.json coord_events[4] (172, 776), script 17"} ] } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json index 2a7a89546d..4fd4f96d58 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/PlayerHouse1F.json @@ -25,5 +25,9 @@ ], "objects": [ {"name": "mom_0", "kind": "npc", "x": 7, "y": 8, "facing": "north"} + ], + "coord_events": [ + {"name": "moms_warning", "x": 6, "y": 10, "width": 1, "length": 1, "var": "VAR_PLAYER_HOUSE_STATE", "value": 1, "source": "pret/pokeplatinum res/field/events/events_twinleaf_town_player_house_1f.json coord_events[0] (6, 10), script 5"}, + {"name": "moms_call", "x": 6, "y": 9, "width": 1, "length": 1, "var": "CONSOLE_MOMS_CALL", "value": 0, "source": "console r45, r46 (2026-11-01): after her heal Mom calls us back as we step above the door; no coord event in the decomp"} ] } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route202.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route202.json index e52d694739..23f8ef2d6c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route202.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/Route202.json @@ -137,5 +137,8 @@ {"x": 19, "y": 29, "glyph": "#", "measured_by": "r253, 2026-11-01"}, {"x": 20, "y": 29, "glyph": "#", "measured_by": "r253, 2026-11-01"}, {"x": 21, "y": 30, "glyph": "#", "measured_by": "r254, 2026-11-01"} + ], + "coord_events": [ + {"name": "catch_tutorial", "x": 21, "y": 25, "width": 1, "length": 5, "var": "VAR_ROUTE_202_STATE", "value": 0, "source": "pret/pokeplatinum res/field/events/events_route_202.json coord_events[0] (180, 825), script 6"} ] } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/SandgemTown.json b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/SandgemTown.json index 4f7a4e61b4..3f05806a7d 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/SandgemTown.json +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Maps/SandgemTown.json @@ -51,5 +51,8 @@ }, "objects": [ {"name": "youngster_0", "kind": "npc", "x": 27, "y": 23, "wander": {"x0": 26, "y0": 22, "x1": 28, "y1": 24}} + ], + "coord_events": [ + {"name": "dawn_walks_us_to_the_lab", "x": 4, "y": 10, "width": 1, "length": 6, "var": "VAR_SANDGEM_TOWN_STATE", "value": 0, "source": "pret/pokeplatinum res/field/events/events_sandgem_town.json coord_events[0] (164, 842), script 2"} ] } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp index a10d802ffe..3d2e618f32 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp @@ -11,7 +11,8 @@ * Events, battles, shops and saves are stubs: a dialogue they open is * closed, and the table's pose_after puts the player where they end. A * heal checks that a walk north from where we stand enters a door. A scene - * starts when the player steps onto its trigger (SimTrigger). Rooms with no + * starts when the player steps onto one of the maps' coord events while + * its story variable holds (story_vars()). Rooms with no * map (the Trainers' School) are passed over. * */ @@ -27,6 +28,8 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Pokemon/AutoStory/Pokemon_StorySegments.h" #include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "Data/PokemonBDSP_AutoStory_CoordEvents.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" #include "Utils/PokemonBDSP_AutoStory_GridSim.h" #include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" #include "PokemonBDSP_AutoStory_Events.h" @@ -115,35 +118,51 @@ struct RowTrigger{ SimTrigger trigger; }; -std::vector column(int x, int y0, int y1){ - std::vector tiles; - for (int y = y0; y <= y1; y++){ - tiles.push_back({x, y}); - } - return tiles; +// When a story variable holds the value its coord events wait for: from +// row `row` on, once the step `after` has run ("" from the row's start). +struct VarState{ + std::string var; + int value; + std::string row; + std::string after; +}; + +// The story variables of 00.04 to 00.05c: Dawn's catching tutorial waits on +// Route 202 from 00.04's start, Mom calls us back once she has healed us, +// and the Poketch president waits on Jubilife's row 40 in 00.05b. +std::vector story_vars(){ + return { + {.var = "VAR_ROUTE_202_STATE", .value = 0, .row = "00.04", .after = ""}, + {.var = "CONSOLE_MOMS_CALL", .value = 0, .row = "00.04", .after = "mom_heals_us"}, + {.var = "VAR_POKETCH_CAMPAIGN_STATE", .value = 1, .row = "00.05b", .after = ""}, + }; } -std::vector row_of(int y, int x0, int x1){ - std::vector tiles; - for (int x = x0; x <= x1; x++){ - tiles.push_back({x, y}); + +// The maps' coord events (story_coord_events()) whose variable holds. +std::optional var_state(const CoordEvent& event){ + for (const VarState& state : story_vars()){ + if (state.var == event.var && state.value == event.value){ + return state; + } } - return tiles; + return std::nullopt; +} + +RowTrigger row_trigger(const std::string& map, const CoordEvent& event, const VarState& state){ + return {.row = state.row, .after = state.after, .map = map, .trigger = {.name = event.name, .tiles = event.tiles}}; } -// The coord events of 00.04 to 00.05c, from the decomp as the table cites -// them: Mom's call above the door after she heals us (r45, r46), Dawn's -// catching tutorial on Route 202, and the Poketch president on Jubilife's -// row 40. std::vector story_triggers(){ - const auto trigger = [](std::string row, std::string after, std::string map, std::string name, std::vector tiles){ - return RowTrigger{.row = std::move(row), .after = std::move(after), .map = std::move(map), - .trigger = SimTrigger{.name = std::move(name), .tiles = std::move(tiles)}}; - }; - return { - trigger("00.04", "mom_heals_us", "PlayerHouse1F", "moms_call", {TilePos{6, 9}}), - trigger("00.04", "", "Route202", "catch_tutorial", column(21, 25, 29)), - trigger("00.05b", "", "JubilifeCity", "president", row_of(40, 44, 48)), - }; + std::vector triggers; + for (const auto& [map, json] : bdsp_map_json()){ + for (const CoordEvent& event : story_coord_events(map)){ + const std::optional state = var_state(event); + if (state){ + triggers.push_back(row_trigger(map, event, *state)); + } + } + } + return triggers; } @@ -357,6 +376,34 @@ UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ } +// Every coord event has ground the player can step onto, and the ones +// the story's variables arm are there. +class Test_CoordEvents : public UnitTest{ +public: + Test_CoordEvents() + : UnitTest("PokemonBDSP::AutoStory - Story sim: the maps' coord events are on open ground or doors") + {} + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + for (const auto& [map, json] : bdsp_map_json()){ + const StoryWorld* world = story_plan_world(map, map); + for (const CoordEvent& event : world == nullptr ? std::vector{} : story_coord_events(map)){ + const auto open = [&](const TilePos& pos){ + const TilePos tile = story_to_world(*world, map, pos); + return standable(*world, tile) || world->tile(tile) == Pokemon::TileKind::DOOR; + }; + // The decomp's boxes may run into walls (Sandgem's (4, 10..15)). + if (!std::ranges::any_of(event.tiles, open)){ + return std::format("{} {}: no tile is open ground or a door.", map, event.name); + } + } + } + const size_t armed = story_triggers().size(); + return armed == story_vars().size() ? UnitTestResult(true) + : UnitTestResult(std::format("Expected {} coord events armed by the story's variables, found {}.", story_vars().size(), armed)); + } +}; + + class Test_StorySim : public UnitTest{ public: Test_StorySim(std::string name, std::string from, std::string to) @@ -381,6 +428,7 @@ class Test_StorySim : public UnitTest{ void add_tests_AutoStory_StorySim(UnitTestDatabase& database){ + database.add(); database.add("00.04 to 00.04d: home, the catch, Sandgem's Center, Route 202", "00.04", "00.04d"); database.add("00.05 to 00.05c: Jubilife's Center, School, clowns and Poketch", "00.05", "00.05c"); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index c781e6b2c0..6840ca42f7 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1700,6 +1700,8 @@ file(GLOB LIBRARY_SOURCES Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_Walks.h Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.cpp Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_WildBattle.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_Maps.h Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_RoutePlanner.cpp From c32d50cd858cff9f138692e087871a189f7811fd Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 07:39:44 -0800 Subject: [PATCH 3/6] BDSP AutoStory sim: miss each fade and banner in turn (#162) The story sim's StoryStepIO can now miss the Nth door fade (run_into_door reports no fade though the player warped; the settle after it sees none either, as on the console once the fade is over) or the Nth map banner (run_until_map). A new test runs 00.04 to 00.05c once per fade and banner the clean run makes, and requires each miss to be got over or to stop the run in the step that missed: a later failure means the miss went unseen. Today all four (3 fades, 1 banner) stop in their own step. A missed fade costs settle()'s 120 s and a failed attempt. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StorySim_Tests.cpp | 131 ++++++++++++++++-- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp index 3d2e618f32..e339b8832c 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include "Common/Cpp/Exceptions.h" #include "Common/Cpp/TestRunners/UnitTestDatabase.h" @@ -57,12 +58,29 @@ using Pokemon::StoryStepKind; using Pokemon::TilePos; +// Which detections the simulation misses on purpose (#162): the `fade`th +// door fade (run_into_door(), from 0) or the `banner`th map banner +// (run_until_map()) of the run goes unseen, though the player did go +// through. The console missed both (r75, r81). +// The counts and flags below are the run's: how many fades and banners it +// looked for, whether a miss happened since the executor last looked, and +// whether a missed fade is still unseen. +struct SimMisses{ + std::optional fade; + std::optional banner; + size_t fades = 0; + size_t banners = 0; + bool missed = false; + bool fade_unseen = false; +}; + // StoryStepIO on the simulation. class SimStoryStepIO : public StoryStepIO{ public: - SimStoryStepIO(GridSim& sim, std::vector& notes) + SimStoryStepIO(GridSim& sim, std::vector& notes, SimMisses& misses) : m_sim(sim) , m_route(sim, notes) + , m_misses(misses) {} virtual RouteIO& route() override{ @@ -80,9 +98,14 @@ class SimStoryStepIO : public StoryStepIO{ } } virtual bool run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&) override{ + const bool miss = m_misses.banner == m_misses.banners++; for (Milliseconds ran = 0ms; ran < limit; ran += ROUTE_RUN_MS_PER_TILE_ACROSS){ const SimMove move = m_sim.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); if (story_to_local(m_sim.world(), map, nearest_tile(m_sim.player().pos))){ + if (miss){ + m_misses.missed = true; + continue; + } return true; } if (move.distance == 0 || move.stop != SimStop::DONE){ @@ -92,20 +115,30 @@ class SimStoryStepIO : public StoryStepIO{ return false; } virtual bool run_into_door(Direction direction, bool run) override{ - return m_sim.hold(direction, 12000ms, run ? SimPace::RUN : SimPace::WALK).stop == SimStop::WARP; + const bool warped = m_sim.hold(direction, 12000ms, run ? SimPace::RUN : SimPace::WALK).stop == SimStop::WARP; + if (warped && m_misses.fade == m_misses.fades++){ + m_misses.missed = true; + m_misses.fade_unseen = true; + return false; + } + return warped; } // A scene may take us to another map (Mom walks us out, 00.04): with a // dialogue open the stub scene plays it. Otherwise only a door fades. + // A fade missed on the way in stays missed: it was over before settle() + // looked, which then fails as it does on the console after 120 s. virtual void settle(Button, const std::string& label, bool fade) override{ - if (fade && !m_sim.player().warped && !m_sim.player().dialog){ + if (fade && ((!m_sim.player().warped && !m_sim.player().dialog) || m_misses.fade_unseen)){ m_route.note(label + ": no fade, so we are not where the step goes (map mismatch).", RouteNote::FAILURE); } + m_misses.fade_unseen = false; m_sim.end_dialogue(); } private: GridSim& m_sim; GridSimRouteIO m_route; + SimMisses& m_misses; }; @@ -169,10 +202,11 @@ std::vector story_triggers(){ // The program's walking steps on the simulation, the rest stubbed. class SimExecutor : public Pokemon::StoryStepExecutor{ public: - explicit SimExecutor(Logger& logger) + explicit SimExecutor(Logger& logger, SimMisses misses = {}) : m_logger(logger) + , m_misses(misses) , m_sim({}, {0, 0}, Direction::NORTH, true) - , m_io(m_sim, m_notes) + , m_io(m_sim, m_notes, m_misses) , m_walks(m_io) { // Mom stands a row above the decomp's (7, 8) from 00.04 on (r43). @@ -183,6 +217,17 @@ class SimExecutor : public Pokemon::StoryStepExecutor{ const std::string& error() const{ return m_error; } + const SimMisses& misses() const{ + return m_misses; + } + // The row and step where an injected miss happened, and where the run + // first failed: "" if none. + const std::string& missed_step() const{ + return m_missed_step; + } + const std::string& failed_step() const{ + return m_failed_step; + } virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ m_row = data.id; m_walks.begin_attempt(data.id); @@ -221,6 +266,7 @@ class SimExecutor : public Pokemon::StoryStepExecutor{ Logger& m_logger; std::vector m_notes; + SimMisses m_misses; GridSim m_sim; SimStoryStepIO m_io; StoryWalkSteps m_walks; @@ -232,6 +278,8 @@ class SimExecutor : public Pokemon::StoryStepExecutor{ // Whether the step running now is on a room with no map. bool m_passed_over = false; std::string m_error; + std::string m_missed_step; + std::string m_failed_step; }; // The world the player walks on for `map`: the planner's, with what the @@ -349,17 +397,20 @@ Outcome SimExecutor::run(RunState& state, const StoryStep& step){ outcome = Pokemon::outcome_failed("sim_mismatch", step.name + ": " + mismatch); } m_logger.log(std::format("[{}] {}: {}, the player at {}.", m_row, step.name, Pokemon::to_string(outcome), to_string(m_sim.player().pos))); + if (std::exchange(m_misses.missed, false) && m_missed_step.empty()){ + m_missed_step = m_row + " " + step.name; + } if (!outcome.succeeded() && m_error.empty()){ m_error = m_row + " " + Pokemon::to_string(outcome); + m_failed_step = m_row + " " + step.name; } arm(step.name); return outcome; } -// Runs `range` of the table on the simulation, stopping at the first failure. -UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ - SimExecutor executor(logger); +// Runs `range` of the table on `executor`, stopping at the first failure. +Pokemon::SegmentReport run_rows_on(SimExecutor& executor, Logger& logger, const Pokemon::SegmentRange& range){ std::set saved; Pokemon::Segment segment = make_story_segment(story_checkpoints(), executor, saved); Pokemon::SegmentPolicy policy; @@ -368,7 +419,13 @@ UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ hooks.reload = [](RunState&){ return Pokemon::outcome_ok(); }; hooks.log = [&logger](const std::string& line){ logger.log(line); }; RunState state; - const Pokemon::SegmentReport report = Pokemon::run_segment(state, segment, range, policy, hooks); + return Pokemon::run_segment(state, segment, range, policy, hooks); +} + +// Runs `range` of the table on the simulation, stopping at the first failure. +UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ + SimExecutor executor(logger); + const Pokemon::SegmentReport report = run_rows_on(executor, logger, range); if (!report.completed){ return std::format("stopped at {} {}: {}", report.failed_checkpoint, report.failed_event, executor.error()); } @@ -404,6 +461,61 @@ class Test_CoordEvents : public UnitTest{ }; +// A detection the console can miss, missed in turn at each place the run +// makes it (#162). Each miss must either be got over (the run completes, +// every walk ending on its tiles) or stop the run in the step that missed: +// a miss that lets the run go on and fail later went unseen, and a later +// step started from somewhere it did not expect. +class Test_StorySimMisses : public UnitTest{ +public: + Test_StorySimMisses(std::string name, std::string from, std::string to) + : UnitTest("PokemonBDSP::AutoStory - Story sim, detector misses: " + std::move(name)) + , m_range{.from = std::move(from), .to = std::move(to)} + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + SimExecutor clean(logger); + if (!run_rows_on(clean, logger, m_range).completed){ + return "the run fails without misses: " + clean.error(); + } + std::vector runs; + for (size_t i = 0; i < clean.misses().fades; i++){ + runs.push_back({.fade = i}); + } + for (size_t i = 0; i < clean.misses().banners; i++){ + runs.push_back({.banner = i}); + } + logger.log(std::format("{} fades and {} banners to miss.", clean.misses().fades, clean.misses().banners)); + std::vector unseen; + for (const SimMisses& misses : runs){ + SimExecutor executor(logger, misses); + const Pokemon::SegmentReport report = run_rows_on(executor, logger, m_range); + const std::string what = misses.fade ? std::format("fade {}", *misses.fade) : std::format("banner {}", *misses.banner); + logger.log(std::format("Missed {} in {}: {}.", what, executor.missed_step(), + report.completed ? "got over it" : "stopped in " + executor.failed_step() + ": " + executor.error())); + if (!report.completed && executor.failed_step() != executor.missed_step()){ + unseen.push_back(std::format("{} missed in {}, failed later in {}: {}", + what, executor.missed_step(), executor.failed_step(), executor.error())); + } + } + if (!unseen.empty()){ + std::string out = "Misses that went unseen:"; + for (const std::string& line : unseen){ + out += "\n " + line; + } + return out; + } + return true; + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + Pokemon::SegmentRange m_range; +}; + + class Test_StorySim : public UnitTest{ public: Test_StorySim(std::string name, std::string from, std::string to) @@ -431,6 +543,7 @@ void add_tests_AutoStory_StorySim(UnitTestDatabase& database){ database.add(); database.add("00.04 to 00.04d: home, the catch, Sandgem's Center, Route 202", "00.04", "00.04d"); database.add("00.05 to 00.05c: Jubilife's Center, School, clowns and Poketch", "00.05", "00.05c"); + database.add("00.04 to 00.05c", "00.04", "00.05c"); } From d5818159fa9af98dc08cd94e03fa36922444dd50 Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 00:14:16 -0800 Subject: [PATCH 4/6] BDSP AutoStory sim: run the whole table, check every row's end The story sim ran only 00.04 to 00.05c. It now runs 00.00 to 01.11, the whole table and each row alone, with the program's own walking steps; scenes, events, battles and shops end where the table says. Each row must start where the row before left the player, and every walking step with a declared or worked-out end is checked against it in world tiles (a leg out of Twinleaf ends on Route 201's tiles). What it needed: the story variables of the coord events the table walks onto (00.01 to 00.03, 01.01 to 01.07), the houses' stairs as doors, Barry in Oreburgh's Center door until his lines (01.03), and B running only once the shoes' scene has given them (Route 201 timed a walk as a run). A tap or hold through a door or cave mouth that misses the fade now goes on onto the step's far side, and a banner reads three tiles into its map ("a tile or a few past the border"). The exit tests run r75's and r81's old open-ended walks that way: they end in Route 203's pond pocket (52, 9) and on Route 204's cliff (20, 30), and the end-tile check fails them; the table's taps to the banner never end there, whatever is missed. The detector-miss test covers the whole table (20 fades, 8 banners), each miss on its own row and the next. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonBDSP_AutoStory_StorySim_Tests.cpp | 620 ++++++++++++++---- .../Utils/PokemonBDSP_AutoStory_GridSim.cpp | 5 +- .../Utils/PokemonBDSP_AutoStory_GridSim.h | 7 +- 3 files changed, 497 insertions(+), 135 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp index e8b368a84b..18835105ff 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp @@ -13,13 +13,22 @@ * heal checks that a walk north from where we stand enters a door. A scene * starts when the player steps onto one of the maps' coord events while * its story variable holds (story_vars()). Rooms with no - * map (the Trainers' School) are passed over. + * map (the Trainers' School) are passed over. Each row must start where + * the row before left the player. + * + * A tap or hold through a door or cave mouth stops on its tile, as the + * console stops at the fade; a hold that does not see the fade goes on + * onto the step's pose_after and on along it, and a location banner reads + * only a few tiles into its map (SIM_BANNER_TILES). The exit tests run + * r75's and r81's old open-ended walks that way. * */ #include #include #include +#include +#include #include #include #include @@ -74,13 +83,46 @@ struct SimMisses{ bool fade_unseen = false; }; -// StoryStepIO on the simulation. +// How many tiles a held walk goes on into a map before its location +// banner reads: "it shows a tile or a few past the border" (the table's +// route_201_by_the_path(), measured on the console). Standing still, it +// reads at once. Through a cave mouth the fade comes first, and a hold +// whose fade went unseen goes on the same way (r75, r81). +constexpr size_t SIM_BANNER_TILES = 3; + +// What a run on the simulation did. +struct SimRun{ + // The first failure, as "row outcome", or "". + std::string error; + // The rows and steps where an injected miss happened and where the run + // first failed, as "row step": "" if none. + std::string missed_step; + std::string failed_step; + // Walking steps that end where nothing says, as "row step". + std::vector unchecked; + // Each row run, with the fades and banners looked for before it. + struct RowStart{ + std::string row; + size_t fades; + size_t banners; + }; + std::vector rows; + // Where the player stood after the last step, if placed. + const StoryWorld* world = nullptr; + TilePos end; +}; + +// StoryStepIO on the simulation. A walk through a door or a cave mouth +// stops on its tile, as the console stops at the fade; a hold that misses +// the fade carries on, onto `far_side()` (the step's pose_after: the +// sim's maps have no warps' destinations) and on along it. class SimStoryStepIO : public StoryStepIO{ public: - SimStoryStepIO(GridSim& sim, std::vector& notes, SimMisses& misses) + SimStoryStepIO(GridSim& sim, std::vector& notes, SimMisses& misses, std::function far_side) : m_sim(sim) , m_route(sim, notes) , m_misses(misses) + , m_far_side(std::move(far_side)) {} virtual RouteIO& route() override{ @@ -89,58 +131,16 @@ class SimStoryStepIO : public StoryStepIO{ virtual void fight(const std::string&) override{ m_sim.end_dialogue(); } - virtual void walk_until_stopped(Direction direction, size_t tiles, const std::string& label) override{ - m_sim.tap(direction, ROUTE_TURN_TAP, false); - const Milliseconds limit = ROUTE_START_DELAY + 800ms + route_ms_per_tile(direction) * (int64_t)(2 * tiles); - const SimMove move = m_sim.hold(direction, limit, SimPace::WALK); - if (move.stop == SimStop::DIALOG || move.stop == SimStop::WARP){ - m_route.note(label + ": " + sim_stop_name(move.stop) + " instead of a stop.", RouteNote::FAILURE); - } - } - virtual bool run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&) override{ - const bool miss = m_misses.banner == m_misses.banners++; - for (Milliseconds ran = 0ms; ran < limit; ran += ROUTE_RUN_MS_PER_TILE_ACROSS){ - const SimMove move = m_sim.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); - if (story_to_local(m_sim.world(), map, nearest_tile(m_sim.player().pos))){ - if (miss){ - m_misses.missed = true; - continue; - } - return true; - } - if (move.distance == 0 || move.stop != SimStop::DONE){ - return false; - } - } - return false; - } + virtual void walk_until_stopped(Direction direction, size_t tiles, const std::string& label) override; + virtual bool run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&) override; virtual bool run_into_door(Direction direction, bool run) override{ const bool warped = m_sim.hold(direction, 12000ms, run ? SimPace::RUN : SimPace::WALK).stop == SimStop::WARP; - if (warped && m_misses.fade == m_misses.fades++){ - m_misses.missed = true; + if (warped && fade_missed()){ m_misses.fade_unseen = true; return false; } return warped; } - // A banner miss: the fade was seen, the banner not within 8 s, so the - // step fails there, as on the console. - virtual std::optional tap_to_banner(Direction direction, const std::string&, size_t max_taps) override{ - for (size_t tap = 1; tap <= max_taps; tap++){ - const SimMove move = m_sim.tap(direction, 160ms, false); - if (move.stop == SimStop::WARP){ - if (m_misses.banner == m_misses.banners++){ - m_misses.missed = true; - return std::nullopt; - } - return tap; - } - if (move.stop == SimStop::DIALOG){ - return std::nullopt; - } - } - return std::nullopt; - } // A scene may take us to another map (Mom walks us out, 00.04): with a // dialogue open the stub scene plays it. Otherwise only a door fades. // A fade missed on the way in stays missed: it was over before settle() @@ -152,13 +152,92 @@ class SimStoryStepIO : public StoryStepIO{ m_misses.fade_unseen = false; m_sim.end_dialogue(); } + virtual std::optional tap_to_banner(Direction direction, const std::string& map, size_t max_taps) override; private: + // Whether the run misses this fade or this banner (SimMisses). + bool fade_missed(){ + const bool miss = m_misses.fade == m_misses.fades++; + m_misses.missed |= miss; + return miss; + } + bool banner_missed(){ + const bool miss = m_misses.banner == m_misses.banners++; + m_misses.missed |= miss; + return miss; + } + bool on_map(const std::string& map) const{ + return story_to_local(m_sim.world(), map, nearest_tile(m_sim.player().pos)).has_value(); + } + GridSim& m_sim; GridSimRouteIO m_route; SimMisses& m_misses; + std::function m_far_side; }; +// A hold until something stops us. A door stops it at the fade, which +// fails the step; a fade missed lets the hold go on past the mouth (r81's +// timed walk south out of the Ravaged Path, onto Route 204's cliff). +void SimStoryStepIO::walk_until_stopped(Direction direction, size_t tiles, const std::string& label){ + m_sim.tap(direction, ROUTE_TURN_TAP, false); + Milliseconds limit = ROUTE_START_DELAY + 800ms + route_ms_per_tile(direction) * (int64_t)(2 * tiles); + SimMove move = m_sim.hold(direction, limit, SimPace::WALK); + if (move.stop == SimStop::WARP && fade_missed() && m_far_side()){ + limit -= std::min(limit, move.time); + move = m_sim.hold(direction, limit, SimPace::WALK); + } + if (move.stop == SimStop::DIALOG || move.stop == SimStop::WARP){ + m_route.note(label + ": " + sim_stop_name(move.stop) + " instead of a stop.", RouteNote::FAILURE); + } +} + +// The D-pad, a tile at a time, until `map`'s banner reads; through a door +// the walk goes on beyond it (the fade eats no tiles of a hold). +bool SimStoryStepIO::run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&){ + const bool miss = banner_missed(); + size_t on = 0; + for (Milliseconds ran = 0ms; ran < limit; ran += ROUTE_RUN_MS_PER_TILE_ACROSS){ + SimMove move = m_sim.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); + const bool crossed = move.stop == SimStop::WARP && m_far_side(); + const bool stopped = !crossed && (move.distance == 0 || move.stop != SimStop::DONE); + on = on_map(map) ? on + 1 : 0; + if (on > 0 && (on > SIM_BANNER_TILES || stopped) && !miss){ + return true; + } + if (stopped){ + return false; + } + } + return false; +} + +// D-pad taps, each looked at, until the fade and then the banner (the +// console's ConsoleStoryStepIO::tap_to_banner()). Each look stands still, +// so a banner on the far side reads at once. A fade seen ends the taps, +// banner or not; with the fade missed the next look still sees the banner. +std::optional SimStoryStepIO::tap_to_banner(Direction direction, const std::string& map, size_t max_taps){ + bool crossed = false; + for (size_t tap = 1; tap <= max_taps; tap++){ + const SimMove move = m_sim.tap(direction, 160ms, false); + if (move.stop == SimStop::DIALOG){ + return std::nullopt; + } + bool faded = false; + if (move.stop == SimStop::WARP){ + faded = !fade_missed(); + crossed = m_far_side(); + } + if (crossed && on_map(map) && !banner_missed()){ + return tap; + } + if (faded){ + return std::nullopt; + } + } + return std::nullopt; +} + // A scene's trigger the simulation fires from row `row` on, once the step // `after` has run ("" from the row's start), on `map`'s `tiles`. @@ -178,14 +257,36 @@ struct VarState{ std::string after; }; -// The story variables of 00.04 to 00.05c: Dawn's catching tutorial waits on -// Route 202 from 00.04's start, Mom calls us back once she has healed us, -// and the Poketch president waits on Jubilife's row 40 in 00.05b. +// The story variables whose coord events the table walks onto, from the +// row (and step) the table's reasons give, with the maps' coord events +// (Diamond's zone events; Platinum's for the houses): Barry's doorstep and +// Mom's warning in 00.01, Barry's Route 201 warning, then its pull-back +// row 19 once he has warned us (r14, r16); the lake's entrance in 00.02; +// Rowan's stop and Dawn at Sandgem's edge in 00.03; Dawn's catching +// tutorial on Route 202 from 00.04's start, Mom's call once she has healed +// us; the Poketch president on Jubilife's row 40 in 00.05b; Barry on Route +// 203 in 01.01; the Gate's Hiker and Oreburgh's welcome in 01.02; Barry's +// bump in Oreburgh after Roark in 01.05; and Rowan in Jubilife in 01.07. +// Left out: the Twinleaf guitarist (spent upstairs), Platinum's rival +// steps around our bedroom's start, Dawn at Jubilife's steps (BDSP starts +// her below them: an event), and Floaroma's (10..12, 29) (Diamond only). std::vector story_vars(){ return { + {.var = "VAR_TWINLEAF_TOWN_RIVAL_TRIGGER_STATE", .value = 0, .row = "00.01", .after = ""}, + {.var = "VAR_PLAYER_HOUSE_STATE", .value = 1, .row = "00.01", .after = ""}, + {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 0, .row = "00.01", .after = ""}, + {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 1, .row = "00.01", .after = "barry_warns_us"}, + {.var = "VAR_VERITY_LAKEFRONT_STATE", .value = 0, .row = "00.02", .after = ""}, + {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 2, .row = "00.03", .after = ""}, + {.var = "VAR_SANDGEM_TOWN_STATE", .value = 0, .row = "00.03", .after = ""}, {.var = "VAR_ROUTE_202_STATE", .value = 0, .row = "00.04", .after = ""}, {.var = "CONSOLE_MOMS_CALL", .value = 0, .row = "00.04", .after = "mom_heals_us"}, {.var = "VAR_POKETCH_CAMPAIGN_STATE", .value = 1, .row = "00.05b", .after = ""}, + {.var = "VAR_ROUTE_203_RIVAL_STATE", .value = 0, .row = "01.01", .after = ""}, + {.var = "VAR_OREBURGH_GATE_1F_HIKER_STATE", .value = 1, .row = "01.02", .after = ""}, + {.var = "VAR_OREBURGH_CITY_STATE", .value = 0, .row = "01.02", .after = ""}, + {.var = "VAR_OREBURGH_CITY_STATE", .value = 2, .row = "01.05", .after = ""}, + {.var = "VAR_JUBILIFE_CITY_STATE", .value = 3, .row = "01.07", .after = ""}, }; } @@ -217,37 +318,64 @@ std::vector story_triggers(){ } +// Someone the maps do not have, who stands on `tile` of `map` and is +// talked to there or walked round: from row `row` on if `until` is "", +// else in `row` until its step `until` has run. +struct SimPerson{ + std::string name; + std::string map; + TilePos tile; + std::string row; + std::string until; +}; +std::vector sim_people(){ + return { + // Mom stands a row above the decomp's (7, 8) from 00.04 on (r43). + {.name = "mom", .map = "PlayerHouse1F", .tile = {7, 7}, .row = "00.04", .until = ""}, + // Barry stands in the Center's door until his lines (01.03; Roark + // ignored A until then, r101 and r104). + {.name = "barry", .map = "OreburghCity", .tile = {47, 20}, .row = "01.03", .until = "barrys_lines"}, + }; +} + +// A house's stairs, entered by a step `side` from the map's "stairs" +// point into what the map draws as a wall (the table's 00.01: r8 and r9 +// for ours, r11 for Barry's). +struct SimStairs{ + std::string map; + Direction side; +}; +std::vector sim_stairs(){ + return { + {.map = "PlayerHouse2F", .side = Direction::EAST}, + {.map = "RivalHouse1F", .side = Direction::EAST}, + {.map = "RivalHouse2F", .side = Direction::WEST}, + }; +} + + // The program's walking steps on the simulation, the rest stubbed. class SimExecutor : public Pokemon::StoryStepExecutor{ public: explicit SimExecutor(Logger& logger, SimMisses misses = {}) : m_logger(logger) , m_misses(misses) - , m_sim({}, {0, 0}, Direction::NORTH, true) - , m_io(m_sim, m_notes, m_misses) + , m_sim({}, {0, 0}, Direction::NORTH, false) + , m_io(m_sim, m_notes, m_misses, [this]{ return cross(); }) , m_walks(m_io) - { - // Mom stands a row above the decomp's (7, 8) from 00.04 on (r43). - m_house = *story_plan_world("PlayerHouse1F", "PlayerHouse1F"); - m_house.block({7, 7}); - } + {} - const std::string& error() const{ - return m_error; - } const SimMisses& misses() const{ return m_misses; } - // The row and step where an injected miss happened, and where the run - // first failed: "" if none. - const std::string& missed_step() const{ - return m_missed_step; - } - const std::string& failed_step() const{ - return m_failed_step; + const SimRun& result() const{ + return m_result; } virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ m_row = data.id; + m_start_mismatch = data.start ? off_pose(*data.start) : ""; + m_ran.clear(); + m_result.rows.push_back({.row = data.id, .fades = m_misses.fades, .banners = m_misses.banners}); m_walks.begin_attempt(data.id); arm(""); } @@ -279,7 +407,9 @@ class SimExecutor : public Pokemon::StoryStepExecutor{ SimWorld setup(const StoryWorld* world) const; void arm(const std::string& after); bool sync(const Pose& pose, const StoryWorld* on = nullptr); - std::string check(const StoryStep& step, const Outcome& outcome) const; + bool cross(); + std::string off_pose(const Pose& pose) const; + std::string check(const StoryStep& step, const Outcome& outcome); Outcome run_step(RunState& state, const StoryStep& step); Logger& m_logger; @@ -288,30 +418,57 @@ class SimExecutor : public Pokemon::StoryStepExecutor{ GridSim m_sim; SimStoryStepIO m_io; StoryWalkSteps m_walks; - StoryWorld m_house; + // The planner's worlds with sim_people() on them, by world and who. + mutable std::map, std::unique_ptr> m_peopled; + // The steps of this row that have run. + std::set m_ran; std::string m_row; std::vector m_armed; + // Where the step running now ends, if it says: a door's far side. + std::optional m_far; + // Why the player is not on this row's start, or "". + std::string m_start_mismatch; // Whether the player has been put on a map yet. bool m_placed = false; // Whether the step running now is on a room with no map. bool m_passed_over = false; - std::string m_error; - std::string m_missed_step; - std::string m_failed_step; + SimRun m_result; }; // The world the player walks on for `map`: the planner's, with what the // console showed that the maps lack. Nothing for a room with no map. const StoryWorld* SimExecutor::sim_world(const std::string& map) const{ const StoryWorld* world = story_plan_world(map, map); - if (world == story_plan_world("PlayerHouse1F", "PlayerHouse1F") && m_row >= "00.04"){ - return &m_house; + std::vector here; + std::string who; + for (const SimPerson& person : sim_people()){ + const bool now = person.until.empty() ? m_row >= person.row : m_row == person.row && !m_ran.contains(person.until); + if (world != nullptr && now && story_plan_world(person.map, person.map) == world){ + here.push_back(person); + who += person.name + ";"; + } } - return world; + if (here.empty()){ + return world; + } + std::unique_ptr& peopled = m_peopled[{world, who}]; + if (!peopled){ + peopled = std::make_unique(*world); + for (const SimPerson& person : here){ + peopled->block(story_to_world(*world, person.map, person.tile)); + } + } + return peopled.get(); } SimWorld SimExecutor::setup(const StoryWorld* world) const{ - SimWorld out{.world = world, .trainers = story_trainers(*world)}; + // B runs once the shoes' scene has given them: the scene ends at home. + SimWorld out{.world = world, .trainers = story_trainers(*world), .can_run = m_walks.can_run()}; + for (const SimStairs& stairs : sim_stairs()){ + if (sim_world(stairs.map) == world){ + out.doors.push_back(story_to_world(*world, stairs.map, map_point(stairs.map, "stairs").next(stairs.side))); + } + } for (const RowTrigger& armed : m_armed){ if (sim_world(armed.map) != world){ continue; @@ -340,8 +497,9 @@ void SimExecutor::arm(const std::string& after){ } } -// Puts the player on `pose` unless they stand on one of its tiles already. -// Returns whether the pose is on a map the simulation has. +// Puts the player on `pose`, on the world `on` if given, unless they stand +// on one of its tiles already. Returns whether the pose is on a map the +// simulation has. bool SimExecutor::sync(const Pose& pose, const StoryWorld* on){ const std::vector tiles = Pokemon::tiles_of(pose); const StoryWorld* world = on != nullptr ? on : pose.map.empty() ? nullptr : sim_world(pose.map); @@ -359,6 +517,39 @@ bool SimExecutor::sync(const Pose& pose, const StoryWorld* on){ return true; } +// Through a door or a cave mouth onto the step's far side, the player's +// facing kept: a hold goes on from there. +bool SimExecutor::cross(){ + const StoryWorld* world = m_far ? sim_world(m_far->map) : nullptr; + const std::vector tiles = m_far ? Pokemon::tiles_of(*m_far) : std::vector{}; + if (world == nullptr || tiles.empty()){ + return false; + } + m_sim.place(setup(world), story_to_world(*world, m_far->map, tiles.front()), std::nullopt); + return true; +} + +// Why the player does not stand on one of `pose`'s tiles, or "" if they +// do or the simulation cannot tell (no map, or not placed yet). +std::string SimExecutor::off_pose(const Pose& pose) const{ + const std::vector tiles = Pokemon::tiles_of(pose); + if (!m_placed || tiles.empty() || sim_world(pose.map) == nullptr){ + return ""; + } + const SimPlayer& player = m_sim.player(); + const TilePos at = nearest_tile(player.pos); + // In world tiles: a pose a leg worked out from the map it started on + // may lie past that map's edge, on the next map (Twinleaf's exit). + // story_to_world() throws for a map not on the player's world. + try{ + const auto there = [&](const TilePos& tile){ return story_to_world(m_sim.world(), pose.map, tile) == at; }; + if (on_grid(player.pos) && std::ranges::any_of(tiles, there)){ + return ""; + } + }catch (const InternalProgramError&){} + return std::format("ended at {} ({}), not on {}.", to_string(player.pos), describe(m_sim.world(), at), Pokemon::to_string(pose)); +} + Outcome SimExecutor::heal(RunState& state, const StoryStep& step){ if (sync(state.pose) && m_sim.hold(Direction::NORTH, 3000ms, SimPace::RUN).stop != SimStop::WARP){ return Pokemon::outcome_failed("no_door", step.name + ": no door north of " + to_string(m_sim.player().pos)); @@ -366,39 +557,42 @@ Outcome SimExecutor::heal(RunState& state, const StoryStep& step){ return Pokemon::outcome_ok(); } -// Why a walking step did not end where the table says, or "". -std::string SimExecutor::check(const StoryStep& step, const Outcome& outcome) const{ +// Why a walking step did not end where the table says, or "". A step +// through a door the simulation did not cross is not checked: the sim's +// maps have no warps' destinations. +std::string SimExecutor::check(const StoryStep& step, const Outcome& outcome){ const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || step.kind == StoryStepKind::STEP; const std::optional expected = step.pose_after ? step.pose_after : outcome.pose; - if (!walks || !expected || Pokemon::tiles_of(*expected).empty() || !m_placed){ + if (!walks || !m_placed){ return ""; } - const SimPlayer& player = m_sim.player(); - const std::optional local = story_to_local(m_sim.world(), expected->map, nearest_tile(player.pos)); - if (!local){ - return player.warped ? "" : std::format("ended on {} at {}, not on {}.", describe(m_sim.world(), nearest_tile(player.pos)), - to_string(player.pos), expected->map); + if (!expected || Pokemon::tiles_of(*expected).empty()){ + m_result.unchecked.push_back(m_row + " " + step.name); + return ""; } - const std::vector tiles = Pokemon::tiles_of(*expected); - if (!on_grid(player.pos) || std::find(tiles.begin(), tiles.end(), *local) == tiles.end()){ - return std::format("ended at {} ({}), not on {}.", to_string(player.pos), describe(m_sim.world(), nearest_tile(player.pos)), - Pokemon::to_string(*expected)); + const SimPlayer& player = m_sim.player(); + if (player.warped && !story_to_local(m_sim.world(), expected->map, nearest_tile(player.pos))){ + return ""; } - return ""; + return off_pose(*expected); } Outcome SimExecutor::run_step(RunState& state, const StoryStep& step){ + if (!m_start_mismatch.empty()){ + return Pokemon::outcome_failed("sim_mismatch", "the row before " + std::exchange(m_start_mismatch, "")); + } + m_far = step.pose_after; if (story_events().contains(step.variant)){ return stub(); } - // A scene goes on from where the step before left us. + // A scene goes on from where the step before left us. A route walks + // on the world it plans on (Jubilife alone has no Route 204 north of it). const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || step.kind == StoryStepKind::STEP || step.kind == StoryStepKind::TALK; - // A route walks on the world it plans on (Jubilife alone has no Route - // 204 north of it); the house keeps Mom where the console showed her. const StoryWorld* on = step.kind == StoryStepKind::ROUTE && step.target && !state.pose.map.empty() ? story_plan_world(state.pose.map, step.target->map) : nullptr; - if (on != nullptr && on == story_plan_world("PlayerHouse1F", "PlayerHouse1F")){ + // A world of one map is sim_world()'s, with its people. + if (on != nullptr && on == story_plan_world(state.pose.map, state.pose.map)){ on = nullptr; } if (walks && !sync(state.pose, on)){ @@ -421,23 +615,39 @@ Outcome SimExecutor::run(RunState& state, const StoryStep& step){ if (!mismatch.empty()){ outcome = Pokemon::outcome_failed("sim_mismatch", step.name + ": " + mismatch); } + // What the simulation does not play (scenes, events, battles) ends + // where the table says it does. + if (outcome.succeeded() && step.pose_after){ + sync(*step.pose_after); + } m_logger.log(std::format("[{}] {}: {}, the player at {}.", m_row, step.name, Pokemon::to_string(outcome), to_string(m_sim.player().pos))); - if (std::exchange(m_misses.missed, false) && m_missed_step.empty()){ - m_missed_step = m_row + " " + step.name; + if (std::exchange(m_misses.missed, false) && m_result.missed_step.empty()){ + m_result.missed_step = m_row + " " + step.name; } - if (!outcome.succeeded() && m_error.empty()){ - m_error = m_row + " " + Pokemon::to_string(outcome); - m_failed_step = m_row + " " + step.name; + if (!outcome.succeeded() && m_result.error.empty()){ + // The walker's last notes, to see why. + for (size_t i = m_notes.size() - std::min(m_notes.size(), 40); i < m_notes.size(); i++){ + m_logger.log(std::format("[{}] note: {}", m_row, m_notes[i])); + } + m_result.error = m_row + " " + Pokemon::to_string(outcome); + m_result.failed_step = m_row + " " + step.name; } + m_result.world = m_placed ? &m_sim.world() : nullptr; + m_result.end = nearest_tile(m_sim.player().pos); + m_ran.insert(step.name); arm(step.name); return outcome; } -// Runs `range` of the table on `executor`, stopping at the first failure. -Pokemon::SegmentReport run_rows_on(SimExecutor& executor, Logger& logger, const Pokemon::SegmentRange& range){ +// Runs `range` of `rows` (the table unless given) on `executor`, stopping +// at the first failure. +Pokemon::SegmentReport run_rows_on( + SimExecutor& executor, Logger& logger, const Pokemon::SegmentRange& range, + const std::vector& rows = story_checkpoints() +){ std::set saved; - Pokemon::Segment segment = make_story_segment(story_checkpoints(), executor, saved); + Pokemon::Segment segment = make_story_segment(rows, executor, saved); Pokemon::SegmentPolicy policy; policy.stop_at_first_failure = true; Pokemon::SegmentHooks hooks; @@ -451,12 +661,143 @@ Pokemon::SegmentReport run_rows_on(SimExecutor& executor, Logger& logger, const UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ SimExecutor executor(logger); const Pokemon::SegmentReport report = run_rows_on(executor, logger, range); + for (const std::string& step : executor.result().unchecked){ + logger.log("No end to check after " + step + "."); + } if (!report.completed){ - return std::format("stopped at {} {}: {}", report.failed_checkpoint, report.failed_event, executor.error()); + return std::format("stopped at {} {}: {}", report.failed_checkpoint, report.failed_event, executor.result().error); } return true; } +// The table with row `row`'s step of `step`'s name replaced by `step`. +std::vector table_with(const std::string& row, const StoryStep& step){ + std::vector rows = story_checkpoints(); + for (StoryCheckpoint& checkpoint : rows){ + for (StoryStep& old : checkpoint.steps){ + if (checkpoint.id == row && old.name == step.name){ + old = step; + return rows; + } + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No step " + step.name + " in " + row); +} + +bool ends_on(const SimRun& run, const Pose& pose){ + const std::vector tiles = Pokemon::tiles_of(pose); + const std::optional local = run.world ? story_to_local(*run.world, pose.map, run.end) : std::nullopt; + return local && std::find(tiles.begin(), tiles.end(), *local) != tiles.end(); +} +std::string end_of(const SimRun& run){ + return run.world ? describe(*run.world, run.end) : "nowhere"; +} + + +// A way out of a map that a hold walked on past (r75, r81), against the +// table's tap to the banner. The old step, with `misses`, must end on +// `overshoot`, caught there by the end-tile check. The table's step must +// never end there, whichever one fade or banner of the row is missed: it +// ends on its tile or stops in that step. +class Test_StorySimExit : public UnitTest{ +public: + Test_StorySimExit(std::string name, std::string row, StoryStep old_step, SimMisses misses, Pose overshoot) + : UnitTest("PokemonBDSP::AutoStory - Story sim, exits: " + std::move(name)) + , m_row(std::move(row)) + , m_old(std::move(old_step)) + , m_misses(std::move(misses)) + , m_overshoot(std::move(overshoot)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + const std::string old = old_form(logger); + return old.empty() ? table_form(logger) : UnitTestResult(old); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + // Why the old step did not go wrong as on the console, or "". + std::string old_form(Logger& logger) const{ + SimExecutor executor(logger, m_misses); + const std::vector rows = table_with(m_row, m_old); + run_rows_on(executor, logger, {.from = m_row, .to = m_row}, rows); + const SimRun& run = executor.result(); + logger.log(std::format("The old {}: {}, ended on {}.", m_old.name, run.error, end_of(run))); + if (run.failed_step != m_row + " " + m_old.name || !ends_on(run, m_overshoot)){ + return std::format("the old {} should end on {} and fail there; it ended on {}: {}", + m_old.name, Pokemon::to_string(m_overshoot), end_of(run), run.error.empty() ? "no failure" : run.error); + } + return ""; + } + UnitTestResult table_form(Logger& logger) const{ + SimExecutor clean(logger); + if (!run_rows_on(clean, logger, {.from = m_row, .to = m_row}).completed){ + return "the table's row fails without misses: " + clean.result().error; + } + std::vector runs; + for (size_t i = 0; i < clean.misses().fades; i++){ + runs.push_back({.fade = i}); + } + for (size_t i = 0; i < clean.misses().banners; i++){ + runs.push_back({.banner = i}); + } + for (const SimMisses& misses : runs){ + SimExecutor executor(logger, misses); + run_rows_on(executor, logger, {.from = m_row, .to = m_row}); + if (ends_on(executor.result(), m_overshoot)){ + return std::format("the table's {} ended on {} with a miss in {}.", m_old.name, end_of(executor.result()), + executor.result().missed_step); + } + } + return true; + } + + std::string m_row; + StoryStep m_old; + SimMisses m_misses; + Pose m_overshoot; +}; + +// r75: out of the Oreburgh Gate as a run west until Route 203's banner +// (the old event west_until_transition()). The banner reads a few tiles +// in: the run ends west of the Gate's warp tile (55, 9), in the pocket by +// the pond (Route 203's x 49 to 54, rows 6 to 13). +StoryStep r75_old_gate_exit(){ + StoryStep step; + step.kind = StoryStepKind::LEGS; + step.name = "out_of_oreburgh_gate"; + step.legs = {{.direction = Direction::WEST, .tiles = 8}}; + step.variant = "to_map"; + step.pose_after = Pokemon::tile_pose("Route203", {TilePos{55, 9}}, Direction::WEST); + return step; +} +Pose r75_pond_pocket(){ + std::vector tiles; + for (int y = 6; y <= 13; y++){ + for (int x = 49; x <= 54; x++){ + tiles.push_back({x, y}); + } + } + return Pokemon::tile_pose("Route203", tiles); +} +// r81: out of the Ravaged Path as a timed walk south (the old event's +// 4 s walk_dir()), its fade missed: on past cave_exit (20, 26) to the +// cliff, column 20 down to row 30. +StoryStep r81_old_cave_exit(){ + StoryStep step; + step.kind = StoryStepKind::LEGS; + step.name = "out_of_the_ravaged_path"; + step.legs = {{.direction = Direction::SOUTH, .tiles = 4}}; + step.variant = "to_wall"; + step.pose_after = Pokemon::tile_pose("Route204North", {TilePos{20, 26}}, Direction::SOUTH); + return step; +} +Pose r81_below_the_mouth(){ + return Pokemon::tile_pose("Route204North", {TilePos{20, 27}, TilePos{20, 28}, TilePos{20, 29}, TilePos{20, 30}}); +} + // Every coord event has ground the player can step onto, and the ones // the story's variables arm are there. @@ -490,7 +831,8 @@ class Test_CoordEvents : public UnitTest{ // makes it (#162). Each miss must either be got over (the run completes, // every walk ending on its tiles) or stop the run in the step that missed: // a miss that lets the run go on and fail later went unseen, and a later -// step started from somewhere it did not expect. +// step started from somewhere it did not expect. Each miss runs its own +// row and the next, which checks where the row left us. class Test_StorySimMisses : public UnitTest{ public: Test_StorySimMisses(std::string name, std::string from, std::string to) @@ -501,42 +843,50 @@ class Test_StorySimMisses : public UnitTest{ try{ SimExecutor clean(logger); if (!run_rows_on(clean, logger, m_range).completed){ - return "the run fails without misses: " + clean.error(); - } - std::vector runs; - for (size_t i = 0; i < clean.misses().fades; i++){ - runs.push_back({.fade = i}); - } - for (size_t i = 0; i < clean.misses().banners; i++){ - runs.push_back({.banner = i}); + return "the run fails without misses: " + clean.result().error; } logger.log(std::format("{} fades and {} banners to miss.", clean.misses().fades, clean.misses().banners)); std::vector unseen; - for (const SimMisses& misses : runs){ - SimExecutor executor(logger, misses); - const Pokemon::SegmentReport report = run_rows_on(executor, logger, m_range); - const std::string what = misses.fade ? std::format("fade {}", *misses.fade) : std::format("banner {}", *misses.banner); - logger.log(std::format("Missed {} in {}: {}.", what, executor.missed_step(), - report.completed ? "got over it" : "stopped in " + executor.failed_step() + ": " + executor.error())); - if (!report.completed && executor.failed_step() != executor.missed_step()){ - unseen.push_back(std::format("{} missed in {}, failed later in {}: {}", - what, executor.missed_step(), executor.failed_step(), executor.error())); + for (size_t i = 0; i < clean.misses().fades + clean.misses().banners; i++){ + const std::string line = miss(logger, clean.result(), i < clean.misses().fades, i < clean.misses().fades ? i : i - clean.misses().fades); + if (!line.empty()){ + unseen.push_back(line); } } - if (!unseen.empty()){ - std::string out = "Misses that went unseen:"; - for (const std::string& line : unseen){ - out += "\n " + line; - } - return out; + std::string out = "Misses that went unseen:"; + for (const std::string& line : unseen){ + out += "\n " + line; } - return true; + return unseen.empty() ? UnitTestResult(true) : UnitTestResult(out); }catch (Exception& e){ return std::string(e.name()) + ": " + e.message(); } } private: + // Misses the `index`th fade (or banner) of the clean run `clean` in its + // row. Returns how it went unseen, or "". + static std::string miss(Logger& logger, const SimRun& clean, bool fade, size_t index){ + size_t k = 0; + while (k + 1 < clean.rows.size() && (fade ? clean.rows[k + 1].fades : clean.rows[k + 1].banners) <= index){ + k++; + } + const SimRun::RowStart& start = clean.rows[k]; + SimMisses misses; + (fade ? misses.fade : misses.banner) = index - (fade ? start.fades : start.banners); + SimExecutor executor(logger, misses); + const Pokemon::SegmentReport report = run_rows_on(executor, logger, + {.from = start.row, .to = clean.rows[std::min(k + 1, clean.rows.size() - 1)].row}); + const SimRun& run = executor.result(); + const std::string what = std::format("{} {}", fade ? "fade" : "banner", index); + logger.log(std::format("Missed {} in {}: {}.", what, run.missed_step, + report.completed ? "got over it" : "stopped in " + run.failed_step + ": " + run.error)); + if (report.completed || run.failed_step == run.missed_step){ + return ""; + } + return std::format("{} missed in {}, failed later in {}: {}", what, run.missed_step, run.failed_step, run.error); + } + Pokemon::SegmentRange m_range; }; @@ -566,9 +916,13 @@ class Test_StorySim : public UnitTest{ void add_tests_AutoStory_StorySim(UnitTestDatabase& database){ database.add(); - database.add("00.04 to 00.04d: home, the catch, Sandgem's Center, Route 202", "00.04", "00.04d"); - database.add("00.05 to 00.05c: Jubilife's Center, School, clowns and Poketch", "00.05", "00.05c"); - database.add("00.04 to 00.05c", "00.04", "00.05c"); + database.add("00.00 to 01.11", "00.00", "01.11"); + database.add("00.00 to 01.11: the whole table, from the new game to Floaroma", "00.00", "01.11"); + for (const StoryCheckpoint& row : story_checkpoints()){ + database.add(std::format("row {}", row.id), row.id, row.id); + } + database.add("r75, out of the Oreburgh Gate", "01.05", r75_old_gate_exit(), SimMisses{}, r75_pond_pocket()); + database.add("r81, out of the Ravaged Path", "01.09", r81_old_cave_exit(), SimMisses{.fade = 0}, r81_below_the_mouth()); } diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp index 302b175634..ab0bc9134b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp @@ -117,6 +117,9 @@ void GridSim::place(SimWorld world, const TilePos& start, std::optional GridSim::enter_next(Direction direction, int part, int& m const std::optional landing = PokemonBDSP::step(world, from, direction); if (landing && *landing != ahead[0]){ part = 2 * SIM_TILE; - }else if (!landing && is_door(world, ahead[0])){ + }else if (!landing && (is_door(world, ahead[0]) || std::find(m_world.doors.begin(), m_world.doors.end(), ahead[0]) != m_world.doors.end())){ m_player.pos = sim_pos(ahead[0]); m_player.warped = ahead[0]; moved += SIM_TILE; diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h index 4cb6b329fc..f28b20d067 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h @@ -64,11 +64,16 @@ struct SimTrigger{ }; // Where the simulated player walks: the world (which must outlive the -// simulation), its trainers and its scene triggers, in world tiles. +// simulation), its trainers and its scene triggers, in world tiles; and +// the tiles that warp like doors though the maps draw them as walls (a +// house's stairs, entered from beside them); and, if the caller knows, +// whether B runs from here on (the Running Shoes, 00.03). struct SimWorld{ const StoryWorld* world = nullptr; std::vector trainers = {}; std::vector triggers = {}; + std::vector doors = {}; + std::optional can_run = std::nullopt; }; enum class SimStop{ From a9b57b2e2acc019726159fc775981e74d5a9eb27 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 5 Nov 2026 22:43:05 -0800 Subject: [PATCH 5/6] BDSP AutoStory sim: align with PR #231 lint conventions --- .../PokemonBDSP_AutoStory_CoordEvents.cpp | 8 +- .../Data/PokemonBDSP_AutoStory_CoordEvents.h | 7 +- .../PokemonBDSP_AutoStory_GridSim_Tests.cpp | 54 +- .../PokemonBDSP_AutoStory_GridSim_Tests.h | 6 +- .../PokemonBDSP_AutoStory_StoryExecutor.cpp | 6 +- .../PokemonBDSP_AutoStory_StorySim_Tests.cpp | 1852 ++++++++--------- .../PokemonBDSP_AutoStory_StorySim_Tests.h | 6 +- .../PokemonBDSP_AutoStory_StoryStepIO.cpp | 8 +- .../PokemonBDSP_AutoStory_StoryStepIO.h | 7 +- .../PokemonBDSP_AutoStory_StoryWalkSteps.cpp | 8 +- .../PokemonBDSP_AutoStory_StoryWalkSteps.h | 7 +- .../Utils/PokemonBDSP_AutoStory_GridSim.cpp | 10 +- .../Utils/PokemonBDSP_AutoStory_GridSim.h | 10 +- 13 files changed, 1006 insertions(+), 983 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp index 3a500b0a54..c3fe3fea36 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.cpp @@ -85,7 +85,7 @@ std::vector read_events(const std::string& name, const std::string& return events; } -} +} // namespace const std::vector& story_coord_events(std::string_view map){ @@ -104,6 +104,6 @@ const std::vector& story_coord_events(std::string_view map){ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h index 2c38f3c9df..51f2d8514a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Data/PokemonBDSP_AutoStory_CoordEvents.h @@ -28,6 +28,7 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ +// Coordinate-triggered event parsed from BDSP map data. struct CoordEvent{ std::string name; // The map's own tiles: `width` columns from x, `length` rows from y. @@ -44,7 +45,7 @@ const std::vector& story_coord_events(std::string_view map); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp index 8d5d49b517..785c6a9638 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.cpp @@ -384,35 +384,63 @@ UnitTestResult test_door_routes(Logger& logger){ } +struct AutoStoryGridSimInput{ + std::string name; + std::function body; +}; + +struct AutoStoryGridSimExpectation{ +}; + +struct AutoStoryGridSimCase{ + AutoStoryGridSimInput input; + AutoStoryGridSimExpectation expectation; +}; + class Test_GridSim : public UnitTest{ public: - Test_GridSim(std::string name, std::function body) - : UnitTest("PokemonBDSP::AutoStory - Grid sim: " + std::move(name)) - , m_body(std::move(body)) + explicit Test_GridSim(AutoStoryGridSimCase test_case) + : UnitTest("PokemonBDSP::AutoStory - Grid sim: " + test_case.input.name) + , m_case(std::move(test_case)) {} virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ try{ - return m_body(logger); + return m_case.input.body(logger); }catch (Exception& e){ return std::string(e.name()) + ": " + e.message(); } } private: - std::function m_body; + AutoStoryGridSimCase m_case; }; -} - +} // namespace + + +const AutoStoryGridSimCase AUTO_STORY_GRID_SIM_CASES[] = { + { + .input = {"taps, holds, stick offsets, doors, people and trainers as measured", test_moves}, + .expectation = {}, + }, + { + .input = {"the table's routes end on their targets", test_table_routes}, + .expectation = {}, + }, + { + .input = {"a stick-tap turn misses the Sandgem Center and Jubilife School doors", test_door_routes}, + .expectation = {}, + }, +}; void add_tests_AutoStory_GridSim(UnitTestDatabase& database){ - database.add("taps, holds, stick offsets, doors, people and trainers as measured", test_moves); - database.add("the table's routes end on their targets", test_table_routes); - database.add("a stick-tap turn misses the Sandgem Center and Jubilife School doors", test_door_routes); + for (const auto& item : AUTO_STORY_GRID_SIM_CASES){ + database.add(item); + } } -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h index 16c74d7cce..d80dd04f8b 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_GridSim_Tests.h @@ -22,7 +22,7 @@ void add_tests_AutoStory_GridSim(UnitTestDatabase& database); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp index 9008d1f9f1..5b8f8ef3dd 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryExecutor.cpp @@ -320,6 +320,6 @@ Pokemon::Segment make_story_segment( -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp index 18835105ff..af21d4bf85 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp @@ -1,932 +1,920 @@ -/* BDSP AutoStory Story Sim Tests - * - * From: https://github.com/PokemonAutomation/ - * - * Rows of the story table run as the program runs them (the segment - * runner, make_story_segment()), with the program's own walking steps - * (StoryWalkSteps: ROUTE, LEGS, STEP, TALK, MASH) on the grid simulation. - * After each walking step the simulated player must stand, on the grid, - * on a tile the table gives; a step through a door must have warped. - * - * Events, battles, shops and saves are stubs: a dialogue they open is - * closed, and the table's pose_after puts the player where they end. A - * heal checks that a walk north from where we stand enters a door. A scene - * starts when the player steps onto one of the maps' coord events while - * its story variable holds (story_vars()). Rooms with no - * map (the Trainers' School) are passed over. Each row must start where - * the row before left the player. - * - * A tap or hold through a door or cave mouth stops on its tile, as the - * console stops at the fade; a hold that does not see the fade goes on - * onto the step's pose_after and on along it, and a location banner reads - * only a few tiles into its map (SIM_BANNER_TILES). The exit tests run - * r75's and r81's old open-ended walks that way. - * - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "Pokemon/AutoStory/Pokemon_StorySegments.h" -#include "Pokemon/AutoStory/Pokemon_TilePose.h" -#include "Data/PokemonBDSP_AutoStory_CoordEvents.h" -#include "Data/PokemonBDSP_AutoStory_Maps.h" -#include "Utils/PokemonBDSP_AutoStory_GridSim.h" -#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" -#include "PokemonBDSP_AutoStory_Events.h" -#include "PokemonBDSP_AutoStory_StoryExecutor.h" -#include "PokemonBDSP_AutoStory_StoryPlaces.h" -#include "PokemonBDSP_AutoStory_StoryStepIO.h" -#include "PokemonBDSP_AutoStory_StoryTable.h" -#include "PokemonBDSP_AutoStory_StoryWalkSteps.h" -#include "PokemonBDSP_AutoStory_StorySim_Tests.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonBDSP{ - - -namespace{ - -using namespace std::chrono_literals; -using Pokemon::Direction; -using Pokemon::Outcome; -using Pokemon::Pose; -using Pokemon::RunState; -using Pokemon::StoryStep; -using Pokemon::StoryStepKind; -using Pokemon::TilePos; - - -// Which detections the simulation misses on purpose (#162): the `fade`th -// door fade (run_into_door(), from 0) or the `banner`th map banner -// (run_until_map()) of the run goes unseen, though the player did go -// through. The console missed both (r75, r81). -// The counts and flags below are the run's: how many fades and banners it -// looked for, whether a miss happened since the executor last looked, and -// whether a missed fade is still unseen. -struct SimMisses{ - std::optional fade; - std::optional banner; - size_t fades = 0; - size_t banners = 0; - bool missed = false; - bool fade_unseen = false; -}; - -// How many tiles a held walk goes on into a map before its location -// banner reads: "it shows a tile or a few past the border" (the table's -// route_201_by_the_path(), measured on the console). Standing still, it -// reads at once. Through a cave mouth the fade comes first, and a hold -// whose fade went unseen goes on the same way (r75, r81). -constexpr size_t SIM_BANNER_TILES = 3; - -// What a run on the simulation did. -struct SimRun{ - // The first failure, as "row outcome", or "". - std::string error; - // The rows and steps where an injected miss happened and where the run - // first failed, as "row step": "" if none. - std::string missed_step; - std::string failed_step; - // Walking steps that end where nothing says, as "row step". - std::vector unchecked; - // Each row run, with the fades and banners looked for before it. - struct RowStart{ - std::string row; - size_t fades; - size_t banners; - }; - std::vector rows; - // Where the player stood after the last step, if placed. - const StoryWorld* world = nullptr; - TilePos end; -}; - -// StoryStepIO on the simulation. A walk through a door or a cave mouth -// stops on its tile, as the console stops at the fade; a hold that misses -// the fade carries on, onto `far_side()` (the step's pose_after: the -// sim's maps have no warps' destinations) and on along it. -class SimStoryStepIO : public StoryStepIO{ -public: - SimStoryStepIO(GridSim& sim, std::vector& notes, SimMisses& misses, std::function far_side) - : m_sim(sim) - , m_route(sim, notes) - , m_misses(misses) - , m_far_side(std::move(far_side)) - {} - - virtual RouteIO& route() override{ - return m_route; - } - virtual void fight(const std::string&) override{ - m_sim.end_dialogue(); - } - virtual void walk_until_stopped(Direction direction, size_t tiles, const std::string& label) override; - virtual bool run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&) override; - virtual bool run_into_door(Direction direction, bool run) override{ - const bool warped = m_sim.hold(direction, 12000ms, run ? SimPace::RUN : SimPace::WALK).stop == SimStop::WARP; - if (warped && fade_missed()){ - m_misses.fade_unseen = true; - return false; - } - return warped; - } - // A scene may take us to another map (Mom walks us out, 00.04): with a - // dialogue open the stub scene plays it. Otherwise only a door fades. - // A fade missed on the way in stays missed: it was over before settle() - // looked, which then fails as it does on the console after 120 s. - virtual void settle(Button, const std::string& label, bool fade) override{ - if (fade && ((!m_sim.player().warped && !m_sim.player().dialog) || m_misses.fade_unseen)){ - m_route.note(label + ": no fade, so we are not where the step goes (map mismatch).", RouteNote::FAILURE); - } - m_misses.fade_unseen = false; - m_sim.end_dialogue(); - } - virtual std::optional tap_to_banner(Direction direction, const std::string& map, size_t max_taps) override; - -private: - // Whether the run misses this fade or this banner (SimMisses). - bool fade_missed(){ - const bool miss = m_misses.fade == m_misses.fades++; - m_misses.missed |= miss; - return miss; - } - bool banner_missed(){ - const bool miss = m_misses.banner == m_misses.banners++; - m_misses.missed |= miss; - return miss; - } - bool on_map(const std::string& map) const{ - return story_to_local(m_sim.world(), map, nearest_tile(m_sim.player().pos)).has_value(); - } - - GridSim& m_sim; - GridSimRouteIO m_route; - SimMisses& m_misses; - std::function m_far_side; -}; - -// A hold until something stops us. A door stops it at the fade, which -// fails the step; a fade missed lets the hold go on past the mouth (r81's -// timed walk south out of the Ravaged Path, onto Route 204's cliff). -void SimStoryStepIO::walk_until_stopped(Direction direction, size_t tiles, const std::string& label){ - m_sim.tap(direction, ROUTE_TURN_TAP, false); - Milliseconds limit = ROUTE_START_DELAY + 800ms + route_ms_per_tile(direction) * (int64_t)(2 * tiles); - SimMove move = m_sim.hold(direction, limit, SimPace::WALK); - if (move.stop == SimStop::WARP && fade_missed() && m_far_side()){ - limit -= std::min(limit, move.time); - move = m_sim.hold(direction, limit, SimPace::WALK); - } - if (move.stop == SimStop::DIALOG || move.stop == SimStop::WARP){ - m_route.note(label + ": " + sim_stop_name(move.stop) + " instead of a stop.", RouteNote::FAILURE); - } -} - -// The D-pad, a tile at a time, until `map`'s banner reads; through a door -// the walk goes on beyond it (the fade eats no tiles of a hold). -bool SimStoryStepIO::run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&){ - const bool miss = banner_missed(); - size_t on = 0; - for (Milliseconds ran = 0ms; ran < limit; ran += ROUTE_RUN_MS_PER_TILE_ACROSS){ - SimMove move = m_sim.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); - const bool crossed = move.stop == SimStop::WARP && m_far_side(); - const bool stopped = !crossed && (move.distance == 0 || move.stop != SimStop::DONE); - on = on_map(map) ? on + 1 : 0; - if (on > 0 && (on > SIM_BANNER_TILES || stopped) && !miss){ - return true; - } - if (stopped){ - return false; - } - } - return false; -} - -// D-pad taps, each looked at, until the fade and then the banner (the -// console's ConsoleStoryStepIO::tap_to_banner()). Each look stands still, -// so a banner on the far side reads at once. A fade seen ends the taps, -// banner or not; with the fade missed the next look still sees the banner. -std::optional SimStoryStepIO::tap_to_banner(Direction direction, const std::string& map, size_t max_taps){ - bool crossed = false; - for (size_t tap = 1; tap <= max_taps; tap++){ - const SimMove move = m_sim.tap(direction, 160ms, false); - if (move.stop == SimStop::DIALOG){ - return std::nullopt; - } - bool faded = false; - if (move.stop == SimStop::WARP){ - faded = !fade_missed(); - crossed = m_far_side(); - } - if (crossed && on_map(map) && !banner_missed()){ - return tap; - } - if (faded){ - return std::nullopt; - } - } - return std::nullopt; -} - - -// A scene's trigger the simulation fires from row `row` on, once the step -// `after` has run ("" from the row's start), on `map`'s `tiles`. -struct RowTrigger{ - std::string row; - std::string after; - std::string map; - SimTrigger trigger; -}; - -// When a story variable holds the value its coord events wait for: from -// row `row` on, once the step `after` has run ("" from the row's start). -struct VarState{ - std::string var; - int value; - std::string row; - std::string after; -}; - -// The story variables whose coord events the table walks onto, from the -// row (and step) the table's reasons give, with the maps' coord events -// (Diamond's zone events; Platinum's for the houses): Barry's doorstep and -// Mom's warning in 00.01, Barry's Route 201 warning, then its pull-back -// row 19 once he has warned us (r14, r16); the lake's entrance in 00.02; -// Rowan's stop and Dawn at Sandgem's edge in 00.03; Dawn's catching -// tutorial on Route 202 from 00.04's start, Mom's call once she has healed -// us; the Poketch president on Jubilife's row 40 in 00.05b; Barry on Route -// 203 in 01.01; the Gate's Hiker and Oreburgh's welcome in 01.02; Barry's -// bump in Oreburgh after Roark in 01.05; and Rowan in Jubilife in 01.07. -// Left out: the Twinleaf guitarist (spent upstairs), Platinum's rival -// steps around our bedroom's start, Dawn at Jubilife's steps (BDSP starts -// her below them: an event), and Floaroma's (10..12, 29) (Diamond only). -std::vector story_vars(){ - return { - {.var = "VAR_TWINLEAF_TOWN_RIVAL_TRIGGER_STATE", .value = 0, .row = "00.01", .after = ""}, - {.var = "VAR_PLAYER_HOUSE_STATE", .value = 1, .row = "00.01", .after = ""}, - {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 0, .row = "00.01", .after = ""}, - {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 1, .row = "00.01", .after = "barry_warns_us"}, - {.var = "VAR_VERITY_LAKEFRONT_STATE", .value = 0, .row = "00.02", .after = ""}, - {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 2, .row = "00.03", .after = ""}, - {.var = "VAR_SANDGEM_TOWN_STATE", .value = 0, .row = "00.03", .after = ""}, - {.var = "VAR_ROUTE_202_STATE", .value = 0, .row = "00.04", .after = ""}, - {.var = "CONSOLE_MOMS_CALL", .value = 0, .row = "00.04", .after = "mom_heals_us"}, - {.var = "VAR_POKETCH_CAMPAIGN_STATE", .value = 1, .row = "00.05b", .after = ""}, - {.var = "VAR_ROUTE_203_RIVAL_STATE", .value = 0, .row = "01.01", .after = ""}, - {.var = "VAR_OREBURGH_GATE_1F_HIKER_STATE", .value = 1, .row = "01.02", .after = ""}, - {.var = "VAR_OREBURGH_CITY_STATE", .value = 0, .row = "01.02", .after = ""}, - {.var = "VAR_OREBURGH_CITY_STATE", .value = 2, .row = "01.05", .after = ""}, - {.var = "VAR_JUBILIFE_CITY_STATE", .value = 3, .row = "01.07", .after = ""}, - }; -} - -// The maps' coord events (story_coord_events()) whose variable holds. -std::optional var_state(const CoordEvent& event){ - for (const VarState& state : story_vars()){ - if (state.var == event.var && state.value == event.value){ - return state; - } - } - return std::nullopt; -} - -RowTrigger row_trigger(const std::string& map, const CoordEvent& event, const VarState& state){ - return {.row = state.row, .after = state.after, .map = map, .trigger = {.name = event.name, .tiles = event.tiles}}; -} - -std::vector story_triggers(){ - std::vector triggers; - for (const auto& [map, json] : bdsp_map_json()){ - for (const CoordEvent& event : story_coord_events(map)){ - const std::optional state = var_state(event); - if (state){ - triggers.push_back(row_trigger(map, event, *state)); - } - } - } - return triggers; -} - - -// Someone the maps do not have, who stands on `tile` of `map` and is -// talked to there or walked round: from row `row` on if `until` is "", -// else in `row` until its step `until` has run. -struct SimPerson{ - std::string name; - std::string map; - TilePos tile; - std::string row; - std::string until; -}; -std::vector sim_people(){ - return { - // Mom stands a row above the decomp's (7, 8) from 00.04 on (r43). - {.name = "mom", .map = "PlayerHouse1F", .tile = {7, 7}, .row = "00.04", .until = ""}, - // Barry stands in the Center's door until his lines (01.03; Roark - // ignored A until then, r101 and r104). - {.name = "barry", .map = "OreburghCity", .tile = {47, 20}, .row = "01.03", .until = "barrys_lines"}, - }; -} - -// A house's stairs, entered by a step `side` from the map's "stairs" -// point into what the map draws as a wall (the table's 00.01: r8 and r9 -// for ours, r11 for Barry's). -struct SimStairs{ - std::string map; - Direction side; -}; -std::vector sim_stairs(){ - return { - {.map = "PlayerHouse2F", .side = Direction::EAST}, - {.map = "RivalHouse1F", .side = Direction::EAST}, - {.map = "RivalHouse2F", .side = Direction::WEST}, - }; -} - - -// The program's walking steps on the simulation, the rest stubbed. -class SimExecutor : public Pokemon::StoryStepExecutor{ -public: - explicit SimExecutor(Logger& logger, SimMisses misses = {}) - : m_logger(logger) - , m_misses(misses) - , m_sim({}, {0, 0}, Direction::NORTH, false) - , m_io(m_sim, m_notes, m_misses, [this]{ return cross(); }) - , m_walks(m_io) - {} - - const SimMisses& misses() const{ - return m_misses; - } - const SimRun& result() const{ - return m_result; - } - virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ - m_row = data.id; - m_start_mismatch = data.start ? off_pose(*data.start) : ""; - m_ran.clear(); - m_result.rows.push_back({.row = data.id, .fades = m_misses.fades, .banners = m_misses.banners}); - m_walks.begin_attempt(data.id); - arm(""); - } - virtual Outcome run(RunState& state, const StoryStep& step) override; - -protected: - virtual Outcome route(RunState& state, const StoryStep& step) override{ return m_walks.route(state, step); } - virtual Outcome legs(RunState& state, const StoryStep& step) override{ return m_walks.legs(state, step); } - virtual Outcome step(RunState& state, const StoryStep& step) override{ return m_walks.step(state, step); } - virtual Outcome talk(RunState& state, const StoryStep& step) override{ return m_walks.talk(state, step); } - virtual Outcome mash(RunState& state, const StoryStep& step) override{ return m_walks.mash(state, step); } - virtual Outcome heal(RunState& state, const StoryStep& step) override; - virtual Outcome new_game(RunState&, const StoryStep&) override{ return stub(); } - virtual Outcome options(RunState&, const StoryStep&) override{ return stub(); } - virtual Outcome answer(RunState&, const StoryStep&) override{ return stub(); } - virtual Outcome battle(RunState&, const StoryStep&) override{ return stub(); } - virtual Outcome train(RunState&, const StoryStep&) override{ return stub(); } - virtual Outcome shop(RunState&, const StoryStep&) override{ return stub(); } - virtual Outcome save(RunState&, const StoryStep&) override{ return stub(); } - virtual Outcome game(RunState&, const StoryStep&) override{ return stub(); } - -private: - // A step the simulation does not play: its dialogue opened and closed. - Outcome stub(){ - m_sim.end_dialogue(); - return Pokemon::outcome_ok(); - } - const StoryWorld* sim_world(const std::string& map) const; - SimWorld setup(const StoryWorld* world) const; - void arm(const std::string& after); - bool sync(const Pose& pose, const StoryWorld* on = nullptr); - bool cross(); - std::string off_pose(const Pose& pose) const; - std::string check(const StoryStep& step, const Outcome& outcome); - Outcome run_step(RunState& state, const StoryStep& step); - - Logger& m_logger; - std::vector m_notes; - SimMisses m_misses; - GridSim m_sim; - SimStoryStepIO m_io; - StoryWalkSteps m_walks; - // The planner's worlds with sim_people() on them, by world and who. - mutable std::map, std::unique_ptr> m_peopled; - // The steps of this row that have run. - std::set m_ran; - std::string m_row; - std::vector m_armed; - // Where the step running now ends, if it says: a door's far side. - std::optional m_far; - // Why the player is not on this row's start, or "". - std::string m_start_mismatch; - // Whether the player has been put on a map yet. - bool m_placed = false; - // Whether the step running now is on a room with no map. - bool m_passed_over = false; - SimRun m_result; -}; - -// The world the player walks on for `map`: the planner's, with what the -// console showed that the maps lack. Nothing for a room with no map. -const StoryWorld* SimExecutor::sim_world(const std::string& map) const{ - const StoryWorld* world = story_plan_world(map, map); - std::vector here; - std::string who; - for (const SimPerson& person : sim_people()){ - const bool now = person.until.empty() ? m_row >= person.row : m_row == person.row && !m_ran.contains(person.until); - if (world != nullptr && now && story_plan_world(person.map, person.map) == world){ - here.push_back(person); - who += person.name + ";"; - } - } - if (here.empty()){ - return world; - } - std::unique_ptr& peopled = m_peopled[{world, who}]; - if (!peopled){ - peopled = std::make_unique(*world); - for (const SimPerson& person : here){ - peopled->block(story_to_world(*world, person.map, person.tile)); - } - } - return peopled.get(); -} - -SimWorld SimExecutor::setup(const StoryWorld* world) const{ - // B runs once the shoes' scene has given them: the scene ends at home. - SimWorld out{.world = world, .trainers = story_trainers(*world), .can_run = m_walks.can_run()}; - for (const SimStairs& stairs : sim_stairs()){ - if (sim_world(stairs.map) == world){ - out.doors.push_back(story_to_world(*world, stairs.map, map_point(stairs.map, "stairs").next(stairs.side))); - } - } - for (const RowTrigger& armed : m_armed){ - if (sim_world(armed.map) != world){ - continue; - } - SimTrigger trigger{.name = armed.trigger.name}; - for (const TilePos& pos : armed.trigger.tiles){ - trigger.tiles.push_back(story_to_world(*world, armed.map, pos)); - } - out.triggers.push_back(std::move(trigger)); - } - return out; -} - -// Arms the triggers of this row that start after `after`. -void SimExecutor::arm(const std::string& after){ - size_t armed = 0; - for (const RowTrigger& trigger : story_triggers()){ - if (trigger.row == m_row && trigger.after == after){ - m_armed.push_back(trigger); - armed++; - } - } - const SimPlayer& player = m_sim.player(); - if (armed > 0 && m_placed && on_grid(player.pos) && !player.dialog){ - m_sim.place(setup(&m_sim.world()), nearest_tile(player.pos), player.facing); - } -} - -// Puts the player on `pose`, on the world `on` if given, unless they stand -// on one of its tiles already. Returns whether the pose is on a map the -// simulation has. -bool SimExecutor::sync(const Pose& pose, const StoryWorld* on){ - const std::vector tiles = Pokemon::tiles_of(pose); - const StoryWorld* world = on != nullptr ? on : pose.map.empty() ? nullptr : sim_world(pose.map); - if (world == nullptr || tiles.empty()){ - return false; - } - const SimPlayer& player = m_sim.player(); - const std::optional local = !m_placed || &m_sim.world() != world - ? std::nullopt : story_to_local(*world, pose.map, nearest_tile(player.pos)); - if (local && on_grid(player.pos) && std::find(tiles.begin(), tiles.end(), *local) != tiles.end()){ - return true; - } - m_sim.place(setup(world), story_to_world(*world, pose.map, tiles.front()), Pokemon::facing_of(pose)); - m_placed = true; - return true; -} - -// Through a door or a cave mouth onto the step's far side, the player's -// facing kept: a hold goes on from there. -bool SimExecutor::cross(){ - const StoryWorld* world = m_far ? sim_world(m_far->map) : nullptr; - const std::vector tiles = m_far ? Pokemon::tiles_of(*m_far) : std::vector{}; - if (world == nullptr || tiles.empty()){ - return false; - } - m_sim.place(setup(world), story_to_world(*world, m_far->map, tiles.front()), std::nullopt); - return true; -} - -// Why the player does not stand on one of `pose`'s tiles, or "" if they -// do or the simulation cannot tell (no map, or not placed yet). -std::string SimExecutor::off_pose(const Pose& pose) const{ - const std::vector tiles = Pokemon::tiles_of(pose); - if (!m_placed || tiles.empty() || sim_world(pose.map) == nullptr){ - return ""; - } - const SimPlayer& player = m_sim.player(); - const TilePos at = nearest_tile(player.pos); - // In world tiles: a pose a leg worked out from the map it started on - // may lie past that map's edge, on the next map (Twinleaf's exit). - // story_to_world() throws for a map not on the player's world. - try{ - const auto there = [&](const TilePos& tile){ return story_to_world(m_sim.world(), pose.map, tile) == at; }; - if (on_grid(player.pos) && std::ranges::any_of(tiles, there)){ - return ""; - } - }catch (const InternalProgramError&){} - return std::format("ended at {} ({}), not on {}.", to_string(player.pos), describe(m_sim.world(), at), Pokemon::to_string(pose)); -} - -Outcome SimExecutor::heal(RunState& state, const StoryStep& step){ - if (sync(state.pose) && m_sim.hold(Direction::NORTH, 3000ms, SimPace::RUN).stop != SimStop::WARP){ - return Pokemon::outcome_failed("no_door", step.name + ": no door north of " + to_string(m_sim.player().pos)); - } - return Pokemon::outcome_ok(); -} - -// Why a walking step did not end where the table says, or "". A step -// through a door the simulation did not cross is not checked: the sim's -// maps have no warps' destinations. -std::string SimExecutor::check(const StoryStep& step, const Outcome& outcome){ - const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || step.kind == StoryStepKind::STEP; - const std::optional expected = step.pose_after ? step.pose_after : outcome.pose; - if (!walks || !m_placed){ - return ""; - } - if (!expected || Pokemon::tiles_of(*expected).empty()){ - m_result.unchecked.push_back(m_row + " " + step.name); - return ""; - } - const SimPlayer& player = m_sim.player(); - if (player.warped && !story_to_local(m_sim.world(), expected->map, nearest_tile(player.pos))){ - return ""; - } - return off_pose(*expected); -} - -Outcome SimExecutor::run_step(RunState& state, const StoryStep& step){ - if (!m_start_mismatch.empty()){ - return Pokemon::outcome_failed("sim_mismatch", "the row before " + std::exchange(m_start_mismatch, "")); - } - m_far = step.pose_after; - if (story_events().contains(step.variant)){ - return stub(); - } - // A scene goes on from where the step before left us. A route walks - // on the world it plans on (Jubilife alone has no Route 204 north of it). - const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || - step.kind == StoryStepKind::STEP || step.kind == StoryStepKind::TALK; - const StoryWorld* on = step.kind == StoryStepKind::ROUTE && step.target && !state.pose.map.empty() - ? story_plan_world(state.pose.map, step.target->map) : nullptr; - // A world of one map is sim_world()'s, with its people. - if (on != nullptr && on == story_plan_world(state.pose.map, state.pose.map)){ - on = nullptr; - } - if (walks && !sync(state.pose, on)){ - m_logger.log(std::format("[{}] {}: no map for {}; passed over.", m_row, step.name, Pokemon::to_string(state.pose))); - m_passed_over = true; - return Pokemon::outcome_ok(); - } - return Pokemon::StoryStepExecutor::run(state, step); -} - -Outcome SimExecutor::run(RunState& state, const StoryStep& step){ - Outcome outcome; - m_passed_over = false; - try{ - outcome = run_step(state, step); - }catch (const GridSimFailure& e){ - outcome = Pokemon::outcome_failed("sim_failure", e.message); - } - const std::string mismatch = outcome.succeeded() && !m_passed_over ? check(step, outcome) : ""; - if (!mismatch.empty()){ - outcome = Pokemon::outcome_failed("sim_mismatch", step.name + ": " + mismatch); - } - // What the simulation does not play (scenes, events, battles) ends - // where the table says it does. - if (outcome.succeeded() && step.pose_after){ - sync(*step.pose_after); - } - m_logger.log(std::format("[{}] {}: {}, the player at {}.", m_row, step.name, Pokemon::to_string(outcome), to_string(m_sim.player().pos))); - if (std::exchange(m_misses.missed, false) && m_result.missed_step.empty()){ - m_result.missed_step = m_row + " " + step.name; - } - if (!outcome.succeeded() && m_result.error.empty()){ - // The walker's last notes, to see why. - for (size_t i = m_notes.size() - std::min(m_notes.size(), 40); i < m_notes.size(); i++){ - m_logger.log(std::format("[{}] note: {}", m_row, m_notes[i])); - } - m_result.error = m_row + " " + Pokemon::to_string(outcome); - m_result.failed_step = m_row + " " + step.name; - } - m_result.world = m_placed ? &m_sim.world() : nullptr; - m_result.end = nearest_tile(m_sim.player().pos); - m_ran.insert(step.name); - arm(step.name); - return outcome; -} - - -// Runs `range` of `rows` (the table unless given) on `executor`, stopping -// at the first failure. -Pokemon::SegmentReport run_rows_on( - SimExecutor& executor, Logger& logger, const Pokemon::SegmentRange& range, - const std::vector& rows = story_checkpoints() -){ - std::set saved; - Pokemon::Segment segment = make_story_segment(rows, executor, saved); - Pokemon::SegmentPolicy policy; - policy.stop_at_first_failure = true; - Pokemon::SegmentHooks hooks; - hooks.reload = [](RunState&){ return Pokemon::outcome_ok(); }; - hooks.log = [&logger](const std::string& line){ logger.log(line); }; - RunState state; - return Pokemon::run_segment(state, segment, range, policy, hooks); -} - -// Runs `range` of the table on the simulation, stopping at the first failure. -UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ - SimExecutor executor(logger); - const Pokemon::SegmentReport report = run_rows_on(executor, logger, range); - for (const std::string& step : executor.result().unchecked){ - logger.log("No end to check after " + step + "."); - } - if (!report.completed){ - return std::format("stopped at {} {}: {}", report.failed_checkpoint, report.failed_event, executor.result().error); - } - return true; -} - -// The table with row `row`'s step of `step`'s name replaced by `step`. -std::vector table_with(const std::string& row, const StoryStep& step){ - std::vector rows = story_checkpoints(); - for (StoryCheckpoint& checkpoint : rows){ - for (StoryStep& old : checkpoint.steps){ - if (checkpoint.id == row && old.name == step.name){ - old = step; - return rows; - } - } - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No step " + step.name + " in " + row); -} - -bool ends_on(const SimRun& run, const Pose& pose){ - const std::vector tiles = Pokemon::tiles_of(pose); - const std::optional local = run.world ? story_to_local(*run.world, pose.map, run.end) : std::nullopt; - return local && std::find(tiles.begin(), tiles.end(), *local) != tiles.end(); -} -std::string end_of(const SimRun& run){ - return run.world ? describe(*run.world, run.end) : "nowhere"; -} - - -// A way out of a map that a hold walked on past (r75, r81), against the -// table's tap to the banner. The old step, with `misses`, must end on -// `overshoot`, caught there by the end-tile check. The table's step must -// never end there, whichever one fade or banner of the row is missed: it -// ends on its tile or stops in that step. -class Test_StorySimExit : public UnitTest{ -public: - Test_StorySimExit(std::string name, std::string row, StoryStep old_step, SimMisses misses, Pose overshoot) - : UnitTest("PokemonBDSP::AutoStory - Story sim, exits: " + std::move(name)) - , m_row(std::move(row)) - , m_old(std::move(old_step)) - , m_misses(std::move(misses)) - , m_overshoot(std::move(overshoot)) - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - try{ - const std::string old = old_form(logger); - return old.empty() ? table_form(logger) : UnitTestResult(old); - }catch (Exception& e){ - return std::string(e.name()) + ": " + e.message(); - } - } - -private: - // Why the old step did not go wrong as on the console, or "". - std::string old_form(Logger& logger) const{ - SimExecutor executor(logger, m_misses); - const std::vector rows = table_with(m_row, m_old); - run_rows_on(executor, logger, {.from = m_row, .to = m_row}, rows); - const SimRun& run = executor.result(); - logger.log(std::format("The old {}: {}, ended on {}.", m_old.name, run.error, end_of(run))); - if (run.failed_step != m_row + " " + m_old.name || !ends_on(run, m_overshoot)){ - return std::format("the old {} should end on {} and fail there; it ended on {}: {}", - m_old.name, Pokemon::to_string(m_overshoot), end_of(run), run.error.empty() ? "no failure" : run.error); - } - return ""; - } - UnitTestResult table_form(Logger& logger) const{ - SimExecutor clean(logger); - if (!run_rows_on(clean, logger, {.from = m_row, .to = m_row}).completed){ - return "the table's row fails without misses: " + clean.result().error; - } - std::vector runs; - for (size_t i = 0; i < clean.misses().fades; i++){ - runs.push_back({.fade = i}); - } - for (size_t i = 0; i < clean.misses().banners; i++){ - runs.push_back({.banner = i}); - } - for (const SimMisses& misses : runs){ - SimExecutor executor(logger, misses); - run_rows_on(executor, logger, {.from = m_row, .to = m_row}); - if (ends_on(executor.result(), m_overshoot)){ - return std::format("the table's {} ended on {} with a miss in {}.", m_old.name, end_of(executor.result()), - executor.result().missed_step); - } - } - return true; - } - - std::string m_row; - StoryStep m_old; - SimMisses m_misses; - Pose m_overshoot; -}; - -// r75: out of the Oreburgh Gate as a run west until Route 203's banner -// (the old event west_until_transition()). The banner reads a few tiles -// in: the run ends west of the Gate's warp tile (55, 9), in the pocket by -// the pond (Route 203's x 49 to 54, rows 6 to 13). -StoryStep r75_old_gate_exit(){ - StoryStep step; - step.kind = StoryStepKind::LEGS; - step.name = "out_of_oreburgh_gate"; - step.legs = {{.direction = Direction::WEST, .tiles = 8}}; - step.variant = "to_map"; - step.pose_after = Pokemon::tile_pose("Route203", {TilePos{55, 9}}, Direction::WEST); - return step; -} -Pose r75_pond_pocket(){ - std::vector tiles; - for (int y = 6; y <= 13; y++){ - for (int x = 49; x <= 54; x++){ - tiles.push_back({x, y}); - } - } - return Pokemon::tile_pose("Route203", tiles); -} -// r81: out of the Ravaged Path as a timed walk south (the old event's -// 4 s walk_dir()), its fade missed: on past cave_exit (20, 26) to the -// cliff, column 20 down to row 30. -StoryStep r81_old_cave_exit(){ - StoryStep step; - step.kind = StoryStepKind::LEGS; - step.name = "out_of_the_ravaged_path"; - step.legs = {{.direction = Direction::SOUTH, .tiles = 4}}; - step.variant = "to_wall"; - step.pose_after = Pokemon::tile_pose("Route204North", {TilePos{20, 26}}, Direction::SOUTH); - return step; -} -Pose r81_below_the_mouth(){ - return Pokemon::tile_pose("Route204North", {TilePos{20, 27}, TilePos{20, 28}, TilePos{20, 29}, TilePos{20, 30}}); -} - - -// Every coord event has ground the player can step onto, and the ones -// the story's variables arm are there. -class Test_CoordEvents : public UnitTest{ -public: - Test_CoordEvents() - : UnitTest("PokemonBDSP::AutoStory - Story sim: the maps' coord events are on open ground or doors") - {} - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - for (const auto& [map, json] : bdsp_map_json()){ - const StoryWorld* world = story_plan_world(map, map); - for (const CoordEvent& event : world == nullptr ? std::vector{} : story_coord_events(map)){ - const auto open = [&](const TilePos& pos){ - const TilePos tile = story_to_world(*world, map, pos); - return standable(*world, tile) || world->tile(tile) == Pokemon::TileKind::DOOR; - }; - // The decomp's boxes may run into walls (Sandgem's (4, 10..15)). - if (!std::ranges::any_of(event.tiles, open)){ - return std::format("{} {}: no tile is open ground or a door.", map, event.name); - } - } - } - const size_t armed = story_triggers().size(); - return armed == story_vars().size() ? UnitTestResult(true) - : UnitTestResult(std::format("Expected {} coord events armed by the story's variables, found {}.", story_vars().size(), armed)); - } -}; - - -// A detection the console can miss, missed in turn at each place the run -// makes it (#162). Each miss must either be got over (the run completes, -// every walk ending on its tiles) or stop the run in the step that missed: -// a miss that lets the run go on and fail later went unseen, and a later -// step started from somewhere it did not expect. Each miss runs its own -// row and the next, which checks where the row left us. -class Test_StorySimMisses : public UnitTest{ -public: - Test_StorySimMisses(std::string name, std::string from, std::string to) - : UnitTest("PokemonBDSP::AutoStory - Story sim, detector misses: " + std::move(name)) - , m_range{.from = std::move(from), .to = std::move(to)} - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - try{ - SimExecutor clean(logger); - if (!run_rows_on(clean, logger, m_range).completed){ - return "the run fails without misses: " + clean.result().error; - } - logger.log(std::format("{} fades and {} banners to miss.", clean.misses().fades, clean.misses().banners)); - std::vector unseen; - for (size_t i = 0; i < clean.misses().fades + clean.misses().banners; i++){ - const std::string line = miss(logger, clean.result(), i < clean.misses().fades, i < clean.misses().fades ? i : i - clean.misses().fades); - if (!line.empty()){ - unseen.push_back(line); - } - } - std::string out = "Misses that went unseen:"; - for (const std::string& line : unseen){ - out += "\n " + line; - } - return unseen.empty() ? UnitTestResult(true) : UnitTestResult(out); - }catch (Exception& e){ - return std::string(e.name()) + ": " + e.message(); - } - } - -private: - // Misses the `index`th fade (or banner) of the clean run `clean` in its - // row. Returns how it went unseen, or "". - static std::string miss(Logger& logger, const SimRun& clean, bool fade, size_t index){ - size_t k = 0; - while (k + 1 < clean.rows.size() && (fade ? clean.rows[k + 1].fades : clean.rows[k + 1].banners) <= index){ - k++; - } - const SimRun::RowStart& start = clean.rows[k]; - SimMisses misses; - (fade ? misses.fade : misses.banner) = index - (fade ? start.fades : start.banners); - SimExecutor executor(logger, misses); - const Pokemon::SegmentReport report = run_rows_on(executor, logger, - {.from = start.row, .to = clean.rows[std::min(k + 1, clean.rows.size() - 1)].row}); - const SimRun& run = executor.result(); - const std::string what = std::format("{} {}", fade ? "fade" : "banner", index); - logger.log(std::format("Missed {} in {}: {}.", what, run.missed_step, - report.completed ? "got over it" : "stopped in " + run.failed_step + ": " + run.error)); - if (report.completed || run.failed_step == run.missed_step){ - return ""; - } - return std::format("{} missed in {}, failed later in {}: {}", what, run.missed_step, run.failed_step, run.error); - } - - Pokemon::SegmentRange m_range; -}; - - -class Test_StorySim : public UnitTest{ -public: - Test_StorySim(std::string name, std::string from, std::string to) - : UnitTest("PokemonBDSP::AutoStory - Story sim: " + std::move(name)) - , m_from(std::move(from)) - , m_to(std::move(to)) - {} - virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ - try{ - return run_rows(logger, {.from = m_from, .to = m_to}); - }catch (Exception& e){ - return std::string(e.name()) + ": " + e.message(); - } - } - -private: - std::string m_from; - std::string m_to; -}; - -} - - -void add_tests_AutoStory_StorySim(UnitTestDatabase& database){ - database.add(); - database.add("00.00 to 01.11", "00.00", "01.11"); - database.add("00.00 to 01.11: the whole table, from the new game to Floaroma", "00.00", "01.11"); - for (const StoryCheckpoint& row : story_checkpoints()){ - database.add(std::format("row {}", row.id), row.id, row.id); - } - database.add("r75, out of the Oreburgh Gate", "01.05", r75_old_gate_exit(), SimMisses{}, r75_pond_pocket()); - database.add("r81, out of the Ravaged Path", "01.09", r81_old_cave_exit(), SimMisses{.fade = 0}, r81_below_the_mouth()); -} - - - -} -} -} +/* BDSP AutoStory Story Sim Tests + * + * From: https://github.com/PokemonAutomation/ + * + * Rows of the story table run as the program runs them (the segment + * runner, make_story_segment()), with the program's own walking steps + * (StoryWalkSteps: ROUTE, LEGS, STEP, TALK, MASH) on the grid simulation. + * After each walking step the simulated player must stand, on the grid, + * on a tile the table gives; a step through a door must have warped. + * + * Events, battles, shops and saves are stubs: a dialogue they open is + * closed, and the table's pose_after puts the player where they end. A + * heal checks that a walk north from where we stand enters a door. A scene + * starts when the player steps onto one of the maps' coord events while + * its story variable holds (story_vars()). Rooms with no + * map (the Trainers' School) are passed over. Each row must start where + * the row before left the player. + * + * A tap or hold through a door or cave mouth stops on its tile, as the + * console stops at the fade; a hold that does not see the fade goes on + * onto the step's pose_after and on along it, and a location banner reads + * only a few tiles into its map (SIM_BANNER_TILES). The exit tests run + * r75's and r81's old open-ended walks that way. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "Pokemon/AutoStory/Pokemon_StorySegments.h" +#include "Pokemon/AutoStory/Pokemon_TilePose.h" +#include "Data/PokemonBDSP_AutoStory_CoordEvents.h" +#include "Data/PokemonBDSP_AutoStory_Maps.h" +#include "Utils/PokemonBDSP_AutoStory_GridSim.h" +#include "Utils/PokemonBDSP_AutoStory_RouteWalk.h" +#include "PokemonBDSP_AutoStory_Events.h" +#include "PokemonBDSP_AutoStory_StoryExecutor.h" +#include "PokemonBDSP_AutoStory_StoryPlaces.h" +#include "PokemonBDSP_AutoStory_StoryStepIO.h" +#include "PokemonBDSP_AutoStory_StoryTable.h" +#include "PokemonBDSP_AutoStory_StoryWalkSteps.h" +#include "PokemonBDSP_AutoStory_StorySim_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonBDSP{ + + +namespace{ + +using namespace std::chrono_literals; +using Pokemon::Direction; +using Pokemon::Outcome; +using Pokemon::Pose; +using Pokemon::RunState; +using Pokemon::StoryStep; +using Pokemon::StoryStepKind; +using Pokemon::TilePos; + + +// Which detections the simulation misses on purpose (#162): the `fade`th +// door fade (run_into_door(), from 0) or the `banner`th map banner +// (run_until_map()) of the run goes unseen, though the player did go +// through. The console missed both (r75, r81). +// The counts and flags below are the run's: how many fades and banners it +// looked for, whether a miss happened since the executor last looked, and +// whether a missed fade is still unseen. +struct SimMisses{ + std::optional fade; + std::optional banner; + size_t fades = 0; + size_t banners = 0; + bool missed = false; + bool fade_unseen = false; +}; + +// How many tiles a held walk goes on into a map before its location +// banner reads: "it shows a tile or a few past the border" (the table's +// route_201_by_the_path(), measured on the console). Standing still, it +// reads at once. Through a cave mouth the fade comes first, and a hold +// whose fade went unseen goes on the same way (r75, r81). +constexpr size_t SIM_BANNER_TILES = 3; + +// What a run on the simulation did. +struct SimRun{ + // The first failure, as "row outcome", or "". + std::string error; + // The rows and steps where an injected miss happened and where the run + // first failed, as "row step": "" if none. + std::string missed_step; + std::string failed_step; + // Walking steps that end where nothing says, as "row step". + std::vector unchecked; + // Each row run, with the fades and banners looked for before it. + struct RowStart{ + std::string row; + size_t fades; + size_t banners; + }; + std::vector rows; + // Where the player stood after the last step, if placed. + const StoryWorld* world = nullptr; + TilePos end; +}; + +// StoryStepIO on the simulation. A walk through a door or a cave mouth +// stops on its tile, as the console stops at the fade; a hold that misses +// the fade carries on, onto `far_side()` (the step's pose_after: the +// sim's maps have no warps' destinations) and on along it. +class SimStoryStepIO : public StoryStepIO{ +public: + SimStoryStepIO(GridSim& sim, std::vector& notes, SimMisses& misses, std::function far_side) + : m_sim(sim) + , m_route(sim, notes) + , m_misses(misses) + , m_far_side(std::move(far_side)) + {} + + virtual RouteIO& route() override{ + return m_route; + } + virtual void fight(const std::string&) override{ + m_sim.end_dialogue(); + } + virtual void walk_until_stopped(Direction direction, size_t tiles, const std::string& label) override; + virtual bool run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&) override; + virtual bool run_into_door(Direction direction, bool run) override{ + const bool warped = m_sim.hold(direction, 12000ms, run ? SimPace::RUN : SimPace::WALK).stop == SimStop::WARP; + if (warped && fade_missed()){ + m_misses.fade_unseen = true; + return false; + } + return warped; + } + // A scene may take us to another map (Mom walks us out, 00.04): with a + // dialogue open the stub scene plays it. Otherwise only a door fades. + // A fade missed on the way in stays missed: it was over before settle() + // looked, which then fails as it does on the console after 120 s. + virtual void settle(Button, const std::string& label, bool fade) override{ + if (fade && ((!m_sim.player().warped && !m_sim.player().dialog) || m_misses.fade_unseen)){ + m_route.note(label + ": no fade, so we are not where the step goes (map mismatch).", RouteNote::FAILURE); + } + m_misses.fade_unseen = false; + m_sim.end_dialogue(); + } + virtual std::optional tap_to_banner(Direction direction, const std::string& map, size_t max_taps) override; + +private: + // Whether the run misses this fade or this banner (SimMisses). + bool fade_missed(){ + const bool miss = m_misses.fade == m_misses.fades++; + m_misses.missed |= miss; + return miss; + } + bool banner_missed(){ + const bool miss = m_misses.banner == m_misses.banners++; + m_misses.missed |= miss; + return miss; + } + bool on_map(const std::string& map) const{ + return story_to_local(m_sim.world(), map, nearest_tile(m_sim.player().pos)).has_value(); + } + + GridSim& m_sim; + GridSimRouteIO m_route; + SimMisses& m_misses; + std::function m_far_side; +}; + +// A hold until something stops us. A door stops it at the fade, which +// fails the step; a fade missed lets the hold go on past the mouth (r81's +// timed walk south out of the Ravaged Path, onto Route 204's cliff). +void SimStoryStepIO::walk_until_stopped(Direction direction, size_t tiles, const std::string& label){ + m_sim.tap(direction, ROUTE_TURN_TAP, false); + Milliseconds limit = ROUTE_START_DELAY + 800ms + route_ms_per_tile(direction) * (int64_t)(2 * tiles); + SimMove move = m_sim.hold(direction, limit, SimPace::WALK); + if (move.stop == SimStop::WARP && fade_missed() && m_far_side()){ + limit -= std::min(limit, move.time); + move = m_sim.hold(direction, limit, SimPace::WALK); + } + if (move.stop == SimStop::DIALOG || move.stop == SimStop::WARP){ + m_route.note(label + ": " + sim_stop_name(move.stop) + " instead of a stop.", RouteNote::FAILURE); + } +} + +// The D-pad, a tile at a time, until `map`'s banner reads; through a door +// the walk goes on beyond it (the fade eats no tiles of a hold). +bool SimStoryStepIO::run_until_map(Direction direction, Milliseconds limit, const std::string& map, const std::string&){ + const bool miss = banner_missed(); + size_t on = 0; + for (Milliseconds ran = 0ms; ran < limit; ran += ROUTE_RUN_MS_PER_TILE_ACROSS){ + SimMove move = m_sim.tap(direction, ROUTE_MS_PER_TILE_ACROSS, false); + const bool crossed = move.stop == SimStop::WARP && m_far_side(); + const bool stopped = !crossed && (move.distance == 0 || move.stop != SimStop::DONE); + on = on_map(map) ? on + 1 : 0; + if (on > 0 && (on > SIM_BANNER_TILES || stopped) && !miss){ + return true; + } + if (stopped){ + return false; + } + } + return false; +} + +// D-pad taps, each looked at, until the fade and then the banner (the +// console's ConsoleStoryStepIO::tap_to_banner()). Each look stands still, +// so a banner on the far side reads at once. A fade seen ends the taps, +// banner or not; with the fade missed the next look still sees the banner. +std::optional SimStoryStepIO::tap_to_banner(Direction direction, const std::string& map, size_t max_taps){ + bool crossed = false; + for (size_t tap = 1; tap <= max_taps; tap++){ + const SimMove move = m_sim.tap(direction, 160ms, false); + if (move.stop == SimStop::DIALOG){ + return std::nullopt; + } + bool faded = false; + if (move.stop == SimStop::WARP){ + faded = !fade_missed(); + crossed = m_far_side(); + } + if (crossed && on_map(map) && !banner_missed()){ + return tap; + } + if (faded){ + return std::nullopt; + } + } + return std::nullopt; +} + + +// A scene's trigger the simulation fires from row `row` on, once the step +// `after` has run ("" from the row's start), on `map`'s `tiles`. +struct RowTrigger{ + std::string row; + std::string after; + std::string map; + SimTrigger trigger; +}; + +// When a story variable holds the value its coord events wait for: from +// row `row` on, once the step `after` has run ("" from the row's start). +struct VarState{ + std::string var; + int value; + std::string row; + std::string after; +}; + +// The story variables whose coord events the table walks onto, from the +// row (and step) the table's reasons give, with the maps' coord events +// (Diamond's zone events; Platinum's for the houses): Barry's doorstep and +// Mom's warning in 00.01, Barry's Route 201 warning, then its pull-back +// row 19 once he has warned us (r14, r16); the lake's entrance in 00.02; +// Rowan's stop and Dawn at Sandgem's edge in 00.03; Dawn's catching +// tutorial on Route 202 from 00.04's start, Mom's call once she has healed +// us; the Poketch president on Jubilife's row 40 in 00.05b; Barry on Route +// 203 in 01.01; the Gate's Hiker and Oreburgh's welcome in 01.02; Barry's +// bump in Oreburgh after Roark in 01.05; and Rowan in Jubilife in 01.07. +// Left out: the Twinleaf guitarist (spent upstairs), Platinum's rival +// steps around our bedroom's start, Dawn at Jubilife's steps (BDSP starts +// her below them: an event), and Floaroma's (10..12, 29) (Diamond only). +std::vector story_vars(){ + return { + {.var = "VAR_TWINLEAF_TOWN_RIVAL_TRIGGER_STATE", .value = 0, .row = "00.01", .after = ""}, + {.var = "VAR_PLAYER_HOUSE_STATE", .value = 1, .row = "00.01", .after = ""}, + {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 0, .row = "00.01", .after = ""}, + {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 1, .row = "00.01", .after = "barry_warns_us"}, + {.var = "VAR_VERITY_LAKEFRONT_STATE", .value = 0, .row = "00.02", .after = ""}, + {.var = "VAR_FOLLOWER_RIVAL_STATE", .value = 2, .row = "00.03", .after = ""}, + {.var = "VAR_SANDGEM_TOWN_STATE", .value = 0, .row = "00.03", .after = ""}, + {.var = "VAR_ROUTE_202_STATE", .value = 0, .row = "00.04", .after = ""}, + {.var = "CONSOLE_MOMS_CALL", .value = 0, .row = "00.04", .after = "mom_heals_us"}, + {.var = "VAR_POKETCH_CAMPAIGN_STATE", .value = 1, .row = "00.05b", .after = ""}, + {.var = "VAR_ROUTE_203_RIVAL_STATE", .value = 0, .row = "01.01", .after = ""}, + {.var = "VAR_OREBURGH_GATE_1F_HIKER_STATE", .value = 1, .row = "01.02", .after = ""}, + {.var = "VAR_OREBURGH_CITY_STATE", .value = 0, .row = "01.02", .after = ""}, + {.var = "VAR_OREBURGH_CITY_STATE", .value = 2, .row = "01.05", .after = ""}, + {.var = "VAR_JUBILIFE_CITY_STATE", .value = 3, .row = "01.07", .after = ""}, + }; +} + +// The maps' coord events (story_coord_events()) whose variable holds. +std::optional var_state(const CoordEvent& event){ + for (const VarState& state : story_vars()){ + if (state.var == event.var && state.value == event.value){ + return state; + } + } + return std::nullopt; +} + +RowTrigger row_trigger(const std::string& map, const CoordEvent& event, const VarState& state){ + return {.row = state.row, .after = state.after, .map = map, .trigger = {.name = event.name, .tiles = event.tiles}}; +} + +std::vector story_triggers(){ + std::vector triggers; + for (const auto& [map, json] : bdsp_map_json()){ + for (const CoordEvent& event : story_coord_events(map)){ + const std::optional state = var_state(event); + if (state){ + triggers.push_back(row_trigger(map, event, *state)); + } + } + } + return triggers; +} + + +// Someone the maps do not have, who stands on `tile` of `map` and is +// talked to there or walked round: from row `row` on if `until` is "", +// else in `row` until its step `until` has run. +struct SimPerson{ + std::string name; + std::string map; + TilePos tile; + std::string row; + std::string until; +}; +std::vector sim_people(){ + return { + // Mom stands a row above the decomp's (7, 8) from 00.04 on (r43). + {.name = "mom", .map = "PlayerHouse1F", .tile = {7, 7}, .row = "00.04", .until = ""}, + // Barry stands in the Center's door until his lines (01.03; Roark + // ignored A until then, r101 and r104). + {.name = "barry", .map = "OreburghCity", .tile = {47, 20}, .row = "01.03", .until = "barrys_lines"}, + }; +} + +// A house's stairs, entered by a step `side` from the map's "stairs" +// point into what the map draws as a wall (the table's 00.01: r8 and r9 +// for ours, r11 for Barry's). +struct SimStairs{ + std::string map; + Direction side; +}; +std::vector sim_stairs(){ + return { + {.map = "PlayerHouse2F", .side = Direction::EAST}, + {.map = "RivalHouse1F", .side = Direction::EAST}, + {.map = "RivalHouse2F", .side = Direction::WEST}, + }; +} + + +// The program's walking steps on the simulation, the rest stubbed. +class SimExecutor : public Pokemon::StoryStepExecutor{ +public: + explicit SimExecutor(Logger& logger, SimMisses misses = {}) + : m_logger(logger) + , m_misses(misses) + , m_sim({}, {0, 0}, Direction::NORTH, false) + , m_io(m_sim, m_notes, m_misses, [this]{ return cross(); }) + , m_walks(m_io) + {} + + const SimMisses& misses() const{ + return m_misses; + } + const SimRun& result() const{ + return m_result; + } + virtual void begin_attempt(const Pokemon::StoryCheckpoint& data) override{ + m_row = data.id; + m_start_mismatch = data.start ? off_pose(*data.start) : ""; + m_ran.clear(); + m_result.rows.push_back({.row = data.id, .fades = m_misses.fades, .banners = m_misses.banners}); + m_walks.begin_attempt(data.id); + arm(""); + } + virtual Outcome run(RunState& state, const StoryStep& step) override; + +protected: + virtual Outcome route(RunState& state, const StoryStep& step) override{ return m_walks.route(state, step); } + virtual Outcome legs(RunState& state, const StoryStep& step) override{ return m_walks.legs(state, step); } + virtual Outcome step(RunState& state, const StoryStep& step) override{ return m_walks.step(state, step); } + virtual Outcome talk(RunState& state, const StoryStep& step) override{ return m_walks.talk(state, step); } + virtual Outcome mash(RunState& state, const StoryStep& step) override{ return m_walks.mash(state, step); } + virtual Outcome heal(RunState& state, const StoryStep& step) override; + virtual Outcome new_game(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome options(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome answer(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome battle(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome train(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome shop(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome save(RunState&, const StoryStep&) override{ return stub(); } + virtual Outcome game(RunState&, const StoryStep&) override{ return stub(); } + +private: + // A step the simulation does not play: its dialogue opened and closed. + Outcome stub(){ + m_sim.end_dialogue(); + return Pokemon::outcome_ok(); + } + const StoryWorld* sim_world(const std::string& map) const; + SimWorld setup(const StoryWorld* world) const; + void arm(const std::string& after); + bool sync(const Pose& pose, const StoryWorld* on = nullptr); + bool cross(); + std::string off_pose(const Pose& pose) const; + std::string check(const StoryStep& step, const Outcome& outcome); + Outcome run_step(RunState& state, const StoryStep& step); + + Logger& m_logger; + std::vector m_notes; + SimMisses m_misses; + GridSim m_sim; + SimStoryStepIO m_io; + StoryWalkSteps m_walks; + // The planner's worlds with sim_people() on them, by world and who. + mutable std::map, std::unique_ptr> m_peopled; + // The steps of this row that have run. + std::set m_ran; + std::string m_row; + std::vector m_armed; + // Where the step running now ends, if it says: a door's far side. + std::optional m_far; + // Why the player is not on this row's start, or "". + std::string m_start_mismatch; + // Whether the player has been put on a map yet. + bool m_placed = false; + // Whether the step running now is on a room with no map. + bool m_passed_over = false; + SimRun m_result; +}; + +// The world the player walks on for `map`: the planner's, with what the +// console showed that the maps lack. Nothing for a room with no map. +const StoryWorld* SimExecutor::sim_world(const std::string& map) const{ + const StoryWorld* world = story_plan_world(map, map); + std::vector here; + std::string who; + for (const SimPerson& person : sim_people()){ + const bool now = person.until.empty() ? m_row >= person.row : m_row == person.row && !m_ran.contains(person.until); + if (world != nullptr && now && story_plan_world(person.map, person.map) == world){ + here.push_back(person); + who += person.name + ";"; + } + } + if (here.empty()){ + return world; + } + std::unique_ptr& peopled = m_peopled[{world, who}]; + if (!peopled){ + peopled = std::make_unique(*world); + for (const SimPerson& person : here){ + peopled->block(story_to_world(*world, person.map, person.tile)); + } + } + return peopled.get(); +} + +SimWorld SimExecutor::setup(const StoryWorld* world) const{ + // B runs once the shoes' scene has given them: the scene ends at home. + SimWorld out{.world = world, .trainers = story_trainers(*world), .can_run = m_walks.can_run()}; + for (const SimStairs& stairs : sim_stairs()){ + if (sim_world(stairs.map) == world){ + out.doors.push_back(story_to_world(*world, stairs.map, map_point(stairs.map, "stairs").next(stairs.side))); + } + } + for (const RowTrigger& armed : m_armed){ + if (sim_world(armed.map) != world){ + continue; + } + SimTrigger trigger{.name = armed.trigger.name}; + for (const TilePos& pos : armed.trigger.tiles){ + trigger.tiles.push_back(story_to_world(*world, armed.map, pos)); + } + out.triggers.push_back(std::move(trigger)); + } + return out; +} + +// Arms the triggers of this row that start after `after`. +void SimExecutor::arm(const std::string& after){ + size_t armed = 0; + for (const RowTrigger& trigger : story_triggers()){ + if (trigger.row == m_row && trigger.after == after){ + m_armed.push_back(trigger); + armed++; + } + } + const SimPlayer& player = m_sim.player(); + if (armed > 0 && m_placed && on_grid(player.pos) && !player.dialog){ + m_sim.place(setup(&m_sim.world()), nearest_tile(player.pos), player.facing); + } +} + +// Puts the player on `pose`, on the world `on` if given, unless they stand +// on one of its tiles already. Returns whether the pose is on a map the +// simulation has. +bool SimExecutor::sync(const Pose& pose, const StoryWorld* on){ + const std::vector tiles = Pokemon::tiles_of(pose); + const StoryWorld* world = on != nullptr ? on : pose.map.empty() ? nullptr : sim_world(pose.map); + if (world == nullptr || tiles.empty()){ + return false; + } + const SimPlayer& player = m_sim.player(); + const std::optional local = !m_placed || &m_sim.world() != world + ? std::nullopt : story_to_local(*world, pose.map, nearest_tile(player.pos)); + if (local && on_grid(player.pos) && std::find(tiles.begin(), tiles.end(), *local) != tiles.end()){ + return true; + } + m_sim.place(setup(world), story_to_world(*world, pose.map, tiles.front()), Pokemon::facing_of(pose)); + m_placed = true; + return true; +} + +// Through a door or a cave mouth onto the step's far side, the player's +// facing kept: a hold goes on from there. +bool SimExecutor::cross(){ + const StoryWorld* world = m_far ? sim_world(m_far->map) : nullptr; + const std::vector tiles = m_far ? Pokemon::tiles_of(*m_far) : std::vector{}; + if (world == nullptr || tiles.empty()){ + return false; + } + m_sim.place(setup(world), story_to_world(*world, m_far->map, tiles.front()), std::nullopt); + return true; +} + +// Why the player does not stand on one of `pose`'s tiles, or "" if they +// do or the simulation cannot tell (no map, or not placed yet). +std::string SimExecutor::off_pose(const Pose& pose) const{ + const std::vector tiles = Pokemon::tiles_of(pose); + if (!m_placed || tiles.empty() || sim_world(pose.map) == nullptr){ + return ""; + } + const SimPlayer& player = m_sim.player(); + const TilePos at = nearest_tile(player.pos); + // In world tiles: a pose a leg worked out from the map it started on + // may lie past that map's edge, on the next map (Twinleaf's exit). + // story_to_world() throws for a map not on the player's world. + try{ + const auto there = [&](const TilePos& tile){ return story_to_world(m_sim.world(), pose.map, tile) == at; }; + if (on_grid(player.pos) && std::ranges::any_of(tiles, there)){ + return ""; + } + }catch (const InternalProgramError&){} + return std::format("ended at {} ({}), not on {}.", to_string(player.pos), describe(m_sim.world(), at), Pokemon::to_string(pose)); +} + +Outcome SimExecutor::heal(RunState& state, const StoryStep& step){ + if (sync(state.pose) && m_sim.hold(Direction::NORTH, 3000ms, SimPace::RUN).stop != SimStop::WARP){ + return Pokemon::outcome_failed("no_door", step.name + ": no door north of " + to_string(m_sim.player().pos)); + } + return Pokemon::outcome_ok(); +} + +// Why a walking step did not end where the table says, or "". A step +// through a door the simulation did not cross is not checked: the sim's +// maps have no warps' destinations. +std::string SimExecutor::check(const StoryStep& step, const Outcome& outcome){ + const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || step.kind == StoryStepKind::STEP; + const std::optional expected = step.pose_after ? step.pose_after : outcome.pose; + if (!walks || !m_placed){ + return ""; + } + if (!expected || Pokemon::tiles_of(*expected).empty()){ + m_result.unchecked.push_back(m_row + " " + step.name); + return ""; + } + const SimPlayer& player = m_sim.player(); + if (player.warped && !story_to_local(m_sim.world(), expected->map, nearest_tile(player.pos))){ + return ""; + } + return off_pose(*expected); +} + +Outcome SimExecutor::run_step(RunState& state, const StoryStep& step){ + if (!m_start_mismatch.empty()){ + return Pokemon::outcome_failed("sim_mismatch", "the row before " + std::exchange(m_start_mismatch, "")); + } + m_far = step.pose_after; + if (story_events().contains(step.variant)){ + return stub(); + } + // A scene goes on from where the step before left us. A route walks + // on the world it plans on (Jubilife alone has no Route 204 north of it). + const bool walks = step.kind == StoryStepKind::ROUTE || step.kind == StoryStepKind::LEGS || + step.kind == StoryStepKind::STEP || step.kind == StoryStepKind::TALK; + const StoryWorld* on = step.kind == StoryStepKind::ROUTE && step.target && !state.pose.map.empty() + ? story_plan_world(state.pose.map, step.target->map) : nullptr; + // A world of one map is sim_world()'s, with its people. + if (on != nullptr && on == story_plan_world(state.pose.map, state.pose.map)){ + on = nullptr; + } + if (walks && !sync(state.pose, on)){ + m_logger.log(std::format("[{}] {}: no map for {}; passed over.", m_row, step.name, Pokemon::to_string(state.pose))); + m_passed_over = true; + return Pokemon::outcome_ok(); + } + return Pokemon::StoryStepExecutor::run(state, step); +} + +Outcome SimExecutor::run(RunState& state, const StoryStep& step){ + Outcome outcome; + m_passed_over = false; + try{ + outcome = run_step(state, step); + }catch (const GridSimFailure& e){ + outcome = Pokemon::outcome_failed("sim_failure", e.message); + } + const std::string mismatch = outcome.succeeded() && !m_passed_over ? check(step, outcome) : ""; + if (!mismatch.empty()){ + outcome = Pokemon::outcome_failed("sim_mismatch", step.name + ": " + mismatch); + } + // What the simulation does not play (scenes, events, battles) ends + // where the table says it does. + if (outcome.succeeded() && step.pose_after){ + sync(*step.pose_after); + } + m_logger.log(std::format("[{}] {}: {}, the player at {}.", m_row, step.name, Pokemon::to_string(outcome), to_string(m_sim.player().pos))); + if (std::exchange(m_misses.missed, false) && m_result.missed_step.empty()){ + m_result.missed_step = m_row + " " + step.name; + } + if (!outcome.succeeded() && m_result.error.empty()){ + // The walker's last notes, to see why. + for (size_t i = m_notes.size() - std::min(m_notes.size(), 40); i < m_notes.size(); i++){ + m_logger.log(std::format("[{}] note: {}", m_row, m_notes[i])); + } + m_result.error = m_row + " " + Pokemon::to_string(outcome); + m_result.failed_step = m_row + " " + step.name; + } + m_result.world = m_placed ? &m_sim.world() : nullptr; + m_result.end = nearest_tile(m_sim.player().pos); + m_ran.insert(step.name); + arm(step.name); + return outcome; +} + + +// Runs `range` of `rows` (the table unless given) on `executor`, stopping +// at the first failure. +Pokemon::SegmentReport run_rows_on( + SimExecutor& executor, Logger& logger, const Pokemon::SegmentRange& range, + const std::vector& rows = story_checkpoints() +){ + std::set saved; + Pokemon::Segment segment = make_story_segment(rows, executor, saved); + Pokemon::SegmentPolicy policy; + policy.stop_at_first_failure = true; + Pokemon::SegmentHooks hooks; + hooks.reload = [](RunState&){ return Pokemon::outcome_ok(); }; + hooks.log = [&logger](const std::string& line){ logger.log(line); }; + RunState state; + return Pokemon::run_segment(state, segment, range, policy, hooks); +} + +// Runs `range` of the table on the simulation, stopping at the first failure. +UnitTestResult run_rows(Logger& logger, const Pokemon::SegmentRange& range){ + SimExecutor executor(logger); + const Pokemon::SegmentReport report = run_rows_on(executor, logger, range); + for (const std::string& step : executor.result().unchecked){ + logger.log("No end to check after " + step + "."); + } + if (!report.completed){ + return std::format("stopped at {} {}: {}", report.failed_checkpoint, report.failed_event, executor.result().error); + } + return true; +} + +// The table with row `row`'s step of `step`'s name replaced by `step`. +std::vector table_with(const std::string& row, const StoryStep& step){ + std::vector rows = story_checkpoints(); + for (StoryCheckpoint& checkpoint : rows){ + for (StoryStep& old : checkpoint.steps){ + if (checkpoint.id == row && old.name == step.name){ + old = step; + return rows; + } + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No step " + step.name + " in " + row); +} + +bool ends_on(const SimRun& run, const Pose& pose){ + const std::vector tiles = Pokemon::tiles_of(pose); + const std::optional local = run.world ? story_to_local(*run.world, pose.map, run.end) : std::nullopt; + return local && std::find(tiles.begin(), tiles.end(), *local) != tiles.end(); +} +std::string end_of(const SimRun& run){ + return run.world ? describe(*run.world, run.end) : "nowhere"; +} + + +UnitTestResult run_exit_test(Logger& logger, const std::string& row_id, const StoryStep& old_step, const SimMisses& misses, const Pose& overshoot){ + SimExecutor executor(logger, misses); + const std::vector rows = table_with(row_id, old_step); + run_rows_on(executor, logger, {.from = row_id, .to = row_id}, rows); + const SimRun& run = executor.result(); + logger.log(std::format("The old {}: {}, ended on {}.", old_step.name, run.error, end_of(run))); + if (run.failed_step != row_id + " " + old_step.name || !ends_on(run, overshoot)){ + return std::format("the old {} should end on {} and fail there; it ended on {}: {}", + old_step.name, Pokemon::to_string(overshoot), end_of(run), run.error.empty() ? "no failure" : run.error); + } + SimExecutor clean(logger); + if (!run_rows_on(clean, logger, {.from = row_id, .to = row_id}).completed){ + return "the table's row fails without misses: " + clean.result().error; + } + std::vector runs; + for (size_t i = 0; i < clean.misses().fades; i++){ + runs.push_back({.fade = i}); + } + for (size_t i = 0; i < clean.misses().banners; i++){ + runs.push_back({.banner = i}); + } + for (const SimMisses& m : runs){ + SimExecutor sim(logger, m); + run_rows_on(sim, logger, {.from = row_id, .to = row_id}); + if (ends_on(sim.result(), overshoot)){ + return std::format("the table's {} ended on {} with a miss in {}.", old_step.name, end_of(sim.result()), + sim.result().missed_step); + } + } + return true; +} + +// r75: out of the Oreburgh Gate as a run west until Route 203's banner +// (the old event west_until_transition()). The banner reads a few tiles +// in: the run ends west of the Gate's warp tile (55, 9), in the pocket by +// the pond (Route 203's x 49 to 54, rows 6 to 13). +StoryStep r75_old_gate_exit(){ + StoryStep step; + step.kind = StoryStepKind::LEGS; + step.name = "out_of_oreburgh_gate"; + step.legs = {{.direction = Direction::WEST, .tiles = 8}}; + step.variant = "to_map"; + step.pose_after = Pokemon::tile_pose("Route203", {TilePos{55, 9}}, Direction::WEST); + return step; +} +Pose r75_pond_pocket(){ + std::vector tiles; + for (int y = 6; y <= 13; y++){ + for (int x = 49; x <= 54; x++){ + tiles.push_back({x, y}); + } + } + return Pokemon::tile_pose("Route203", tiles); +} +// r81: out of the Ravaged Path as a timed walk south (the old event's +// 4 s walk_dir()), its fade missed: on past cave_exit (20, 26) to the +// cliff, column 20 down to row 30. +StoryStep r81_old_cave_exit(){ + StoryStep step; + step.kind = StoryStepKind::LEGS; + step.name = "out_of_the_ravaged_path"; + step.legs = {{.direction = Direction::SOUTH, .tiles = 4}}; + step.variant = "to_wall"; + step.pose_after = Pokemon::tile_pose("Route204North", {TilePos{20, 26}}, Direction::SOUTH); + return step; +} +Pose r81_below_the_mouth(){ + return Pokemon::tile_pose("Route204North", {TilePos{20, 27}, TilePos{20, 28}, TilePos{20, 29}, TilePos{20, 30}}); +} + + +UnitTestResult test_coord_events(Logger&){ + for (const auto& [map, json] : bdsp_map_json()){ + const StoryWorld* world = story_plan_world(map, map); + for (const CoordEvent& event : world == nullptr ? std::vector{} : story_coord_events(map)){ + const auto open = [&](const TilePos& pos){ + const TilePos tile = story_to_world(*world, map, pos); + return standable(*world, tile) || world->tile(tile) == Pokemon::TileKind::DOOR; + }; + if (!std::ranges::any_of(event.tiles, open)){ + return std::format("{} {}: no tile is open ground or a door.", map, event.name); + } + } + } + const size_t armed = story_triggers().size(); + return armed == story_vars().size() ? UnitTestResult(true) + : UnitTestResult(std::format("Expected {} coord events armed by the story's variables, found {}.", story_vars().size(), armed)); +} + +std::string miss(Logger& logger, const SimRun& clean, bool fade, size_t index){ + size_t k = 0; + while (k + 1 < clean.rows.size() && (fade ? clean.rows[k + 1].fades : clean.rows[k + 1].banners) <= index){ + k++; + } + const SimRun::RowStart& start = clean.rows[k]; + SimMisses misses; + (fade ? misses.fade : misses.banner) = index - (fade ? start.fades : start.banners); + SimExecutor executor(logger, misses); + const Pokemon::SegmentReport report = run_rows_on(executor, logger, + {.from = start.row, .to = clean.rows[std::min(k + 1, clean.rows.size() - 1)].row}); + const SimRun& run = executor.result(); + const std::string what = std::format("{} {}", fade ? "fade" : "banner", index); + logger.log(std::format("Missed {} in {}: {}.", what, run.missed_step, + report.completed ? "got over it" : "stopped in " + run.failed_step + ": " + run.error)); + if (report.completed || run.failed_step == run.missed_step){ + return ""; + } + return std::format("{} missed in {}, failed later in {}: {}", what, run.missed_step, run.failed_step, run.error); +} + +UnitTestResult test_story_sim_misses(Logger& logger){ + Pokemon::SegmentRange range{.from = "00.00", .to = "01.11"}; + SimExecutor clean(logger); + if (!run_rows_on(clean, logger, range).completed){ + return "the run fails without misses: " + clean.result().error; + } + logger.log(std::format("{} fades and {} banners to miss.", clean.misses().fades, clean.misses().banners)); + std::vector unseen; + for (size_t i = 0; i < clean.misses().fades + clean.misses().banners; i++){ + const std::string line = miss(logger, clean.result(), i < clean.misses().fades, i < clean.misses().fades ? i : i - clean.misses().fades); + if (!line.empty()){ + unseen.push_back(line); + } + } + std::string out = "Misses that went unseen:"; + for (const std::string& line : unseen){ + out += "\n " + line; + } + return unseen.empty() ? UnitTestResult(true) : UnitTestResult(out); +} + +UnitTestResult test_story_sim_whole_table(Logger& logger){ + return run_rows(logger, {.from = "00.00", .to = "01.11"}); +} + +UnitTestResult test_story_sim_each_row(Logger& logger){ + for (const StoryCheckpoint& row : story_checkpoints()){ + const auto res = run_rows(logger, {.from = row.id, .to = row.id}); + if (res.result != UnitTestResult::PASSED){ + return std::format("Row {} failed: {}", row.id, res.message); + } + } + return true; +} + +UnitTestResult test_exit_gate(Logger& logger){ + return run_exit_test(logger, "01.05", r75_old_gate_exit(), SimMisses{}, r75_pond_pocket()); +} + +UnitTestResult test_exit_ravaged_path(Logger& logger){ + return run_exit_test(logger, "01.09", r81_old_cave_exit(), SimMisses{.fade = 0}, r81_below_the_mouth()); +} + +struct AutoStoryStorySimInput{ + std::string name; + std::function body; +}; + +struct AutoStoryStorySimExpectation{ +}; + +struct AutoStoryStorySimCase{ + AutoStoryStorySimInput input; + AutoStoryStorySimExpectation expectation; +}; + +class Test_AutoStoryStorySim : public UnitTest{ +public: + explicit Test_AutoStoryStorySim(AutoStoryStorySimCase test_case) + : UnitTest("PokemonBDSP::AutoStory - Story sim: " + test_case.input.name) + , m_case(std::move(test_case)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_case.input.body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + AutoStoryStorySimCase m_case; +}; + +} // namespace + + +const AutoStoryStorySimCase AUTO_STORY_STORY_SIM_CASES[] = { + { + .input = {"the maps' coord events are on open ground or doors", test_coord_events}, + .expectation = {}, + }, + { + .input = {"detector misses from 00.00 to 01.11", test_story_sim_misses}, + .expectation = {}, + }, + { + .input = {"00.00 to 01.11: the whole table, from the new game to Floaroma", test_story_sim_whole_table}, + .expectation = {}, + }, + { + .input = {"each row from 00.00 to 01.11 in isolation", test_story_sim_each_row}, + .expectation = {}, + }, + { + .input = {"r75, out of the Oreburgh Gate", test_exit_gate}, + .expectation = {}, + }, + { + .input = {"r81, out of the Ravaged Path", test_exit_ravaged_path}, + .expectation = {}, + }, +}; + +void add_tests_AutoStory_StorySim(UnitTestDatabase& database){ + for (const auto& item : AUTO_STORY_STORY_SIM_CASES){ + database.add(item); + } +} + + +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h index dfa12e07be..bc1ed57d3a 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.h @@ -22,7 +22,7 @@ void add_tests_AutoStory_StorySim(UnitTestDatabase& database); -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp index 8e92652ef0..275717d800 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.cpp @@ -145,7 +145,7 @@ TapLook look_after_tap(VideoStream& stream, ProControllerContext& context, const } } -} +} // namespace ConsoleStoryStepIO::ConsoleStoryStepIO(VideoStream& stream, ProControllerContext& context) @@ -211,6 +211,6 @@ void ConsoleStoryStepIO::settle(Button button, const std::string& label, bool fa -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h index 60691c86b0..a6175834ac 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryStepIO.h @@ -27,6 +27,7 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ +// Abstract interface for story step input/output operations. class StoryStepIO{ public: virtual ~StoryStepIO() = default; @@ -80,7 +81,7 @@ class ConsoleStoryStepIO : public StoryStepIO{ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp index 8fc3c7c3c5..067a6281af 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.cpp @@ -44,7 +44,7 @@ std::string label_of(const StoryStep& step){ throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "RouteIO::note() returned from a failure: " + message); } -} +} // namespace void StoryWalkSteps::begin_attempt(const std::string& row){ @@ -259,6 +259,6 @@ Outcome StoryWalkSteps::mash(RunState& state, const StoryStep& step){ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h index 8c21209913..fa89abaf14 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StoryWalkSteps.h @@ -23,6 +23,7 @@ namespace NintendoSwitch{ namespace PokemonBDSP{ +// Executes walking and movement-related story steps via StoryStepIO. class StoryWalkSteps{ public: explicit StoryWalkSteps(StoryStepIO& io) @@ -68,7 +69,7 @@ class StoryWalkSteps{ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp index ab0bc9134b..fd90d56444 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.cpp @@ -67,7 +67,7 @@ Milliseconds per_tile(Direction direction, SimPace pace, bool can_run){ return route_ms_per_tile(direction, pace == SimPace::RUN && can_run ? MoveSpeed::RUN : MoveSpeed::WALK); } -} +} // namespace SimPos sim_pos(const TilePos& tile){ @@ -290,7 +290,7 @@ WalkStop walk_stop(SimStop stop){ return WalkStop::LIMIT; } -} +} // namespace WalkResult GridSimRouteIO::walk_to_wall(const RouteWallWalk& walk){ const SimPace pace = walk.options.pace == DpadPace::RUN ? SimPace::RUN : SimPace::WALK; @@ -359,6 +359,6 @@ void GridSimRouteIO::note(const std::string& text, RouteNote kind){ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h index f28b20d067..727aac0518 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/Utils/PokemonBDSP_AutoStory_GridSim.h @@ -48,6 +48,7 @@ struct SimPos{ int y = 0; bool operator==(const SimPos& other) const = default; }; +// Converts a TilePos to its fixed-point fractional simulation coordinate. SimPos sim_pos(const Pokemon::TilePos& tile); // Whether the player stands exactly on a tile. bool on_grid(const SimPos& pos); @@ -76,6 +77,7 @@ struct SimWorld{ std::optional can_run = std::nullopt; }; +// Reason why simulated player movement stopped. enum class SimStop{ DONE, // the input finished BLOCKED, // stopped by a wall, a person, or a door off its column @@ -83,6 +85,7 @@ enum class SimStop{ WARP, // stepped onto a door LIMIT, // a held walk ran out of time }; +// Returns the debug string for a SimStop reason. const char* sim_stop_name(SimStop stop); // What one input did. @@ -114,6 +117,7 @@ enum class SimPace{ RUN, // B held: a run once the player has the Running Shoes }; +// Simulates grid-based player movement and collisions in BDSP. class GridSim{ public: GridSim(SimWorld world, const Pokemon::TilePos& start, Pokemon::Direction facing, bool can_run); @@ -180,7 +184,7 @@ struct GridSimFailure{ -} -} -} +} // namespace PokemonBDSP +} // namespace NintendoSwitch +} // namespace PokemonAutomation #endif From f453d2aca5c98a7863a27fecbe458463fb1b69e7 Mon Sep 17 00:00:00 2001 From: dayfine Date: Thu, 5 Nov 2026 22:54:39 -0800 Subject: [PATCH 6/6] BDSP AutoStory sim: initialize all designated fields for Windows Clang --- .../AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp index af21d4bf85..0d3d718354 100644 --- a/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp +++ b/SerialPrograms/Source/PokemonBDSP/Programs/AutoStory/PokemonBDSP_AutoStory_StorySim_Tests.cpp @@ -473,7 +473,7 @@ SimWorld SimExecutor::setup(const StoryWorld* world) const{ if (sim_world(armed.map) != world){ continue; } - SimTrigger trigger{.name = armed.trigger.name}; + SimTrigger trigger{.name = armed.trigger.name, .tiles = {}}; for (const TilePos& pos : armed.trigger.tiles){ trigger.tiles.push_back(story_to_world(*world, armed.map, pos)); } @@ -710,10 +710,10 @@ UnitTestResult run_exit_test(Logger& logger, const std::string& row_id, const St } std::vector runs; for (size_t i = 0; i < clean.misses().fades; i++){ - runs.push_back({.fade = i}); + runs.push_back({.fade = i, .banner = std::nullopt}); } for (size_t i = 0; i < clean.misses().banners; i++){ - runs.push_back({.banner = i}); + runs.push_back({.fade = std::nullopt, .banner = i}); } for (const SimMisses& m : runs){ SimExecutor sim(logger, m); @@ -844,7 +844,7 @@ UnitTestResult test_exit_gate(Logger& logger){ } UnitTestResult test_exit_ravaged_path(Logger& logger){ - return run_exit_test(logger, "01.09", r81_old_cave_exit(), SimMisses{.fade = 0}, r81_below_the_mouth()); + return run_exit_test(logger, "01.09", r81_old_cave_exit(), SimMisses{.fade = 0, .banner = std::nullopt}, r81_below_the_mouth()); } struct AutoStoryStorySimInput{