From 6f65a95e58f6401c1dd852487ba19ac9a5cedf24 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 15:27:51 -0800 Subject: [PATCH 01/13] FRLG AutoStory: plan the Starter RNG attempts from SID Helper's candidates With "Stop for Starter RNG" on, AutoStory now searches Starter RNG targets (#181's search_rng_targets) for the TID and SID Helper's candidate SIDs, orders them so each attempt covers the likeliest SIDs not yet ruled out, and lists them in the stop message and log. RngPlanTracker holds the decision after each attempt (shiny, hit but not shiny, missed), for the in-process follow-up. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 2 + .../AutoStory/PokemonFRLG_AutoStory.cpp | 152 +++++- .../AutoStory/PokemonFRLG_AutoStory.h | 17 + .../PokemonFRLG_AutoStoryRngPlan.cpp | 349 ++++++++++++++ .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 172 +++++++ .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 448 ++++++++++++++++++ SerialPrograms/cmake/SourceFiles.cmake | 3 + 7 files changed, 1136 insertions(+), 7 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index d2779cc410..850d865001 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -38,6 +38,7 @@ #include "Programs/PokemonFRLG_NamingScreenKeyboard.h" #include "PokemonFRLG_Navigation.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h" +#include "Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryTable.h" #include "Programs/RngManipulation/PokemonFRLG_RngTargetSearch_Tests.h" #include "Tests/TestUtils.h" @@ -698,6 +699,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory(database); add_tests_AutoStorySimulation(database); add_tests_AutoStoryItems(database); + add_tests_AutoStoryRngPlan(database); add_tests_NamingCursor(database); add_tests_NamingPlan(database); add_tests_NamingKeys(database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index e0556e91f7..5f9b1e41b1 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -80,10 +80,65 @@ AutoStory::AutoStory() "Stop for Starter RNG:
Off by default. At the end of 00.04, save south of the table facing Squirtle's ball " "and stop, where the Starter RNG program starts. After Starter RNG, save right after the rival's battle without moving " "and start AutoStory at 00.05.
" - "With this off, 00.04 and 00.04b run as one checkpoint, as before.", + "With this off, 00.04 and 00.04b run as one checkpoint, as before.
" + "With this on, the run lists the Starter RNG targets to try, in order, from the RNG plan options below.", LockMode::LOCK_WHILE_RUNNING, false ) + , RNG_SOUND( + "RNG plan: Sound:
The in-game sound setting, as in Starter RNG. It picks the seeds.", + { + {SoundSetting::Mono, "mono", "Mono"}, + {SoundSetting::Stereo, "stereo", "Stereo"} + }, + LockMode::LOCK_WHILE_RUNNING, + SoundSetting::Mono + ) + , RNG_TID( + "RNG plan: TID:
The Trainer ID SID Helper read.", + LockMode::LOCK_WHILE_RUNNING, + 0 + ) + , RNG_SID( + false, + "RNG plan: SID:
The SID, once known. Empty: the targets are planned for the SIDs SID Helper lists, " + "from the two options below.", + LockMode::LOCK_WHILE_RUNNING, + "", "" + ) + , SID_HELPER_ADVANCES( + "RNG plan: SID Helper's Target Advances:
As set in SID Helper.", + LockMode::LOCK_WHILE_RUNNING, + 3001, 2275 + ) + , SID_HELPER_CANDIDATES( + "RNG plan: SID Helper's # Candidate SIDs:
As set in SID Helper. The middle one counts as the likeliest.", + LockMode::LOCK_WHILE_RUNNING, + 5, 1, 11 + ) + , RNG_SHINY_REQUIRED( + "RNG plan: Shiny required:
Only targets shiny for a SID candidate, tried until one comes out shiny. " + "Off: the quickest target that passes the nature and IV filters.", + LockMode::LOCK_WHILE_RUNNING, + true + ) + , RNG_NATURES( + false, + "RNG plan: Natures:
Separated by commas, e.g. \"Modest, Timid\". Empty: any nature.", + LockMode::LOCK_WHILE_RUNNING, + "", "Modest, Timid" + ) + , RNG_MIN_IVS( + false, + "RNG plan: Minimum IVs:
HP/Atk/Def/SpA/SpD/Spe.", + LockMode::LOCK_WHILE_RUNNING, + "0/0/0/0/0/0", "0/0/0/0/0/0" + ) + , RNG_MAX_ATTEMPTS( + "RNG plan: Max attempts:
The most targets to list.", + LockMode::LOCK_WHILE_RUNNING, + 5, 1, 20 + ) , NOTIFICATIONS({ &NOTIFICATION_PROGRAM_FINISH, &NOTIFICATION_ERROR_FATAL, @@ -94,6 +149,15 @@ AutoStory::AutoStory() PA_ADD_OPTION(PLAYER_NAME); PA_ADD_OPTION(STOP_FOR_SID_HELPER); PA_ADD_OPTION(STOP_FOR_STARTER_RNG); + PA_ADD_OPTION(RNG_SOUND); + PA_ADD_OPTION(RNG_TID); + PA_ADD_OPTION(RNG_SID); + PA_ADD_OPTION(SID_HELPER_ADVANCES); + PA_ADD_OPTION(SID_HELPER_CANDIDATES); + PA_ADD_OPTION(RNG_SHINY_REQUIRED); + PA_ADD_OPTION(RNG_NATURES); + PA_ADD_OPTION(RNG_MIN_IVS); + PA_ADD_OPTION(RNG_MAX_ATTEMPTS); PA_ADD_OPTION(NOTIFICATIONS); } @@ -214,11 +278,64 @@ static void prepare_start( } // The run stopped for an RNG program, as its option asked: the user's -// turn, with what to do next. -static void stop_for_handoff(SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, const std::string& message){ +// turn, with what to do next, and the lines that go with it (the RNG +// plan's attempts). +static void stop_for_handoff( + SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, + const std::string& message, const std::vector& lines = {} +){ env.console.log(message, COLOR_BLUE); env.console.overlay().add_log(message); - send_program_finished_notification(env, notification, message); + std::string full = message; + for (const std::string& line : lines){ + env.console.log(line, COLOR_BLUE); + env.console.overlay().add_log(line); + full += "\n" + line; + } + send_program_finished_notification(env, notification, full); +} + +std::vector AutoStory::plan_starter_attempts(ConsoleHandle& console) const{ + std::vector candidates; + if (std::string(RNG_SID).empty()){ + candidates = sid_helper_candidates(RNG_TID, SID_HELPER_ADVANCES, SID_HELPER_CANDIDATES); + }else{ + const std::optional sid = parse_sid(RNG_SID); + if (!sid){ + throw UserSetupError(console, "RNG plan: the SID must be a number from 0 to 65535, or empty."); + } + candidates.push_back({*sid, 1}); + } + const std::optional> natures = parse_natures(RNG_NATURES); + if (!natures){ + throw UserSetupError(console, "RNG plan: natures must be nature names separated by commas, e.g. \"Modest, Timid\"."); + } + const std::optional min_ivs = parse_ivs(RNG_MIN_IVS); + if (!min_ivs){ + throw UserSetupError(console, "RNG plan: minimum IVs must be six numbers from 0 to 31, e.g. \"0/0/0/31/0/31\"."); + } + + RngTargetFilters filters; + filters.natures = *natures; + filters.min_ivs = *min_ivs; + filters.shiny_required = RNG_SHINY_REQUIRED; + const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ + // AutoStory plays FireRed only. + .firered = true, + .language = RUN.language(), + .sound = RNG_SOUND, + .tid = RNG_TID, + .candidates = candidates, + .filters = filters, + .max_attempts = RNG_MAX_ATTEMPTS, + }); + if (attempts.empty()){ + throw UserSetupError(console, + "RNG plan: no Starter RNG target passes the natures, minimum IVs and shiny options for TID " + + std::to_string(RNG_TID) + "." + ); + } + return attempts; } void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ @@ -231,14 +348,28 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex checkpoints, first, last, StoryHandoffs{.sid_helper = STOP_FOR_SID_HELPER, .starter_rng = STOP_FOR_STARTER_RNG} ); if (plan.stop == StoryHandoff::SID_HELPER){ + // SID Helper's steps (set_sid_from_name_screen() to read_tid() in + // PokemonFRLG_SidHelper.cpp) could run here instead of stopping; + // they are file-static there for now. play_new_game_to_player_name(env.console, context, player_name); stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH, "Stopped for SID Helper: the player's name is typed and OK is not pressed. " - "Run SID Helper now, then start AutoStory at 00.01b." + "Run SID Helper now. Enter the TID it reads, its Target Advances and its # Candidate SIDs " + "in AutoStory's RNG plan options, then start AutoStory at 00.01b." ); return; } + // The Starter RNG targets, worked out before anything is pressed so a + // setup problem shows now, not after 00.04. + std::vector rng_lines; + if (plan.stop == StoryHandoff::STARTER_RNG){ + rng_lines = rng_attempt_lines(plan_starter_attempts(env.console)); + for (const std::string& line : rng_lines){ + env.console.log("Starter RNG plan: " + line); + } + } + StoryEnv story{env.console, context, checkpoints[first].supplies, checkpoints[first].supplies, false, RUN.checks()}; story.player_name = player_name; prepare_start(env, context, checkpoints, first, story); @@ -257,9 +388,16 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex env.update_stats(); throw_if_failed(report); if (plan.stop == StoryHandoff::STARTER_RNG){ + // Starter RNG runs its own reset and calibration loop in its + // program(); the attempts are the user's to run until that loop can + // be called from here. stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH, - "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. Run Starter RNG now. " - "Then save right after the rival's battle, without moving, and start AutoStory at 00.05." + "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. " + "Run Starter RNG (Squirtle, FireRed, Sound " + std::string(RNG_SOUND == SoundSetting::Stereo ? "Stereo" : "Mono") + ") " + "with attempt 1's Target Seed and Advances. If it hits the target and it isn't shiny, load this save " + "and run the next attempt. If it is shiny, its SID is the one listed (or one of them): enter it as the " + "RNG plan's SID. Then save right after the rival's battle, without moving, and start AutoStory at 00.05.", + rng_lines ); return; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h index 8fd043efb9..b96269d50d 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h @@ -14,11 +14,14 @@ #include "Common/Cpp/Options/BooleanCheckBoxOption.h" #include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/SimpleIntegerOption.h" #include "Common/Cpp/Options/StringOption.h" #include "CommonFramework/Notifications/EventNotificationsTable.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "Pokemon/Options/Pokemon_StoryRunOptions.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" #include "PokemonFRLG_AutoStoryActions.h" +#include "PokemonFRLG_AutoStoryRngPlan.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -42,6 +45,10 @@ class AutoStory : public SingleSwitchProgramInstance{ ) override{} private: + // The Starter RNG attempts the RNG plan options ask for. Throws + // UserSetupError for an option it can't read, or when no target passes. + std::vector plan_starter_attempts(ConsoleHandle& console) const; + // The start and end checkpoints, attempts, checks and language, as in // every game's AutoStory. Pokemon::StoryRunOptions RUN; @@ -50,6 +57,16 @@ class AutoStory : public SingleSwitchProgramInstance{ // The RNG programs the run stops for (StoryHandoff). BooleanCheckBoxOption STOP_FOR_SID_HELPER; BooleanCheckBoxOption STOP_FOR_STARTER_RNG; + // What the Starter RNG stop lists (PokemonFRLG_AutoStoryRngPlan.h). + EnumDropdownOption RNG_SOUND; + SimpleIntegerOption RNG_TID; + StringOption RNG_SID; + SimpleIntegerOption SID_HELPER_ADVANCES; + SimpleIntegerOption SID_HELPER_CANDIDATES; + BooleanCheckBoxOption RNG_SHINY_REQUIRED; + StringOption RNG_NATURES; + StringOption RNG_MIN_IVS; + SimpleIntegerOption RNG_MAX_ATTEMPTS; EventNotificationsOption NOTIFICATIONS; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp new file mode 100644 index 0000000000..5180e27c01 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp @@ -0,0 +1,349 @@ +/* AutoStory RNG Plan + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "PokemonFRLG_AutoStoryRngPlan.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +std::vector sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count){ + // As SidHelper's get_sid_messages(): from "count / 2" steps before the target. + const int start = -int(count) / 2; + const int end = start + int(count); + const int farthest = std::max(-start, end - 1); + + std::vector candidates; + AdvRngSearcher searcher(tid, uint64_t(int64_t(target_advances) + 2 * start)); + for (int i = start; i < end; i++){ + candidates.push_back({uint16_t(searcher.state.s0 >> 16), double(farthest + 1 - std::abs(i))}); + searcher.advance_state(); + searcher.advance_state(); + } + return candidates; +} + + +namespace{ + +std::string trim(const std::string& text){ + const size_t first = text.find_first_not_of(' '); + if (first == std::string::npos){ + return ""; + } + const size_t last = text.find_last_not_of(' '); + return text.substr(first, last - first + 1); +} + +std::vector split(const std::string& text, char separator){ + std::vector parts; + size_t start = 0; + while (true){ + const size_t next = text.find(separator, start); + parts.push_back(trim(text.substr(start, next == std::string::npos ? std::string::npos : next - start))); + if (next == std::string::npos){ + return parts; + } + start = next + 1; + } +} + +} + + +std::optional parse_sid(const std::string& text){ + const std::string digits = trim(text); + if (digits.empty() || digits.size() > 5 || !std::all_of(digits.begin(), digits.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ + return std::nullopt; + } + const int value = std::stoi(digits); + if (value > 0xffff){ + return std::nullopt; + } + return uint16_t(value); +} + +std::optional parse_ivs(const std::string& text){ + const std::vector parts = split(text, '/'); + if (parts.size() != 6){ + return std::nullopt; + } + uint8_t values[6]; + for (size_t c = 0; c < 6; c++){ + const std::string& part = parts[c]; + if (part.empty() || part.size() > 2 || !std::all_of(part.begin(), part.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ + return std::nullopt; + } + const int value = std::stoi(part); + if (value > 31){ + return std::nullopt; + } + values[c] = uint8_t(value); + } + AdvIVs ivs; + ivs.hp = values[0]; + ivs.attack = values[1]; + ivs.defense = values[2]; + ivs.spatk = values[3]; + ivs.spdef = values[4]; + ivs.speed = values[5]; + return ivs; +} + +std::string ivs_to_string(const AdvIVs& ivs){ + return std::format("{}/{}/{}/{}/{}/{}", ivs.hp, ivs.attack, ivs.defense, ivs.spatk, ivs.spdef, ivs.speed); +} + +std::optional> parse_natures(const std::string& text){ + std::vector natures; + if (trim(text).empty()){ + return natures; + } + for (const std::string& name : split(text, ',')){ + const AdvNature nature = string_to_nature(name); + if (nature == AdvNature::Any){ + return std::nullopt; + } + natures.push_back(nature); + } + return natures; +} + + +namespace{ + +// "targets" with only the shiny candidates still "left", and their share of +// the weight left. +std::vector rescore( + const std::vector& targets, + const std::vector& candidates, + const std::vector& left +){ + double total = 0; + for (size_t c = 0; c < candidates.size(); c++){ + if (left[c]){ + total += candidates[c].weight; + } + } + std::vector scored; + scored.reserve(targets.size()); + for (const RngTarget& target : targets){ + RngTarget copy = target; + copy.shiny_for.clear(); + double weight = 0; + for (const ShinyCandidate& shiny : target.shiny_for){ + if (left[shiny.candidate]){ + copy.shiny_for.push_back(shiny); + weight += candidates[shiny.candidate].weight; + } + } + copy.shiny_probability = total > 0 ? weight / total : 0; + scored.emplace_back(std::move(copy)); + } + return scored; +} + +RngAttempt make_attempt(const RngTarget& target, const std::vector& candidates){ + RngAttempt attempt{target, {}}; + for (const ShinyCandidate& shiny : target.shiny_for){ + attempt.sids.push_back(candidates[shiny.candidate].sid); + } + return attempt; +} + +} + + +std::vector plan_rng_attempts( + const std::vector& targets, + const std::vector& candidates, + bool shiny_required, + size_t max_attempts +){ + std::vector attempts; + if (targets.empty() || max_attempts == 0){ + return attempts; + } + std::vector left(candidates.size(), true); + + if (!shiny_required){ + // With no shiny chance, rank_rng_targets() sorts by time alone. + std::vector by_time = targets; + for (RngTarget& target : by_time){ + target.shiny_probability = 0; + } + rank_rng_targets(by_time); + const uint32_t quickest = by_time.front().state; + for (const RngTarget& target : rescore(targets, candidates, left)){ + if (target.state == quickest){ + attempts.push_back(make_attempt(target, candidates)); + break; + } + } + return attempts; + } + + while (attempts.size() < max_attempts){ + std::vector scored = rescore(targets, candidates, left); + rank_rng_targets(scored); + const RngTarget& best = scored.front(); + if (best.shiny_for.empty()){ + break; + } + for (const ShinyCandidate& shiny : best.shiny_for){ + left[shiny.candidate] = false; + } + attempts.push_back(make_attempt(best, candidates)); + if (std::find(left.begin(), left.end(), true) == left.end()){ + break; + } + } + return attempts; +} + + +RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResult& result){ + if (result.shiny){ + return RngAttemptOutcome::SHINY; + } + const AdvIVs& want = target.pokemon.ivs; + const AdvIVs& got = result.ivs; + const bool same_ivs = want.hp == got.hp && want.attack == got.attack && want.defense == got.defense + && want.spatk == got.spatk && want.spdef == got.spdef && want.speed == got.speed; + return result.nature == target.pokemon.nature && same_ivs + ? RngAttemptOutcome::HIT_NOT_SHINY + : RngAttemptOutcome::MISSED; +} + + +RngPlanTracker::RngPlanTracker( + std::vector attempts, + const std::vector& candidates, + bool shiny_required, + size_t max_retries +) + : m_attempts(std::move(attempts)) + , m_shiny_required(shiny_required) + , m_max_retries(max_retries) + , m_step(m_attempts.empty() ? RngPlanStep::OUT_OF_ATTEMPTS : RngPlanStep::RUN_ATTEMPT) +{ + for (const SidCandidate& candidate : candidates){ + m_remaining.push_back(candidate.sid); + } +} + +std::optional RngPlanTracker::confirmed_sid() const{ + if (m_step == RngPlanStep::GOT_SHINY && m_remaining.size() == 1){ + return m_remaining.front(); + } + return std::nullopt; +} + +bool RngPlanTracker::is_ruled_out(uint16_t sid) const{ + return std::find(m_remaining.begin(), m_remaining.end(), sid) == m_remaining.end(); +} + +RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ + if (m_step != RngPlanStep::RUN_ATTEMPT){ + return m_step; + } + const std::vector& sids = m_attempts[m_attempt].sids; + switch (outcome){ + case RngAttemptOutcome::SHINY:{ + // The SID is one the target is shiny for. None of them: it wasn't + // among the candidates. + std::vector narrowed; + for (uint16_t sid : m_remaining){ + if (std::find(sids.begin(), sids.end(), sid) != sids.end()){ + narrowed.push_back(sid); + } + } + m_remaining = std::move(narrowed); + m_step = RngPlanStep::GOT_SHINY; + return m_step; + } + case RngAttemptOutcome::HIT_NOT_SHINY: + if (!m_shiny_required){ + m_step = RngPlanStep::GOT_TARGET; + return m_step; + } + m_remaining.erase( + std::remove_if(m_remaining.begin(), m_remaining.end(), [&](uint16_t sid){ + return std::find(sids.begin(), sids.end(), sid) != sids.end(); + }), + m_remaining.end() + ); + m_retries = 0; + // The next attempt still shiny for a candidate left. + for (size_t next = m_attempt + 1; next < m_attempts.size(); next++){ + const std::vector& next_sids = m_attempts[next].sids; + if (!std::all_of(next_sids.begin(), next_sids.end(), [&](uint16_t sid){ return is_ruled_out(sid); })){ + m_attempt = next; + return m_step; + } + } + m_step = RngPlanStep::OUT_OF_ATTEMPTS; + return m_step; + case RngAttemptOutcome::MISSED: + m_retries++; + if (m_retries > m_max_retries){ + m_step = RngPlanStep::OUT_OF_RETRIES; + } + return m_step; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngAttemptOutcome."); +} + + +std::vector plan_starter_rng(const StarterRngPlanInputs& inputs){ + const SeedsDatabase database(seeds_json_path(inputs.firered, inputs.language)); + // Only the English lists hold the blackout button's seeds (initial_seed_entries()). + const bool english_list = inputs.language != Language::Japanese; + const std::vector seeds = initial_seed_entries(database, inputs.sound, "Help", english_list); + + RngTargetFilters filters = inputs.filters; + filters.min_advances = std::max(filters.min_advances, STARTER_RNG_MIN_ADVANCES); + const std::vector targets = search_rng_targets(seeds, inputs.tid, inputs.candidates, filters); + return plan_rng_attempts(targets, inputs.candidates, filters.shiny_required, inputs.max_attempts); +} + + +std::vector rng_attempt_lines(const std::vector& attempts){ + std::vector lines; + for (size_t c = 0; c < attempts.size(); c++){ + const RngTarget& target = attempts[c].target; + std::string shiny; + if (attempts[c].sids.empty()){ + shiny = "not shiny for any SID candidate"; + }else{ + shiny = "shiny for SID"; + for (size_t s = 0; s < attempts[c].sids.size(); s++){ + shiny += (s == 0 ? " " : ", ") + std::to_string(attempts[c].sids[s]); + } + shiny += std::format(" ({:.0f}% of the candidates left)", target.shiny_probability * 100); + } + lines.push_back(std::format( + "Attempt {}: Target Seed {:04X}, Advances {}: {}, IVs {}, PID {:08X}, {}.", + c + 1, target.seed.seed, target.advances, + nature_to_string(target.pokemon.nature), ivs_to_string(target.pokemon.ivs), target.pokemon.pid, + shiny + )); + } + return lines; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h new file mode 100644 index 0000000000..2640f9070a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h @@ -0,0 +1,172 @@ +/* AutoStory RNG Plan + * + * From: https://github.com/PokemonAutomation/ + * + * Which Starter RNG targets AutoStory asks for, in order, when the SID is + * only narrowed down to SID Helper's candidates, and what to do after each + * one. The loop is the one PokemonFRLG_RngTargetSearch.h describes: + * - Shiny: the SID is one of those the target is shiny for. With one left, + * it is confirmed and later targets can be searched with it alone. + * - Nature and IVs match, but not shiny: the target was hit, so the SIDs it + * is shiny for are wrong. Go on to the next attempt. + * - Nature or IVs differ: the target was missed. Run it again. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryRngPlan_H +#define PokemonAutomation_PokemonFRLG_AutoStoryRngPlan_H + +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "CommonFramework/Language.h" +#include "Pokemon/Pokemon_AdvRng.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The SIDs SID Helper lists for "tid": "count" of them, 2 advances apart, +// centered on "target_advances", as its get_sid_messages() lists them. The +// middle one, where SID Helper aims, weighs the most, and each step away one +// less, so the outermost weigh 1. +std::vector sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count); + +// A SID typed as a number from 0 to 65535. Nothing for any other text. +std::optional parse_sid(const std::string& text); + +// "HP/Atk/Def/SpA/SpD/Spe", as Ten Lines and the RNG programs list IVs. +// Nothing for text that isn't six numbers from 0 to 31. +std::optional parse_ivs(const std::string& text); +std::string ivs_to_string(const Pokemon::AdvIVs& ivs); + +// Natures separated by commas ("Modest, Timid"). Empty text is any nature +// (no entries). Nothing for a name that isn't a nature. +std::optional> parse_natures(const std::string& text); + + +struct RngAttempt{ + RngTarget target; + // The SIDs the target is shiny for, of those the attempts before it + // leave. Empty when the target isn't shiny for any of them. + std::vector sids; +}; + +// The attempts to make, in order, for SID candidates "candidates" (the ones +// "targets" were searched with). Each is the best target by +// rank_rng_targets() over the candidates the attempts before it leave, so a +// target shiny for several candidates comes before one shiny for one, unless +// it takes much longer. Stops after "max_attempts", or once no target is +// shiny for a candidate left. +// +// With "shiny_required" off, a hit ends the plan whether it is shiny or not, +// so the plan is the one quickest target. +std::vector plan_rng_attempts( + const std::vector& targets, + const std::vector& candidates, + bool shiny_required, + size_t max_attempts +); + + +// What came out of one attempt. +enum class RngAttemptOutcome{ + SHINY, + // The target's nature and IVs, not shiny. + HIT_NOT_SHINY, + // Another nature or other IVs: not the target. + MISSED, +}; + +struct RngAttemptResult{ + bool shiny; + Pokemon::AdvNature nature; + Pokemon::AdvIVs ivs; +}; +RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResult& result); + + +enum class RngPlanStep{ + // Run current_attempt(): the first, the next, or the same again after a miss. + RUN_ATTEMPT, + // Shiny. remaining_sids() are the SIDs it can be. + GOT_SHINY, + // Shiny not required, and the target was hit. + GOT_TARGET, + // Every attempt was hit and none was shiny. + OUT_OF_ATTEMPTS, + // One attempt was missed more often than the retries allow. + OUT_OF_RETRIES, +}; + +// Follows the plan through the attempts' outcomes. +class RngPlanTracker{ +public: + RngPlanTracker( + std::vector attempts, + const std::vector& candidates, + bool shiny_required, + size_t max_retries + ); + + RngPlanStep step() const{ return m_step; } + size_t attempt_index() const{ return m_attempt; } + const RngAttempt& current_attempt() const{ return m_attempts[m_attempt]; } + // The SID candidates not ruled out, in the order given. + const std::vector& remaining_sids() const{ return m_remaining; } + // The SID, once a shiny leaves one candidate. + std::optional confirmed_sid() const; + + // The next step after the current attempt came out as "outcome". Once + // the plan has ended, the step stays as it is. + RngPlanStep record(RngAttemptOutcome outcome); + +private: + bool is_ruled_out(uint16_t sid) const; + + std::vector m_attempts; + bool m_shiny_required; + size_t m_max_retries; + std::vector m_remaining; + size_t m_attempt = 0; + size_t m_retries = 0; + RngPlanStep m_step; +}; + + +// What Starter RNG's targets are searched with. +struct StarterRngPlanInputs{ + bool firered; + Language language; + SoundSetting sound; + uint16_t tid; + std::vector candidates; + RngTargetFilters filters; + size_t max_attempts; +}; + +// Starter RNG's smallest Advances setting. +inline constexpr uint64_t STARTER_RNG_MIN_ADVANCES = 940; + +// The attempts for Starter RNG, from the seed list of the version and +// language in Help button mode, the one Starter RNG uses. +std::vector plan_starter_rng(const StarterRngPlanInputs& inputs); + +// One line per attempt, for the log and the stop message: the Target Seed +// and Advances to give Starter RNG, the Pokemon, and the SIDs it is shiny for. +std::vector rng_attempt_lines(const std::vector& attempts); + + +void add_tests_AutoStoryRngPlan(UnitTestDatabase& database); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp new file mode 100644 index 0000000000..8f565b3c14 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -0,0 +1,448 @@ +/* AutoStory RNG Plan Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The attempt order, and the step after each attempt's outcome, on made-up + * targets; SID Helper's candidates and a Starter RNG plan from the FireRed + * seed list. + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_AutoStoryRngPlan.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +class Test_AutoStoryRngPlan : public UnitTest{ +public: + Test_AutoStoryRngPlan(std::string name, std::function body) + : UnitTest("PokemonFRLG::AutoStoryRngPlan - " + 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; +}; + + +std::string join(const std::vector& values){ + std::string text; + for (uint16_t value : values){ + text += (text.empty() ? "" : ",") + std::to_string(value); + } + return "{" + text + "}"; +} + + +// SIDs 100, 200 and 300 weigh 3, 2 and 1. +const std::vector CANDIDATES = {{100, 3}, {200, 2}, {300, 1}}; + +// A target reached "seconds" after the reset, shiny for the candidates at +// "shiny" (indices into CANDIDATES). +RngTarget make_target(uint32_t state, uint64_t seconds, std::vector shiny){ + RngTarget target{}; + target.state = state; + target.seed = {uint16_t(state), seconds * 1000, SeedButton::A, BlackoutButton::None}; + target.advances = 0; + target.pokemon = method1_pokemon(state); + for (size_t candidate : shiny){ + target.shiny_for.push_back({candidate, AdvShinyType::Star}); + } + return target; +} + +// By expected wait per shiny over all three (weight 6): 1 = 50 s / (5/6) = 60, +// 2 = 20 s / (1/6) = 120, 3 = 40 s / (3/6) = 80. Then over SID 300 alone, 2 is +// the only one shiny for it. 4 is the quickest and shiny for none. +const std::vector TARGETS = { + make_target(1, 50, {0, 1}), + make_target(2, 20, {2}), + make_target(3, 40, {0}), + make_target(4, 5, {}), +}; + + +UnitTestResult test_sid_helper_candidates(Logger&){ + const uint16_t tid = 43771; + const uint32_t target_advances = 3001; + const std::vector candidates = sid_helper_candidates(tid, target_advances, 5); + + // The SID is the top half of the state after the TID seed is advanced + // once, then "advances" more times. + auto sid_at = [&](uint32_t advances){ + uint32_t state = tid; + for (uint32_t c = 0; c <= advances; c++){ + state = lcrng_next(state); + } + return uint16_t(state >> 16); + }; + const std::vector advances = {2997, 2999, 3001, 3003, 3005}; + const std::vector weights = {1, 2, 3, 2, 1}; + if (candidates.size() != advances.size()){ + return std::format("Expected {} candidates, got {}.", advances.size(), candidates.size()); + } + for (size_t c = 0; c < candidates.size(); c++){ + if (candidates[c].sid != sid_at(advances[c]) || candidates[c].weight != weights[c]){ + return std::format( + "Candidate {}: expected SID {} weight {}, got SID {} weight {}.", + c, sid_at(advances[c]), weights[c], candidates[c].sid, candidates[c].weight + ); + } + } + + const std::vector even = sid_helper_candidates(tid, target_advances, 4); + const std::vector even_weights = {1, 2, 3, 2}; + for (size_t c = 0; c < even.size() && c < even_weights.size(); c++){ + if (even.size() != even_weights.size() || even[c].sid != candidates[c].sid || even[c].weight != even_weights[c]){ + return std::format("Four candidates: expected the first four of five, weighing 1, 2, 3, 2; candidate {} differs.", c); + } + } + return true; +} + + +UnitTestResult test_parse_sid(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "64694", .expected = "64694"}, + {.text = " 0 ", .expected = "0"}, + {.text = "65535", .expected = "65535"}, + {.text = "65536", .expected = "none"}, + {.text = "", .expected = "none"}, + {.text = "-1", .expected = "none"}, + {.text = "12a", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional sid = parse_sid(item.text); + const std::string got = sid ? std::to_string(*sid) : "none"; + if (got != item.expected){ + return std::format("SID \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +UnitTestResult test_parse_ivs(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "31/14/7/29/1/27", .expected = "31/14/7/29/1/27"}, + {.text = " 0 / 0/0/0/0/31 ", .expected = "0/0/0/0/0/31"}, + {.text = "31/0/0", .expected = "none"}, + {.text = "32/0/0/0/0/0", .expected = "none"}, + {.text = "a/0/0/0/0/0", .expected = "none"}, + {.text = "-1/0/0/0/0/0", .expected = "none"}, + {.text = "", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional ivs = parse_ivs(item.text); + const std::string got = ivs ? ivs_to_string(*ivs) : "none"; + if (got != item.expected){ + return std::format("IVs \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +UnitTestResult test_parse_natures(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "", .expected = "any"}, + {.text = " ", .expected = "any"}, + {.text = "Modest", .expected = "Modest"}, + {.text = "Modest, Timid", .expected = "Modest Timid"}, + {.text = "Modset", .expected = "none"}, + {.text = "Modest,", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional> natures = parse_natures(item.text); + std::string got; + if (!natures){ + got = "none"; + }else if (natures->empty()){ + got = "any"; + }else{ + for (AdvNature nature : *natures){ + got += (got.empty() ? "" : " ") + nature_to_string(nature); + } + } + if (got != item.expected){ + return std::format("Natures \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + + +UnitTestResult test_plan_order(Logger&){ + struct Case{ + std::string name; + std::vector targets; + bool shiny_required; + size_t max_attempts; + std::vector states; + std::vector> sids; + }; + const std::vector no_300 = {TARGETS[0], TARGETS[2], TARGETS[3]}; + const Case cases[] = { + { + .name = "two candidates in one target first, then the last", + .targets = TARGETS, .shiny_required = true, .max_attempts = 5, + .states = {1, 2}, .sids = {{100, 200}, {300}}, + }, + { + .name = "capped at one attempt", + .targets = TARGETS, .shiny_required = true, .max_attempts = 1, + .states = {1}, .sids = {{100, 200}}, + }, + { + .name = "no target shiny for SID 300", + .targets = no_300, .shiny_required = true, .max_attempts = 5, + .states = {1}, .sids = {{100, 200}}, + }, + { + .name = "shiny not required: the quickest", + .targets = TARGETS, .shiny_required = false, .max_attempts = 5, + .states = {4}, .sids = {{}}, + }, + { + .name = "no targets", + .targets = {}, .shiny_required = true, .max_attempts = 5, + .states = {}, .sids = {}, + }, + }; + for (const Case& item : cases){ + const std::vector attempts = plan_rng_attempts(item.targets, CANDIDATES, item.shiny_required, item.max_attempts); + std::vector states; + std::vector> sids; + for (const RngAttempt& attempt : attempts){ + states.push_back(attempt.target.state); + sids.push_back(attempt.sids); + } + if (states != item.states || sids != item.sids){ + std::string got; + for (size_t c = 0; c < attempts.size(); c++){ + got += std::format(" target {} {}", states[c], join(sids[c])); + } + return item.name + ": got" + (got.empty() ? " no attempts" : got) + "."; + } + } + return true; +} + + +UnitTestResult test_classify(Logger&){ + const RngTarget hit = TARGETS[0]; + AdvIVs other_ivs = hit.pokemon.ivs; + other_ivs.speed = uint8_t((other_ivs.speed + 1) % 32); + const AdvNature other_nature = hit.pokemon.nature == AdvNature::Hardy ? AdvNature::Lonely : AdvNature::Hardy; + + struct Case{ + std::string name; + RngAttemptResult result; + RngAttemptOutcome expected; + }; + const Case cases[] = { + {.name = "shiny", .result = {.shiny = true, .nature = other_nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::SHINY}, + {.name = "same nature and IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::HIT_NOT_SHINY}, + {.name = "other IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::MISSED}, + {.name = "other nature", .result = {.shiny = false, .nature = other_nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::MISSED}, + }; + for (const Case& item : cases){ + if (classify_attempt(hit, item.result) != item.expected){ + return item.name + ": classified wrong."; + } + } + return true; +} + + +const char* step_name(RngPlanStep step){ + switch (step){ + case RngPlanStep::RUN_ATTEMPT: return "RUN_ATTEMPT"; + case RngPlanStep::GOT_SHINY: return "GOT_SHINY"; + case RngPlanStep::GOT_TARGET: return "GOT_TARGET"; + case RngPlanStep::OUT_OF_ATTEMPTS: return "OUT_OF_ATTEMPTS"; + case RngPlanStep::OUT_OF_RETRIES: return "OUT_OF_RETRIES"; + } + return "?"; +} + +UnitTestResult test_tracker(Logger&){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + struct Case{ + std::string name; + bool shiny_required; + std::vector outcomes; + // The step after each outcome, and the attempt run next. + std::vector steps; + std::vector attempts; + std::vector remaining; + std::optional confirmed; + }; + const Case cases[] = { + { + .name = "shiny at once: one of two SIDs", + .shiny_required = true, .outcomes = {SHINY}, + .steps = {GOT_SHINY}, .attempts = {0}, + .remaining = {100, 200}, .confirmed = std::nullopt, + }, + { + .name = "hit, then shiny: SID confirmed", + .shiny_required = true, .outcomes = {HIT_NOT_SHINY, SHINY}, + .steps = {RUN_ATTEMPT, GOT_SHINY}, .attempts = {1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "a miss retries the same target", + .shiny_required = true, .outcomes = {MISSED, MISSED, HIT_NOT_SHINY, MISSED}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT}, .attempts = {0, 0, 1, 1}, + .remaining = {300}, .confirmed = std::nullopt, + }, + { + .name = "three misses in a row with two retries", + .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, OUT_OF_RETRIES, OUT_OF_RETRIES}, .attempts = {0, 0, 0, 0}, + .remaining = {100, 200, 300}, .confirmed = std::nullopt, + }, + { + .name = "every attempt hit, none shiny", + .shiny_required = true, .outcomes = {HIT_NOT_SHINY, HIT_NOT_SHINY}, + .steps = {RUN_ATTEMPT, OUT_OF_ATTEMPTS}, .attempts = {1, 1}, + .remaining = {}, .confirmed = std::nullopt, + }, + { + .name = "shiny not required: a hit ends it", + .shiny_required = false, .outcomes = {HIT_NOT_SHINY}, + .steps = {GOT_TARGET}, .attempts = {0}, + .remaining = {100, 200, 300}, .confirmed = std::nullopt, + }, + }; + const std::vector attempts = plan_rng_attempts(TARGETS, CANDIDATES, true, 5); + for (const Case& item : cases){ + RngPlanTracker tracker(attempts, CANDIDATES, item.shiny_required, 2); + if (tracker.step() != RUN_ATTEMPT){ + return item.name + ": expected to start with RUN_ATTEMPT, got " + step_name(tracker.step()) + "."; + } + for (size_t c = 0; c < item.outcomes.size(); c++){ + const RngPlanStep step = tracker.record(item.outcomes[c]); + if (step != item.steps[c] || tracker.attempt_index() != item.attempts[c]){ + return std::format( + "{}: after outcome {}, expected {} at attempt {}, got {} at attempt {}.", + item.name, c, step_name(item.steps[c]), item.attempts[c], step_name(step), tracker.attempt_index() + ); + } + } + if (tracker.remaining_sids() != item.remaining || tracker.confirmed_sid() != item.confirmed){ + return std::format( + "{}: expected SIDs {} (confirmed {}), got {} (confirmed {}).", + item.name, join(item.remaining), item.confirmed ? std::to_string(*item.confirmed) : "none", + join(tracker.remaining_sids()), tracker.confirmed_sid() ? std::to_string(*tracker.confirmed_sid()) : "none" + ); + } + } + + RngPlanTracker empty({}, CANDIDATES, true, 2); + if (empty.step() != OUT_OF_ATTEMPTS){ + return std::string("An empty plan: expected OUT_OF_ATTEMPTS, got ") + step_name(empty.step()) + "."; + } + return true; +} + + +// A plan from the FireRed (English) list: every attempt starts from a seed +// Starter RNG finds, takes at least its smallest Advances, and is shiny for +// the SIDs it lists, which no earlier attempt listed. +UnitTestResult test_starter_plan(Logger&){ + const uint16_t tid = 43771; + StarterRngPlanInputs inputs{ + .firered = true, + .language = Language::English, + .sound = SoundSetting::Mono, + .tid = tid, + .candidates = sid_helper_candidates(tid, 3001, 5), + .filters = {}, + .max_attempts = 5, + }; + const std::vector attempts = plan_starter_rng(inputs); + if (attempts.empty()){ + return "Expected attempts for TID 43771 with five SID candidates, got none."; + } + + const SeedsDatabase database(seeds_json_path(true, Language::English)); + std::vector listed; + for (size_t c = 0; c < attempts.size(); c++){ + const RngAttempt& attempt = attempts[c]; + if (!database.find_seed(attempt.target.seed.seed, SoundSetting::Mono, 5).found){ + return std::format("Attempt {}: Starter RNG can't find Target Seed {:04X}.", c + 1, attempt.target.seed.seed); + } + if (attempt.target.advances < STARTER_RNG_MIN_ADVANCES){ + return std::format("Attempt {}: Advances {} below Starter RNG's {}.", c + 1, attempt.target.advances, STARTER_RNG_MIN_ADVANCES); + } + if (attempt.sids.empty()){ + return std::format("Attempt {}: shiny for no SID candidate.", c + 1); + } + for (uint16_t sid : attempt.sids){ + if (shiny_type(attempt.target.pokemon.pid, tid, sid) == AdvShinyType::Normal){ + return std::format("Attempt {}: PID {:08X} not shiny for SID {}.", c + 1, attempt.target.pokemon.pid, sid); + } + if (std::find(listed.begin(), listed.end(), sid) != listed.end()){ + return std::format("Attempt {}: SID {} already listed by an earlier attempt.", c + 1, sid); + } + listed.push_back(sid); + } + } + if (rng_attempt_lines(attempts).size() != attempts.size()){ + return "Expected one line per attempt."; + } + return true; +} + + +} + + +void add_tests_AutoStoryRngPlan(UnitTestDatabase& database){ + database.add("SID Helper's candidates and weights", test_sid_helper_candidates); + database.add("SID text", test_parse_sid); + database.add("IVs text", test_parse_ivs); + database.add("natures text", test_parse_natures); + database.add("attempt order", test_plan_order); + database.add("attempt outcome", test_classify); + database.add("step after each outcome", test_tracker); + database.add("Starter RNG plan from the FireRed list", test_starter_plan); +} + + + +} +} +} diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 947f585518..20e44b6a98 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1840,6 +1840,9 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItems_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp From 143a931234b341d092913b1e5d3523ec8f37478b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 15:29:53 -0800 Subject: [PATCH 02/13] FRLG AutoStory: move the RNG plan options into their own group The options and the plan they build go in StarterRngPlanOptions, which keeps AutoStory's constructor and program() short; the long tests take their cases from functions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStory.cpp | 160 +++--------------- .../AutoStory/PokemonFRLG_AutoStory.h | 19 +-- .../PokemonFRLG_AutoStoryRngOptions.cpp | 114 +++++++++++++ .../PokemonFRLG_AutoStoryRngOptions.h | 61 +++++++ .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 98 ++++++----- SerialPrograms/cmake/SourceFiles.cmake | 2 + 6 files changed, 260 insertions(+), 194 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 5f9b1e41b1..111808a103 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -79,66 +79,11 @@ AutoStory::AutoStory() , STOP_FOR_STARTER_RNG( "Stop for Starter RNG:
Off by default. At the end of 00.04, save south of the table facing Squirtle's ball " "and stop, where the Starter RNG program starts. After Starter RNG, save right after the rival's battle without moving " - "and start AutoStory at 00.05.
" - "With this off, 00.04 and 00.04b run as one checkpoint, as before.
" - "With this on, the run lists the Starter RNG targets to try, in order, from the RNG plan options below.", + "and start AutoStory at 00.05, listing the targets to try from the RNG plan.
" + "With this off, 00.04 and 00.04b run as one checkpoint, as before.", LockMode::LOCK_WHILE_RUNNING, false ) - , RNG_SOUND( - "RNG plan: Sound:
The in-game sound setting, as in Starter RNG. It picks the seeds.", - { - {SoundSetting::Mono, "mono", "Mono"}, - {SoundSetting::Stereo, "stereo", "Stereo"} - }, - LockMode::LOCK_WHILE_RUNNING, - SoundSetting::Mono - ) - , RNG_TID( - "RNG plan: TID:
The Trainer ID SID Helper read.", - LockMode::LOCK_WHILE_RUNNING, - 0 - ) - , RNG_SID( - false, - "RNG plan: SID:
The SID, once known. Empty: the targets are planned for the SIDs SID Helper lists, " - "from the two options below.", - LockMode::LOCK_WHILE_RUNNING, - "", "" - ) - , SID_HELPER_ADVANCES( - "RNG plan: SID Helper's Target Advances:
As set in SID Helper.", - LockMode::LOCK_WHILE_RUNNING, - 3001, 2275 - ) - , SID_HELPER_CANDIDATES( - "RNG plan: SID Helper's # Candidate SIDs:
As set in SID Helper. The middle one counts as the likeliest.", - LockMode::LOCK_WHILE_RUNNING, - 5, 1, 11 - ) - , RNG_SHINY_REQUIRED( - "RNG plan: Shiny required:
Only targets shiny for a SID candidate, tried until one comes out shiny. " - "Off: the quickest target that passes the nature and IV filters.", - LockMode::LOCK_WHILE_RUNNING, - true - ) - , RNG_NATURES( - false, - "RNG plan: Natures:
Separated by commas, e.g. \"Modest, Timid\". Empty: any nature.", - LockMode::LOCK_WHILE_RUNNING, - "", "Modest, Timid" - ) - , RNG_MIN_IVS( - false, - "RNG plan: Minimum IVs:
HP/Atk/Def/SpA/SpD/Spe.", - LockMode::LOCK_WHILE_RUNNING, - "0/0/0/0/0/0", "0/0/0/0/0/0" - ) - , RNG_MAX_ATTEMPTS( - "RNG plan: Max attempts:
The most targets to list.", - LockMode::LOCK_WHILE_RUNNING, - 5, 1, 20 - ) , NOTIFICATIONS({ &NOTIFICATION_PROGRAM_FINISH, &NOTIFICATION_ERROR_FATAL, @@ -149,15 +94,7 @@ AutoStory::AutoStory() PA_ADD_OPTION(PLAYER_NAME); PA_ADD_OPTION(STOP_FOR_SID_HELPER); PA_ADD_OPTION(STOP_FOR_STARTER_RNG); - PA_ADD_OPTION(RNG_SOUND); - PA_ADD_OPTION(RNG_TID); - PA_ADD_OPTION(RNG_SID); - PA_ADD_OPTION(SID_HELPER_ADVANCES); - PA_ADD_OPTION(SID_HELPER_CANDIDATES); - PA_ADD_OPTION(RNG_SHINY_REQUIRED); - PA_ADD_OPTION(RNG_NATURES); - PA_ADD_OPTION(RNG_MIN_IVS); - PA_ADD_OPTION(RNG_MAX_ATTEMPTS); + PA_ADD_OPTION(RNG_PLAN); PA_ADD_OPTION(NOTIFICATIONS); } @@ -295,47 +232,20 @@ static void stop_for_handoff( send_program_finished_notification(env, notification, full); } -std::vector AutoStory::plan_starter_attempts(ConsoleHandle& console) const{ - std::vector candidates; - if (std::string(RNG_SID).empty()){ - candidates = sid_helper_candidates(RNG_TID, SID_HELPER_ADVANCES, SID_HELPER_CANDIDATES); - }else{ - const std::optional sid = parse_sid(RNG_SID); - if (!sid){ - throw UserSetupError(console, "RNG plan: the SID must be a number from 0 to 65535, or empty."); - } - candidates.push_back({*sid, 1}); - } - const std::optional> natures = parse_natures(RNG_NATURES); - if (!natures){ - throw UserSetupError(console, "RNG plan: natures must be nature names separated by commas, e.g. \"Modest, Timid\"."); - } - const std::optional min_ivs = parse_ivs(RNG_MIN_IVS); - if (!min_ivs){ - throw UserSetupError(console, "RNG plan: minimum IVs must be six numbers from 0 to 31, e.g. \"0/0/0/31/0/31\"."); - } - - RngTargetFilters filters; - filters.natures = *natures; - filters.min_ivs = *min_ivs; - filters.shiny_required = RNG_SHINY_REQUIRED; - const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ - // AutoStory plays FireRed only. - .firered = true, - .language = RUN.language(), - .sound = RNG_SOUND, - .tid = RNG_TID, - .candidates = candidates, - .filters = filters, - .max_attempts = RNG_MAX_ATTEMPTS, - }); - if (attempts.empty()){ - throw UserSetupError(console, - "RNG plan: no Starter RNG target passes the natures, minimum IVs and shiny options for TID " - + std::to_string(RNG_TID) + "." - ); - } - return attempts; +// Starter RNG runs its own reset and calibration loop in its program(), so +// the attempts are the user's to run until that loop can be called from here. +static void stop_for_starter_rng( + SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, + const std::string& sound, const std::vector& attempts +){ + stop_for_handoff(env, notification, + "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. " + "Run Starter RNG (Squirtle, FireRed, Sound " + sound + ") with attempt 1's Target Seed and Advances. " + "If it hits the target and it isn't shiny, load this save and run the next attempt. If it is shiny, " + "its SID is the one listed (or one of them): enter it as the RNG plan's SID. " + "Then save right after the rival's battle, without moving, and start AutoStory at 00.05.", + attempts + ); } void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ @@ -348,27 +258,19 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex checkpoints, first, last, StoryHandoffs{.sid_helper = STOP_FOR_SID_HELPER, .starter_rng = STOP_FOR_STARTER_RNG} ); if (plan.stop == StoryHandoff::SID_HELPER){ - // SID Helper's steps (set_sid_from_name_screen() to read_tid() in - // PokemonFRLG_SidHelper.cpp) could run here instead of stopping; - // they are file-static there for now. + // SID Helper's steps (set_sid_from_name_screen() to read_tid(), + // file-static in PokemonFRLG_SidHelper.cpp) could run here instead. play_new_game_to_player_name(env.console, context, player_name); stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH, - "Stopped for SID Helper: the player's name is typed and OK is not pressed. " - "Run SID Helper now. Enter the TID it reads, its Target Advances and its # Candidate SIDs " - "in AutoStory's RNG plan options, then start AutoStory at 00.01b." + "Stopped for SID Helper: the player's name is typed and OK is not pressed. Run SID Helper now, " + "enter its TID, Target Advances and # Candidate SIDs in the RNG plan, then start AutoStory at 00.01b." ); return; } - - // The Starter RNG targets, worked out before anything is pressed so a - // setup problem shows now, not after 00.04. - std::vector rng_lines; - if (plan.stop == StoryHandoff::STARTER_RNG){ - rng_lines = rng_attempt_lines(plan_starter_attempts(env.console)); - for (const std::string& line : rng_lines){ - env.console.log("Starter RNG plan: " + line); - } - } + // Planned before anything is pressed, so a setup problem shows now. + const std::vector rng_attempts = plan.stop == StoryHandoff::STARTER_RNG + ? rng_attempt_lines(RNG_PLAN.plan(env.console, RUN.language())) + : std::vector{}; StoryEnv story{env.console, context, checkpoints[first].supplies, checkpoints[first].supplies, false, RUN.checks()}; story.player_name = player_name; @@ -388,17 +290,7 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex env.update_stats(); throw_if_failed(report); if (plan.stop == StoryHandoff::STARTER_RNG){ - // Starter RNG runs its own reset and calibration loop in its - // program(); the attempts are the user's to run until that loop can - // be called from here. - stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH, - "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. " - "Run Starter RNG (Squirtle, FireRed, Sound " + std::string(RNG_SOUND == SoundSetting::Stereo ? "Stereo" : "Mono") + ") " - "with attempt 1's Target Seed and Advances. If it hits the target and it isn't shiny, load this save " - "and run the next attempt. If it is shiny, its SID is the one listed (or one of them): enter it as the " - "RNG plan's SID. Then save right after the rival's battle, without moving, and start AutoStory at 00.05.", - rng_lines - ); + stop_for_starter_rng(env, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN.sound_name(), rng_attempts); return; } env.console.log("AutoStory: done through " + plan.rows.back().id + "."); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h index b96269d50d..274ad9e61e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.h @@ -14,14 +14,12 @@ #include "Common/Cpp/Options/BooleanCheckBoxOption.h" #include "Common/Cpp/Options/EnumDropdownOption.h" -#include "Common/Cpp/Options/SimpleIntegerOption.h" #include "Common/Cpp/Options/StringOption.h" #include "CommonFramework/Notifications/EventNotificationsTable.h" #include "NintendoSwitch/NintendoSwitch_SingleSwitchProgram.h" #include "Pokemon/Options/Pokemon_StoryRunOptions.h" -#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" #include "PokemonFRLG_AutoStoryActions.h" -#include "PokemonFRLG_AutoStoryRngPlan.h" +#include "PokemonFRLG_AutoStoryRngOptions.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -45,10 +43,6 @@ class AutoStory : public SingleSwitchProgramInstance{ ) override{} private: - // The Starter RNG attempts the RNG plan options ask for. Throws - // UserSetupError for an option it can't read, or when no target passes. - std::vector plan_starter_attempts(ConsoleHandle& console) const; - // The start and end checkpoints, attempts, checks and language, as in // every game's AutoStory. Pokemon::StoryRunOptions RUN; @@ -57,16 +51,7 @@ class AutoStory : public SingleSwitchProgramInstance{ // The RNG programs the run stops for (StoryHandoff). BooleanCheckBoxOption STOP_FOR_SID_HELPER; BooleanCheckBoxOption STOP_FOR_STARTER_RNG; - // What the Starter RNG stop lists (PokemonFRLG_AutoStoryRngPlan.h). - EnumDropdownOption RNG_SOUND; - SimpleIntegerOption RNG_TID; - StringOption RNG_SID; - SimpleIntegerOption SID_HELPER_ADVANCES; - SimpleIntegerOption SID_HELPER_CANDIDATES; - BooleanCheckBoxOption RNG_SHINY_REQUIRED; - StringOption RNG_NATURES; - StringOption RNG_MIN_IVS; - SimpleIntegerOption RNG_MAX_ATTEMPTS; + StarterRngPlanOptions RNG_PLAN; EventNotificationsOption NOTIFICATIONS; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp new file mode 100644 index 0000000000..049c5061ce --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp @@ -0,0 +1,114 @@ +/* AutoStory RNG Options + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "Common/Cpp/Exceptions.h" +#include "PokemonFRLG_AutoStoryRngOptions.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +StarterRngPlanOptions::StarterRngPlanOptions() + : GroupOption("RNG Plan (Stop for Starter RNG)", LockMode::LOCK_WHILE_RUNNING) + , SOUND( + "Sound:
The in-game sound setting, as in Starter RNG. It picks the seeds.", + {{SoundSetting::Mono, "mono", "Mono"}, {SoundSetting::Stereo, "stereo", "Stereo"}}, + LockMode::LOCK_WHILE_RUNNING, SoundSetting::Mono + ) + , TID("TID:
The Trainer ID SID Helper read.", LockMode::LOCK_WHILE_RUNNING, 0) + , SID( + false, + "SID:
The SID, once known. Empty: plan for the SIDs SID Helper lists, from the two options below.", + LockMode::LOCK_WHILE_RUNNING, "", "" + ) + , SID_HELPER_ADVANCES( + "SID Helper's Target Advances:
As set in SID Helper.", + LockMode::LOCK_WHILE_RUNNING, 3001, 2275 + ) + , SID_HELPER_CANDIDATES( + "SID Helper's # Candidate SIDs:
As set in SID Helper. The middle one counts as the likeliest.", + LockMode::LOCK_WHILE_RUNNING, 5, 1, 11 + ) + , SHINY_REQUIRED( + "Shiny required:
Only targets shiny for a SID candidate, tried until one comes out shiny. " + "Off: the quickest target that passes the natures and IVs.", + LockMode::LOCK_WHILE_RUNNING, true + ) + , NATURES( + false, "Natures:
Separated by commas. Empty: any nature.", + LockMode::LOCK_WHILE_RUNNING, "", "Modest, Timid" + ) + , MIN_IVS( + false, "Minimum IVs:
HP/Atk/Def/SpA/SpD/Spe.", + LockMode::LOCK_WHILE_RUNNING, "0/0/0/0/0/0", "0/0/0/0/0/0" + ) + , MAX_ATTEMPTS("Max attempts:
The most targets to list.", LockMode::LOCK_WHILE_RUNNING, 5, 1, 20) +{ + PA_ADD_OPTION(SOUND); + PA_ADD_OPTION(TID); + PA_ADD_OPTION(SID); + PA_ADD_OPTION(SID_HELPER_ADVANCES); + PA_ADD_OPTION(SID_HELPER_CANDIDATES); + PA_ADD_OPTION(SHINY_REQUIRED); + PA_ADD_OPTION(NATURES); + PA_ADD_OPTION(MIN_IVS); + PA_ADD_OPTION(MAX_ATTEMPTS); +} + +std::string StarterRngPlanOptions::sound_name() const{ + return SOUND == SoundSetting::Stereo ? "Stereo" : "Mono"; +} + +std::vector StarterRngPlanOptions::candidates(Logger& logger) const{ + const std::string sid_text = SID; + if (sid_text.empty()){ + return sid_helper_candidates(TID, SID_HELPER_ADVANCES, SID_HELPER_CANDIDATES); + } + const std::optional sid = parse_sid(sid_text); + if (!sid){ + throw UserSetupError(logger, "RNG plan: the SID must be a number from 0 to 65535, or empty."); + } + return {{*sid, 1}}; +} + +std::vector StarterRngPlanOptions::plan(Logger& logger, Language language) const{ + const std::optional> natures = parse_natures(NATURES); + if (!natures){ + throw UserSetupError(logger, "RNG plan: natures must be nature names separated by commas, e.g. \"Modest, Timid\"."); + } + const std::optional min_ivs = parse_ivs(MIN_IVS); + if (!min_ivs){ + throw UserSetupError(logger, "RNG plan: minimum IVs must be six numbers from 0 to 31, e.g. \"0/0/0/31/0/31\"."); + } + RngTargetFilters filters; + filters.natures = *natures; + filters.min_ivs = *min_ivs; + filters.shiny_required = SHINY_REQUIRED; + + const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ + // AutoStory plays FireRed only. + .firered = true, + .language = language, + .sound = SOUND, + .tid = TID, + .candidates = candidates(logger), + .filters = filters, + .max_attempts = MAX_ATTEMPTS, + }); + if (attempts.empty()){ + throw UserSetupError(logger, + "RNG plan: no Starter RNG target passes the natures, minimum IVs and shiny options for TID " + std::to_string(TID) + "." + ); + } + return attempts; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h new file mode 100644 index 0000000000..d0e6a9e5d0 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h @@ -0,0 +1,61 @@ +/* AutoStory RNG Options + * + * From: https://github.com/PokemonAutomation/ + * + * The RNG plan options: what AutoStory's Starter RNG stop searches targets + * with (PokemonFRLG_AutoStoryRngPlan.h). + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryRngOptions_H +#define PokemonAutomation_PokemonFRLG_AutoStoryRngOptions_H + +#include +#include +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/Options/BooleanCheckBoxOption.h" +#include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/GroupOption.h" +#include "Common/Cpp/Options/SimpleIntegerOption.h" +#include "Common/Cpp/Options/StringOption.h" +#include "CommonFramework/Language.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" +#include "PokemonFRLG_AutoStoryRngPlan.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +class StarterRngPlanOptions : public GroupOption{ +public: + StarterRngPlanOptions(); + + // The attempts these options ask for, for a FireRed game in "language". + // Throws UserSetupError for an option it can't read, or when no target + // passes. + std::vector plan(Logger& logger, Language language) const; + // The sound setting as Starter RNG's dropdown shows it. + std::string sound_name() const; + +public: + EnumDropdownOption SOUND; + SimpleIntegerOption TID; + StringOption SID; + SimpleIntegerOption SID_HELPER_ADVANCES; + SimpleIntegerOption SID_HELPER_CANDIDATES; + BooleanCheckBoxOption SHINY_REQUIRED; + StringOption NATURES; + StringOption MIN_IVS; + SimpleIntegerOption MAX_ATTEMPTS; + +private: + std::vector candidates(Logger& logger) const; +}; + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp index 8f565b3c14..aac50987b0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -201,17 +201,17 @@ UnitTestResult test_parse_natures(Logger&){ } -UnitTestResult test_plan_order(Logger&){ - struct Case{ - std::string name; - std::vector targets; - bool shiny_required; - size_t max_attempts; - std::vector states; - std::vector> sids; - }; +struct PlanCase{ + std::string name; + std::vector targets; + bool shiny_required; + size_t max_attempts; + std::vector states; + std::vector> sids; +}; +std::vector plan_cases(){ const std::vector no_300 = {TARGETS[0], TARGETS[2], TARGETS[3]}; - const Case cases[] = { + return { { .name = "two candidates in one target first, then the last", .targets = TARGETS, .shiny_required = true, .max_attempts = 5, @@ -238,7 +238,10 @@ UnitTestResult test_plan_order(Logger&){ .states = {}, .sids = {}, }, }; - for (const Case& item : cases){ +} + +UnitTestResult test_plan_order(Logger&){ + for (const PlanCase& item : plan_cases()){ const std::vector attempts = plan_rng_attempts(item.targets, CANDIDATES, item.shiny_required, item.max_attempts); std::vector states; std::vector> sids; @@ -295,20 +298,20 @@ const char* step_name(RngPlanStep step){ return "?"; } -UnitTestResult test_tracker(Logger&){ +struct TrackerCase{ + std::string name; + bool shiny_required; + std::vector outcomes; + // The step after each outcome, and the attempt run next. + std::vector steps; + std::vector attempts; + std::vector remaining; + std::optional confirmed; +}; +std::vector tracker_cases(){ using enum RngAttemptOutcome; using enum RngPlanStep; - struct Case{ - std::string name; - bool shiny_required; - std::vector outcomes; - // The step after each outcome, and the attempt run next. - std::vector steps; - std::vector attempts; - std::vector remaining; - std::optional confirmed; - }; - const Case cases[] = { + return { { .name = "shiny at once: one of two SIDs", .shiny_required = true, .outcomes = {SHINY}, @@ -346,32 +349,41 @@ UnitTestResult test_tracker(Logger&){ .remaining = {100, 200, 300}, .confirmed = std::nullopt, }, }; - const std::vector attempts = plan_rng_attempts(TARGETS, CANDIDATES, true, 5); - for (const Case& item : cases){ - RngPlanTracker tracker(attempts, CANDIDATES, item.shiny_required, 2); - if (tracker.step() != RUN_ATTEMPT){ - return item.name + ": expected to start with RUN_ATTEMPT, got " + step_name(tracker.step()) + "."; - } - for (size_t c = 0; c < item.outcomes.size(); c++){ - const RngPlanStep step = tracker.record(item.outcomes[c]); - if (step != item.steps[c] || tracker.attempt_index() != item.attempts[c]){ - return std::format( - "{}: after outcome {}, expected {} at attempt {}, got {} at attempt {}.", - item.name, c, step_name(item.steps[c]), item.attempts[c], step_name(step), tracker.attempt_index() - ); - } - } - if (tracker.remaining_sids() != item.remaining || tracker.confirmed_sid() != item.confirmed){ +} + +UnitTestResult check_tracker_case(const TrackerCase& item){ + RngPlanTracker tracker(plan_rng_attempts(TARGETS, CANDIDATES, true, 5), CANDIDATES, item.shiny_required, 2); + if (tracker.step() != RngPlanStep::RUN_ATTEMPT){ + return item.name + ": expected to start with RUN_ATTEMPT, got " + step_name(tracker.step()) + "."; + } + for (size_t c = 0; c < item.outcomes.size(); c++){ + const RngPlanStep step = tracker.record(item.outcomes[c]); + if (step != item.steps[c] || tracker.attempt_index() != item.attempts[c]){ return std::format( - "{}: expected SIDs {} (confirmed {}), got {} (confirmed {}).", - item.name, join(item.remaining), item.confirmed ? std::to_string(*item.confirmed) : "none", - join(tracker.remaining_sids()), tracker.confirmed_sid() ? std::to_string(*tracker.confirmed_sid()) : "none" + "{}: after outcome {}, expected {} at attempt {}, got {} at attempt {}.", + item.name, c, step_name(item.steps[c]), item.attempts[c], step_name(step), tracker.attempt_index() ); } } + if (tracker.remaining_sids() != item.remaining || tracker.confirmed_sid() != item.confirmed){ + return std::format( + "{}: expected SIDs {} (confirmed {}), got {} (confirmed {}).", + item.name, join(item.remaining), item.confirmed ? std::to_string(*item.confirmed) : "none", + join(tracker.remaining_sids()), tracker.confirmed_sid() ? std::to_string(*tracker.confirmed_sid()) : "none" + ); + } + return true; +} +UnitTestResult test_tracker(Logger&){ + for (const TrackerCase& item : tracker_cases()){ + const UnitTestResult result = check_tracker_case(item); + if (result.result != UnitTestResult::PASSED){ + return result; + } + } RngPlanTracker empty({}, CANDIDATES, true, 2); - if (empty.step() != OUT_OF_ATTEMPTS){ + if (empty.step() != RngPlanStep::OUT_OF_ATTEMPTS){ return std::string("An empty plan: expected OUT_OF_ATTEMPTS, got ") + step_name(empty.step()) + "."; } return true; diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 20e44b6a98..860cc5da9f 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1840,6 +1840,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryEvents.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItems_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp From 1c1c15a5cc88258ba5f40e90cd13a038619601d1 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 15:38:45 -0800 Subject: [PATCH 03/13] FRLG AutoStory: read the TID off the Trainer Card; log RngPlan lines SID Helper's navigate_to_trainer_card() and TID read move to a shared PokemonFRLG_TrainerCard.h; SID Helper calls them as before. TrainerIdReader gains try_read_tid(), which says when the digits can't be read. At the Starter RNG stop AutoStory reads the TID from the card unless the RNG plan's TID (now text, empty by default) overrides it, and stops with an error when it can't. The plan logs grep-able "RngPlan:" lines (TID, SID Helper's candidates, attempts, the confirmed SID's place among the candidates) so the candidate odds can be rebuilt from logs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Inference/PokemonFRLG_TrainerIdReader.cpp | 18 ++- .../Inference/PokemonFRLG_TrainerIdReader.h | 8 ++ .../AutoStory/PokemonFRLG_AutoStory.cpp | 44 +++++++- .../PokemonFRLG_AutoStoryRngOptions.cpp | 62 ++++++++--- .../PokemonFRLG_AutoStoryRngOptions.h | 20 +++- .../PokemonFRLG_AutoStoryRngPlan.cpp | 55 ++++++++- .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 24 +++- .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 34 +++++- .../Programs/PokemonFRLG_TrainerCard.cpp | 105 ++++++++++++++++++ .../Programs/PokemonFRLG_TrainerCard.h | 41 +++++++ .../RngManipulation/PokemonFRLG_SidHelper.cpp | 64 +---------- SerialPrograms/cmake/SourceFiles.cmake | 2 + 12 files changed, 385 insertions(+), 92 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.cpp index f505088994..7569dcab12 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.cpp @@ -40,6 +40,22 @@ void TrainerIdReader::make_overlays(VideoOverlaySet &items) const { uint16_t TrainerIdReader::read_tid( Logger& logger, Language language, const ImageViewRGB32& frame +){ + return uint16_t(read_tid_digits(logger, language, frame)); +} + +std::optional TrainerIdReader::try_read_tid( + Logger& logger, Language language, const ImageViewRGB32& frame +){ + const int tid = read_tid_digits(logger, language, frame); + if (tid < 0 || tid > 0xffff){ + return std::nullopt; + } + return uint16_t(tid); +} + +int TrainerIdReader::read_tid_digits( + Logger& logger, Language language, const ImageViewRGB32& frame ){ ImageViewRGB32 game_screen = extract_box_reference(frame, GameSettings::instance().GAME_BOX); @@ -52,7 +68,7 @@ uint16_t TrainerIdReader::read_tid( // waterfill segmentation + template matching // against the PokemonFRLG/Digits/0-9.png templates. - return uint16_t(read_digits_waterfill_template(logger, tid_region, DigitTemplateType::DialogBox)); + return read_digits_waterfill_template(logger, tid_region, DigitTemplateType::DialogBox); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.h index c763913704..87be48324e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.h @@ -34,8 +34,16 @@ class TrainerIdReader { uint16_t read_tid( Logger& logger, Language language, const ImageViewRGB32& frame ); + // As read_tid(), but nothing when the digits can't be read or are more + // than a TID can be, instead of a wrapped-around number. + std::optional try_read_tid( + Logger& logger, Language language, const ImageViewRGB32& frame + ); private: + // The digits read, or -1. + int read_tid_digits(Logger& logger, Language language, const ImageViewRGB32& frame); + Color m_color; ImageFloatBox m_box_tid; ImageFloatBox m_box_tid_spa; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 111808a103..44e3d74cb2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -7,6 +7,7 @@ #include #include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedException.h" +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" #include "CommonFramework/Notifications/ProgramNotifications.h" #include "CommonFramework/VideoPipeline/VideoOverlay.h" #include "Pokemon/AutoStory/Pokemon_AutoStoryStats.h" @@ -14,6 +15,7 @@ #include "Pokemon/Pokemon_Strings.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h" +#include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryTable.h" #include "PokemonFRLG_AutoStory.h" @@ -232,8 +234,36 @@ static void stop_for_handoff( send_program_finished_notification(env, notification, full); } +// The TID for the Starter RNG plan: the RNG plan's, or read off the +// Trainer Card (START, the player's entry), which is then closed again. +// Before the first Pokemon, as at the stop, the player's entry is second. +static uint16_t starter_rng_tid( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const StarterRngPlanOptions& options, Language language +){ + std::optional tid = options.tid_override(env.console); + if (tid){ + env.console.log(rng_log_tid(*tid, false)); + return *tid; + } + navigate_to_trainer_card(env.console, context); + tid = try_read_trainer_card_tid(env.console, context, language); + if (!tid){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::NO_ERROR_REPORT, + "Couldn't read the TID on the Trainer Card, so there is no Starter RNG plan. The game is saved facing " + "Squirtle's ball, where Starter RNG starts. Type the TID into the RNG plan's TID option for the next run.", + env.console + ); + } + close_trainer_card(env.console, context); + env.console.log(rng_log_tid(*tid, true)); + return *tid; +} + // Starter RNG runs its own reset and calibration loop in its program(), so // the attempts are the user's to run until that loop can be called from here. +// TODO: LeafGreen and Bulbasaur/Charmander, with AutoStory's starter choice. static void stop_for_starter_rng( SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, const std::string& sound, const std::vector& attempts @@ -267,10 +297,10 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex ); return; } - // Planned before anything is pressed, so a setup problem shows now. - const std::vector rng_attempts = plan.stop == StoryHandoff::STARTER_RNG - ? rng_attempt_lines(RNG_PLAN.plan(env.console, RUN.language())) - : std::vector{}; + if (plan.stop == StoryHandoff::STARTER_RNG){ + // The options' text, before anything is pressed. + RNG_PLAN.check(env.console); + } StoryEnv story{env.console, context, checkpoints[first].supplies, checkpoints[first].supplies, false, RUN.checks()}; story.player_name = player_name; @@ -290,7 +320,11 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex env.update_stats(); throw_if_failed(report); if (plan.stop == StoryHandoff::STARTER_RNG){ - stop_for_starter_rng(env, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN.sound_name(), rng_attempts); + const uint16_t tid = starter_rng_tid(env, context, RNG_PLAN, RUN.language()); + stop_for_starter_rng( + env, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN.sound_name(), + rng_attempt_lines(RNG_PLAN.plan(env.console, RUN.language(), tid)) + ); return; } env.console.log("AutoStory: done through " + plan.rows.back().id + "."); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp index 049c5061ce..66b7c02642 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp @@ -19,7 +19,10 @@ StarterRngPlanOptions::StarterRngPlanOptions() {{SoundSetting::Mono, "mono", "Mono"}, {SoundSetting::Stereo, "stereo", "Stereo"}}, LockMode::LOCK_WHILE_RUNNING, SoundSetting::Mono ) - , TID("TID:
The Trainer ID SID Helper read.", LockMode::LOCK_WHILE_RUNNING, 0) + , TID( + false, "TID:
Empty: read from the Trainer Card at the stop. Set it only if that read fails.", + LockMode::LOCK_WHILE_RUNNING, "", "" + ) , SID( false, "SID:
The SID, once known. Empty: plan for the SIDs SID Helper lists, from the two options below.", @@ -63,19 +66,31 @@ std::string StarterRngPlanOptions::sound_name() const{ return SOUND == SoundSetting::Stereo ? "Stereo" : "Mono"; } -std::vector StarterRngPlanOptions::candidates(Logger& logger) const{ - const std::string sid_text = SID; - if (sid_text.empty()){ - return sid_helper_candidates(TID, SID_HELPER_ADVANCES, SID_HELPER_CANDIDATES); +std::optional StarterRngPlanOptions::tid_override(Logger& logger) const{ + const std::string text = TID; + if (text.empty()){ + return std::nullopt; + } + const std::optional tid = parse_trainer_id(text); + if (!tid){ + throw UserSetupError(logger, "RNG plan: the TID must be a number from 0 to 65535, or empty."); + } + return tid; +} + +std::optional StarterRngPlanOptions::sid(Logger& logger) const{ + const std::string text = SID; + if (text.empty()){ + return std::nullopt; } - const std::optional sid = parse_sid(sid_text); + const std::optional sid = parse_trainer_id(text); if (!sid){ throw UserSetupError(logger, "RNG plan: the SID must be a number from 0 to 65535, or empty."); } - return {{*sid, 1}}; + return sid; } -std::vector StarterRngPlanOptions::plan(Logger& logger, Language language) const{ +RngTargetFilters StarterRngPlanOptions::filters(Logger& logger) const{ const std::optional> natures = parse_natures(NATURES); if (!natures){ throw UserSetupError(logger, "RNG plan: natures must be nature names separated by commas, e.g. \"Modest, Timid\"."); @@ -88,22 +103,43 @@ std::vector StarterRngPlanOptions::plan(Logger& logger, Language lan filters.natures = *natures; filters.min_ivs = *min_ivs; filters.shiny_required = SHINY_REQUIRED; + return filters; +} + +void StarterRngPlanOptions::check(Logger& logger) const{ + tid_override(logger); + sid(logger); + filters(logger); +} + +std::vector StarterRngPlanOptions::plan(Logger& logger, Language language, uint16_t tid) const{ + // SID Helper's candidates are logged even with the SID known, to place + // it among them (rng_log_confirmed()). + const std::vector helper = sid_helper_candidates(tid, SID_HELPER_ADVANCES, SID_HELPER_CANDIDATES); + logger.log(rng_log_sid_helper(tid, SID_HELPER_ADVANCES, helper)); + const std::optional known = sid(logger); + if (known){ + logger.log(rng_log_confirmed(tid, *known, helper)); + } const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ - // AutoStory plays FireRed only. + // TODO: LeafGreen, once AutoStory plays it. .firered = true, .language = language, .sound = SOUND, - .tid = TID, - .candidates = candidates(logger), - .filters = filters, + .tid = tid, + .candidates = known ? std::vector{{*known, 1}} : helper, + .filters = filters(logger), .max_attempts = MAX_ATTEMPTS, }); if (attempts.empty()){ throw UserSetupError(logger, - "RNG plan: no Starter RNG target passes the natures, minimum IVs and shiny options for TID " + std::to_string(TID) + "." + "RNG plan: no Starter RNG target passes the natures, minimum IVs and shiny options for TID " + std::to_string(tid) + "." ); } + for (size_t c = 0; c < attempts.size(); c++){ + logger.log(rng_log_attempt(c, attempts[c])); + } return attempts; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h index d0e6a9e5d0..38f4cc0366 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h @@ -10,6 +10,7 @@ #ifndef PokemonAutomation_PokemonFRLG_AutoStoryRngOptions_H #define PokemonAutomation_PokemonFRLG_AutoStoryRngOptions_H +#include #include #include #include "Common/Cpp/Logging/AbstractLogger.h" @@ -31,16 +32,22 @@ class StarterRngPlanOptions : public GroupOption{ public: StarterRngPlanOptions(); - // The attempts these options ask for, for a FireRed game in "language". - // Throws UserSetupError for an option it can't read, or when no target - // passes. - std::vector plan(Logger& logger, Language language) const; + // Throws UserSetupError for a TID, SID, natures or IVs it can't read, + // before the run presses anything. + void check(Logger& logger) const; + // The TID typed in, or nothing to read it from the Trainer Card. + std::optional tid_override(Logger& logger) const; + + // The attempts these options ask for, for TID "tid" in a FireRed game + // in "language", logged as "RngPlan:" lines. Throws UserSetupError + // when no target passes. + std::vector plan(Logger& logger, Language language, uint16_t tid) const; // The sound setting as Starter RNG's dropdown shows it. std::string sound_name() const; public: EnumDropdownOption SOUND; - SimpleIntegerOption TID; + StringOption TID; StringOption SID; SimpleIntegerOption SID_HELPER_ADVANCES; SimpleIntegerOption SID_HELPER_CANDIDATES; @@ -50,7 +57,8 @@ class StarterRngPlanOptions : public GroupOption{ SimpleIntegerOption MAX_ATTEMPTS; private: - std::vector candidates(Logger& logger) const; + RngTargetFilters filters(Logger& logger) const; + std::optional sid(Logger& logger) const; }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp index 5180e27c01..615299ec59 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp @@ -61,7 +61,7 @@ std::vector split(const std::string& text, char separator){ } -std::optional parse_sid(const std::string& text){ +std::optional parse_trainer_id(const std::string& text){ const std::string digits = trim(text); if (digits.empty() || digits.size() > 5 || !std::all_of(digits.begin(), digits.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ return std::nullopt; @@ -318,6 +318,59 @@ std::vector plan_starter_rng(const StarterRngPlanInputs& inputs){ } +namespace{ + +std::string join_sids(const std::vector& sids){ + std::string text; + for (uint16_t sid : sids){ + text += (text.empty() ? "" : ",") + std::to_string(sid); + } + return text; +} + +} + +std::string rng_log_tid(uint16_t tid, bool from_card){ + return std::format("RngPlan: tid={} source={}", tid, from_card ? "card" : "option"); +} + +std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std::vector& candidates){ + std::vector sids; + for (const SidCandidate& candidate : candidates){ + sids.push_back(candidate.sid); + } + return std::format("RngPlan: sid_helper tid={} target_advances={} candidates={}", tid, target_advances, join_sids(sids)); +} + +std::string rng_log_attempt(size_t index, const RngAttempt& attempt){ + return std::format( + "RngPlan: attempt={} seed={:04X} advances={} sids={}", + index + 1, attempt.target.seed.seed, attempt.target.advances, join_sids(attempt.sids) + ); +} + +std::string rng_log_result(size_t index, RngAttemptOutcome outcome){ + const char* name = "MISSED"; + if (outcome == RngAttemptOutcome::SHINY){ + name = "SHINY"; + }else if (outcome == RngAttemptOutcome::HIT_NOT_SHINY){ + name = "HIT_NOT_SHINY"; + } + return std::format("RngPlan: result attempt={} outcome={}", index + 1, name); +} + +std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates){ + size_t k = 0; + for (size_t c = 0; c < sid_helper_candidates.size(); c++){ + if (sid_helper_candidates[c].sid == sid){ + k = c + 1; + break; + } + } + return std::format("RngPlan: confirmed tid={} sid={} candidate={} of={}", tid, sid, k, sid_helper_candidates.size()); +} + + std::vector rng_attempt_lines(const std::vector& attempts){ std::vector lines; for (size_t c = 0; c < attempts.size(); c++){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h index 2640f9070a..bb338e0d4b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h @@ -37,8 +37,8 @@ namespace PokemonFRLG{ // less, so the outermost weigh 1. std::vector sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count); -// A SID typed as a number from 0 to 65535. Nothing for any other text. -std::optional parse_sid(const std::string& text); +// A TID or SID typed as a number from 0 to 65535. Nothing for any other text. +std::optional parse_trainer_id(const std::string& text); // "HP/Atk/Def/SpA/SpD/Spe", as Ten Lines and the RNG programs list IVs. // Nothing for text that isn't six numbers from 0 to 31. @@ -162,6 +162,26 @@ std::vector plan_starter_rng(const StarterRngPlanInputs& inputs); std::vector rng_attempt_lines(const std::vector& attempts); +// The data to rebuild how likely SID Helper's k-th candidate of N is (the +// 1-2-3-2-1 weights of sid_helper_candidates() are a guess), from the logs +// alone. Every line starts with "RngPlan:"; grep for it: +// RngPlan: tid= source= +// RngPlan: sid_helper tid= target_advances= candidates=,... +// RngPlan: attempt= seed= advances= sids=,... +// RngPlan: result attempt= outcome= +// RngPlan: confirmed tid= sid= candidate= of= +// "candidates" are in SID Helper's order; "candidate=k" counts from 1 in +// it, 0 when the SID wasn't among them. Count each TID's "confirmed" once: +// every later run with the SID set logs it again. While the user runs +// Starter RNG by hand, its own "Target Hit!" and "Missed target." lines +// are the attempts' results. +std::string rng_log_tid(uint16_t tid, bool from_card); +std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std::vector& candidates); +std::string rng_log_attempt(size_t index, const RngAttempt& attempt); +std::string rng_log_result(size_t index, RngAttemptOutcome outcome); +std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates); + + void add_tests_AutoStoryRngPlan(UnitTestDatabase& database); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp index aac50987b0..3b83e60d78 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -120,7 +120,7 @@ UnitTestResult test_sid_helper_candidates(Logger&){ } -UnitTestResult test_parse_sid(Logger&){ +UnitTestResult test_parse_trainer_id(Logger&){ struct Case{ std::string text; std::string expected; @@ -135,7 +135,7 @@ UnitTestResult test_parse_sid(Logger&){ {.text = "12a", .expected = "none"}, }; for (const Case& item : cases){ - const std::optional sid = parse_sid(item.text); + const std::optional sid = parse_trainer_id(item.text); const std::string got = sid ? std::to_string(*sid) : "none"; if (got != item.expected){ return std::format("SID \"{}\": expected {}, got {}.", item.text, item.expected, got); @@ -390,6 +390,33 @@ UnitTestResult test_tracker(Logger&){ } +// The "RngPlan:" lines the candidate odds are rebuilt from. +UnitTestResult test_log_lines(Logger&){ + RngAttempt attempt{make_target(1, 50, {0, 1}), {100, 200}}; + attempt.target.seed.seed = 0x2ef6; + attempt.target.advances = 29364; + struct Case{ + std::string got; + std::string expected; + }; + const Case cases[] = { + {.got = rng_log_tid(43771, true), .expected = "RngPlan: tid=43771 source=card"}, + {.got = rng_log_tid(43771, false), .expected = "RngPlan: tid=43771 source=option"}, + {.got = rng_log_sid_helper(43771, 3001, CANDIDATES), .expected = "RngPlan: sid_helper tid=43771 target_advances=3001 candidates=100,200,300"}, + {.got = rng_log_attempt(0, attempt), .expected = "RngPlan: attempt=1 seed=2EF6 advances=29364 sids=100,200"}, + {.got = rng_log_result(1, RngAttemptOutcome::HIT_NOT_SHINY), .expected = "RngPlan: result attempt=2 outcome=HIT_NOT_SHINY"}, + {.got = rng_log_confirmed(43771, 300, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=300 candidate=3 of=3"}, + {.got = rng_log_confirmed(43771, 7, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=7 candidate=0 of=3"}, + }; + for (const Case& item : cases){ + if (item.got != item.expected){ + return "Expected \"" + item.expected + "\", got \"" + item.got + "\"."; + } + } + return true; +} + + // A plan from the FireRed (English) list: every attempt starts from a seed // Starter RNG finds, takes at least its smallest Advances, and is shiny for // the SIDs it lists, which no earlier attempt listed. @@ -444,13 +471,14 @@ UnitTestResult test_starter_plan(Logger&){ void add_tests_AutoStoryRngPlan(UnitTestDatabase& database){ database.add("SID Helper's candidates and weights", test_sid_helper_candidates); - database.add("SID text", test_parse_sid); + database.add("TID and SID text", test_parse_trainer_id); database.add("IVs text", test_parse_ivs); database.add("natures text", test_parse_natures); database.add("attempt order", test_plan_order); database.add("attempt outcome", test_classify); database.add("step after each outcome", test_tracker); database.add("Starter RNG plan from the FireRed list", test_starter_plan); + database.add("RngPlan log lines", test_log_lines); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.cpp new file mode 100644 index 0000000000..54af553f9b --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.cpp @@ -0,0 +1,105 @@ +/* Trainer Card + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" +#include "CommonFramework/VideoPipeline/VideoFeed.h" +#include "CommonFramework/VideoPipeline/VideoOverlay.h" +#include "CommonTools/Async/InferenceRoutines.h" +#include "NintendoSwitch/Commands/NintendoSwitch_Commands_PushButtons.h" +#include "VideoGames/PokemonFRLG/Inference/Menus/PokemonFRLG_TrainerCardDetector.h" +#include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.h" +#include "PokemonFRLG_StartMenuNavigation.h" +#include "PokemonFRLG_TrainerCard.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +void navigate_to_trainer_card(ConsoleHandle& console, ProControllerContext& context){ + int errors = 0; + while (true){ + if (errors >= 5){ + OperationFailedExceptionWithScreenshot::fire( + ErrorReportMode::SEND_ERROR_REPORT, "navigate_to_trainer_card(): failed 5 times to navigate to the Trainer Card", console + ); + } + + open_start_menu(console, context); + + // the positions are different before recieving a Pokemon and Pokedex + bool cursor = move_cursor_to_position( + console, context, SelectionArrowPositionStartMenu(1) + ); + if (!cursor){ + errors++; + pbf_mash_button(context, BUTTON_B, 2000ms); + continue; + } + + TrainerCardWatcher trainer_card(COLOR_RED); + context.wait_for_all_requests(); + int ret = run_until( + console, context, + [](ProControllerContext& context){ + pbf_press_button(context, BUTTON_A, 200ms, 2300ms); + pbf_press_button(context, BUTTON_A, 200ms, 2300ms); + }, + { trainer_card } + ); + + if (ret < 0){ + console.log("navigate_to_trainer_card(): failed to detect Trainer Card"); + errors++; + pbf_mash_button(context, BUTTON_B, 2000ms); + continue; + } + + break; + } +} + +namespace{ + +template +auto read_on_card(ConsoleHandle& console, ProControllerContext& context, Read read){ + TrainerIdReader reader; + VideoOverlaySet overlays(console.overlay()); + reader.make_overlays(overlays); + + VideoSnapshot screen = console.video().snapshot(); + console.log("Trainer Card detected."); + console.log("Reading TID..."); + auto tid = read(reader, screen); + + context.wait_for_all_requests(); + + return tid; +} + +} + +uint16_t read_trainer_card_tid(ConsoleHandle& console, ProControllerContext& context, Language language){ + uint16_t tid = read_on_card(console, context, [&](TrainerIdReader& reader, const VideoSnapshot& screen){ + return reader.read_tid(console.logger(), language, screen); + }); + console.log("TID: " + std::to_string(tid)); + return tid; +} + +std::optional try_read_trainer_card_tid(ConsoleHandle& console, ProControllerContext& context, Language language){ + std::optional tid = read_on_card(console, context, [&](TrainerIdReader& reader, const VideoSnapshot& screen){ + return reader.try_read_tid(console.logger(), language, screen); + }); + console.log(tid ? "TID: " + std::to_string(*tid) : std::string("TID: not read")); + return tid; +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h new file mode 100644 index 0000000000..c52c212cba --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h @@ -0,0 +1,41 @@ +/* Trainer Card + * + * From: https://github.com/PokemonAutomation/ + * + * Opening the Trainer Card from the overworld and reading the TID on it, + * as SID Helper does. Shared with AutoStory, which reads the TID for its + * Starter RNG plan. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_TrainerCard_H +#define PokemonAutomation_PokemonFRLG_TrainerCard_H + +#include +#include +#include "CommonFramework/Language.h" +#include "NintendoSwitch/Controllers/Procon/NintendoSwitch_ProController.h" +#include "NintendoSwitch/NintendoSwitch_ConsoleHandle.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// From the overworld, START then the player's entry, until the card shows. +// The entry is second only before the first Pokemon and the Pokedex. +// Throws OperationFailedException after 5 failed tries. +void navigate_to_trainer_card(ConsoleHandle& console, ProControllerContext& context); + +// The TID on the Trainer Card on screen. An unreadable TID comes back as +// TrainerIdReader::read_tid() returns it. +uint16_t read_trainer_card_tid(ConsoleHandle& console, ProControllerContext& context, Language language); +// Nothing when the TID can't be read. +std::optional try_read_trainer_card_tid(ConsoleHandle& console, ProControllerContext& context, Language language); + + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp index 0c3ac9bd67..cb79d9241f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SidHelper.cpp @@ -18,6 +18,7 @@ #include "VideoGames/PokemonFRLG/Inference/PokemonFRLG_TrainerIdReader.h" #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h" +#include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h" #include "PokemonFRLG_RngCalibration.h" #include "PokemonFRLG_SidHelper.h" @@ -168,65 +169,6 @@ void finish_intro_animations(SingleSwitchProgramEnvironment& env, ProControllerC context.wait_for_all_requests(); } -void navigate_to_trainer_card(SingleSwitchProgramEnvironment& env, ProControllerContext& context){ - int errors = 0; - while (true){ - if (errors >= 5){ - OperationFailedExceptionWithScreenshot::fire( - ErrorReportMode::SEND_ERROR_REPORT, "SidHelper(): failed 5 times to navigate to the Trainer Card", env.console - ); - } - - open_start_menu(env.console, context); - - // the positions are different before recieving a Pokemon and Pokedex - bool cursor = move_cursor_to_position( - env.console, context, SelectionArrowPositionStartMenu(1) - ); - if (!cursor){ - errors++; - pbf_mash_button(context, BUTTON_B, 2000ms); - continue; - } - - TrainerCardWatcher trainer_card(COLOR_RED); - context.wait_for_all_requests(); - int ret = run_until( - env.console, context, - [](ProControllerContext& context){ - pbf_press_button(context, BUTTON_A, 200ms, 2300ms); - pbf_press_button(context, BUTTON_A, 200ms, 2300ms); - }, - { trainer_card } - ); - - if (ret < 0){ - env.log("SidHelper(): failed to detect Trainer Card"); - errors++; - pbf_mash_button(context, BUTTON_B, 2000ms); - continue; - } - - break; - } -} - -uint16_t read_tid(SingleSwitchProgramEnvironment& env, ProControllerContext& context, Language lang){ - TrainerIdReader reader; - VideoOverlaySet overlays(env.console.overlay()); - reader.make_overlays(overlays); - - VideoSnapshot screen = env.console.video().snapshot(); - env.log("Trainer Card detected."); - env.log("Reading TID..."); - uint16_t tid = reader.read_tid(env.logger(), lang, screen); - env.log("TID: " + std::to_string(tid)); - - context.wait_for_all_requests(); - - return tid; -} - std::vector> get_sid_messages( SingleSwitchProgramEnvironment& env, ProControllerContext& context, @@ -310,9 +252,9 @@ void SidHelper::program(SingleSwitchProgramEnvironment& env, ProControllerContex set_sid_from_name_screen(env, context, SID_DELAY, extra_press_at_end); finish_intro_animations(env, context); - navigate_to_trainer_card(env, context); + navigate_to_trainer_card(env.console, context); - uint16_t tid = read_tid(env, context, lang); + uint16_t tid = read_trainer_card_tid(env.console, context, lang); std::vector> sid_messages = get_sid_messages( env, context, tid, TARGET_ADVANCES, NUM_CANDIDATES diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 860cc5da9f..d249925455 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1871,6 +1871,8 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_SafariOptimalAction.h Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.cpp Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_StartMenuNavigation.h + Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.cpp + Source/VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_EggRng.cpp From ee189ed150026353e4a54c49520d6f9f2e696cf7 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 15:39:00 -0800 Subject: [PATCH 04/13] FRLG AutoStory: read the TID and plan inside the Starter RNG stop Keeps program() under the code-health limit. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../Programs/AutoStory/PokemonFRLG_AutoStory.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 44e3d74cb2..86013cc0b3 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -265,12 +265,14 @@ static uint16_t starter_rng_tid( // the attempts are the user's to run until that loop can be called from here. // TODO: LeafGreen and Bulbasaur/Charmander, with AutoStory's starter choice. static void stop_for_starter_rng( - SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, - const std::string& sound, const std::vector& attempts + SingleSwitchProgramEnvironment& env, ProControllerContext& context, EventNotificationOption& notification, + const StarterRngPlanOptions& options, Language language ){ + const uint16_t tid = starter_rng_tid(env, context, options, language); + const std::vector attempts = rng_attempt_lines(options.plan(env.console, language, tid)); stop_for_handoff(env, notification, "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. " - "Run Starter RNG (Squirtle, FireRed, Sound " + sound + ") with attempt 1's Target Seed and Advances. " + "Run Starter RNG (Squirtle, FireRed, Sound " + options.sound_name() + ") with attempt 1's Target Seed and Advances. " "If it hits the target and it isn't shiny, load this save and run the next attempt. If it is shiny, " "its SID is the one listed (or one of them): enter it as the RNG plan's SID. " "Then save right after the rival's battle, without moving, and start AutoStory at 00.05.", @@ -320,11 +322,7 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex env.update_stats(); throw_if_failed(report); if (plan.stop == StoryHandoff::STARTER_RNG){ - const uint16_t tid = starter_rng_tid(env, context, RNG_PLAN, RUN.language()); - stop_for_starter_rng( - env, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN.sound_name(), - rng_attempt_lines(RNG_PLAN.plan(env.console, RUN.language(), tid)) - ); + stop_for_starter_rng(env, context, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN, RUN.language()); return; } env.console.log("AutoStory: done through " + plan.rows.back().id + "."); From 9355ad622608db25089d439a146a93c4fd166434 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 15:41:19 -0800 Subject: [PATCH 05/13] FRLG AutoStory: plan Starter RNG before 00.04 saves under the ball The TID read and the plan run just before 00.04's save, so an unreadable TID or a plan with no target ends the run with 00.03's save kept, and 00.04 can be run again once the RNG plan is fixed. A typed-in TID still skips the card. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../AutoStory/PokemonFRLG_AutoStory.cpp | 71 ++++++++++++++----- 1 file changed, 55 insertions(+), 16 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 86013cc0b3..a005262e15 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -5,6 +5,7 @@ */ #include +#include #include "Common/Cpp/Exceptions.h" #include "CommonFramework/Exceptions/OperationFailedException.h" #include "CommonFramework/Exceptions/OperationFailedExceptionWithScreenshot.h" @@ -234,9 +235,22 @@ static void stop_for_handoff( send_program_finished_notification(env, notification, full); } +// SID Helper's set_sid_from_name_screen(), still file-static in +// PokemonFRLG_SidHelper.cpp, could run here instead of stopping. +static void stop_for_sid_helper( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + EventNotificationOption& notification, const std::string& player_name +){ + play_new_game_to_player_name(env.console, context, player_name); + stop_for_handoff(env, notification, + "Stopped for SID Helper: the player's name is typed and OK is not pressed. Run SID Helper now, " + "enter its Target Advances and # Candidate SIDs in the RNG plan, then start AutoStory at 00.01b." + ); +} + // The TID for the Starter RNG plan: the RNG plan's, or read off the // Trainer Card (START, the player's entry), which is then closed again. -// Before the first Pokemon, as at the stop, the player's entry is second. +// Before the first Pokemon, as under the ball, the player's entry is second. static uint16_t starter_rng_tid( SingleSwitchProgramEnvironment& env, ProControllerContext& context, const StarterRngPlanOptions& options, Language language @@ -251,8 +265,8 @@ static uint16_t starter_rng_tid( if (!tid){ OperationFailedExceptionWithScreenshot::fire( ErrorReportMode::NO_ERROR_REPORT, - "Couldn't read the TID on the Trainer Card, so there is no Starter RNG plan. The game is saved facing " - "Squirtle's ball, where Starter RNG starts. Type the TID into the RNG plan's TID option for the next run.", + "Couldn't read the TID on the Trainer Card, so there is no Starter RNG plan. Nothing was saved: " + "type the TID into the RNG plan's TID option and start AutoStory at 00.04 again.", env.console ); } @@ -261,18 +275,43 @@ static uint16_t starter_rng_tid( return *tid; } +// The Starter RNG attempts, as lines for the stop. Run under the ball before +// 00.04 saves there, so a failure ends the run with 00.03's save kept. +static std::vector starter_rng_attempts( + SingleSwitchProgramEnvironment& env, ProControllerContext& context, + const StarterRngPlanOptions& options, Language language +){ + const uint16_t tid = starter_rng_tid(env, context, options, language); + try{ + return rng_attempt_lines(options.plan(env.console, language, tid)); + }catch (UserSetupError& e){ + throw UserSetupError(env.console, e.message() + " Nothing was saved: fix the RNG plan and start AutoStory at 00.04 again."); + } +} + +// Has the last checkpoint of `segment` call `before` just before it saves. +static void before_last_save(Pokemon::Segment& segment, std::function before){ + Pokemon::CheckpointSpec& spec = segment.checkpoints.back(); + spec.build = [build = std::move(spec.build), before]() mutable{ + Pokemon::Checkpoint checkpoint = build(); + checkpoint.save = [save = std::move(checkpoint.save), before](Pokemon::RunState& state) mutable{ + before(); + return save(state); + }; + return checkpoint; + }; +} + // Starter RNG runs its own reset and calibration loop in its program(), so // the attempts are the user's to run until that loop can be called from here. // TODO: LeafGreen and Bulbasaur/Charmander, with AutoStory's starter choice. static void stop_for_starter_rng( - SingleSwitchProgramEnvironment& env, ProControllerContext& context, EventNotificationOption& notification, - const StarterRngPlanOptions& options, Language language + SingleSwitchProgramEnvironment& env, EventNotificationOption& notification, + const std::string& sound, const std::vector& attempts ){ - const uint16_t tid = starter_rng_tid(env, context, options, language); - const std::vector attempts = rng_attempt_lines(options.plan(env.console, language, tid)); stop_for_handoff(env, notification, "Stopped for Starter RNG: saved south of the table, facing Squirtle's ball. " - "Run Starter RNG (Squirtle, FireRed, Sound " + options.sound_name() + ") with attempt 1's Target Seed and Advances. " + "Run Starter RNG (Squirtle, FireRed, Sound " + sound + ") with attempt 1's Target Seed and Advances. " "If it hits the target and it isn't shiny, load this save and run the next attempt. If it is shiny, " "its SID is the one listed (or one of them): enter it as the RNG plan's SID. " "Then save right after the rival's battle, without moving, and start AutoStory at 00.05.", @@ -290,13 +329,7 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex checkpoints, first, last, StoryHandoffs{.sid_helper = STOP_FOR_SID_HELPER, .starter_rng = STOP_FOR_STARTER_RNG} ); if (plan.stop == StoryHandoff::SID_HELPER){ - // SID Helper's steps (set_sid_from_name_screen() to read_tid(), - // file-static in PokemonFRLG_SidHelper.cpp) could run here instead. - play_new_game_to_player_name(env.console, context, player_name); - stop_for_handoff(env, NOTIFICATION_PROGRAM_FINISH, - "Stopped for SID Helper: the player's name is typed and OK is not pressed. Run SID Helper now, " - "enter its TID, Target Advances and # Candidate SIDs in the RNG plan, then start AutoStory at 00.01b." - ); + stop_for_sid_helper(env, context, NOTIFICATION_PROGRAM_FINISH, player_name); return; } if (plan.stop == StoryHandoff::STARTER_RNG){ @@ -312,6 +345,12 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex executor.set_checks(RUN.checks()); executor.set_language(RUN.language()); Pokemon::Segment segment = make_story_segment(plan.rows, executor); + std::vector rng_attempts; + if (plan.stop == StoryHandoff::STARTER_RNG){ + before_last_save(segment, [&]{ + rng_attempts = starter_rng_attempts(env, context, RNG_PLAN, RUN.language()); + }); + } Pokemon::SegmentHooks hooks = story_hooks(env, story); Pokemon::RunState state; @@ -322,7 +361,7 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex env.update_stats(); throw_if_failed(report); if (plan.stop == StoryHandoff::STARTER_RNG){ - stop_for_starter_rng(env, context, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN, RUN.language()); + stop_for_starter_rng(env, NOTIFICATION_PROGRAM_FINISH, RNG_PLAN.sound_name(), rng_attempts); return; } env.console.log("AutoStory: done through " + plan.rows.back().id + "."); From 73573d0f9e7640d0ab57545f5bc07cd132d48a0b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 16:50:41 -0800 Subject: [PATCH 06/13] FRLG AutoStory: RNG plan result lines with the try, the Pokemon and the next step rng_log_result() takes an RngResultLog: the try, Starter RNG's own outcome, the nature, IVs and PID when known, and what comes next (rng_plan_next()). Outcomes are lower case. RngPlanTracker gains attempt_count(). The header lists every RngPlan: line in the order a run logs them, including the loop's state line. Moved here from #184, which uses it, so that each PR owns its files. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRngPlan.cpp | 61 ++++++++++++++++--- .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 40 +++++++++++- .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 18 +++++- 3 files changed, 108 insertions(+), 11 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp index 615299ec59..6b1f6fc51f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp @@ -349,14 +349,61 @@ std::string rng_log_attempt(size_t index, const RngAttempt& attempt){ ); } -std::string rng_log_result(size_t index, RngAttemptOutcome outcome){ - const char* name = "MISSED"; - if (outcome == RngAttemptOutcome::SHINY){ - name = "SHINY"; - }else if (outcome == RngAttemptOutcome::HIT_NOT_SHINY){ - name = "HIT_NOT_SHINY"; +RngPlanNext rng_plan_next(RngPlanStep step, size_t index, size_t next_index){ + switch (step){ + case RngPlanStep::RUN_ATTEMPT: + return next_index == index ? RngPlanNext::RETRY : RngPlanNext::NEXT_ATTEMPT; + case RngPlanStep::GOT_SHINY: + return RngPlanNext::CONFIRM; + case RngPlanStep::GOT_TARGET: + case RngPlanStep::OUT_OF_ATTEMPTS: + case RngPlanStep::OUT_OF_RETRIES: + return RngPlanNext::STOP; } - return std::format("RngPlan: result attempt={} outcome={}", index + 1, name); + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngPlanStep."); +} + +namespace{ + +const char* outcome_name(RngAttemptOutcome outcome){ + switch (outcome){ + case RngAttemptOutcome::SHINY: + return "shiny"; + case RngAttemptOutcome::HIT_NOT_SHINY: + return "hit_not_shiny"; + case RngAttemptOutcome::MISSED: + return "missed"; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngAttemptOutcome."); +} + +const char* next_name(RngPlanNext next){ + switch (next){ + case RngPlanNext::RETRY: + return "retry"; + case RngPlanNext::NEXT_ATTEMPT: + return "next_attempt"; + case RngPlanNext::CONFIRM: + return "confirm"; + case RngPlanNext::STOP: + return "stop"; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngPlanNext."); +} + +} + +std::string rng_log_result(const RngResultLog& result){ + const std::optional& pokemon = result.pokemon; + return std::format( + "RngPlan: result attempt={} try={} outcome={} starter_rng={} nature={} ivs={} pid={} next={}", + result.index + 1, result.try_number, outcome_name(result.outcome), + result.detail.empty() ? "-" : result.detail, + pokemon ? nature_to_string(pokemon->nature) : "-", + pokemon ? ivs_to_string(pokemon->ivs) : "-", + pokemon ? std::format("{:08X}", pokemon->pid) : "-", + next_name(result.next) + ); } std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates){ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h index bb338e0d4b..7487320295 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h @@ -116,6 +116,7 @@ class RngPlanTracker{ RngPlanStep step() const{ return m_step; } size_t attempt_index() const{ return m_attempt; } + size_t attempt_count() const{ return m_attempts.size(); } const RngAttempt& current_attempt() const{ return m_attempts[m_attempt]; } // The SID candidates not ruled out, in the order given. const std::vector& remaining_sids() const{ return m_remaining; } @@ -164,12 +165,22 @@ std::vector rng_attempt_lines(const std::vector& attemp // The data to rebuild how likely SID Helper's k-th candidate of N is (the // 1-2-3-2-1 weights of sid_helper_candidates() are a guess), from the logs -// alone. Every line starts with "RngPlan:"; grep for it: +// alone, and to follow AutoStory's Starter RNG loop. Every line starts with +// "RngPlan:"; grep for it. In the order a run logs them: // RngPlan: tid= source= // RngPlan: sid_helper tid= target_advances= candidates=,... // RngPlan: attempt= seed= advances= sids=,... -// RngPlan: result attempt= outcome= +// then, with Run Starter RNG in AutoStory, for each try of an attempt +// (rng_log_state() in PokemonFRLG_AutoStoryRngLoop.h): +// RngPlan: state attempt=/ try=/ reset=/ seed= +// advances= sids=,... ruled_out=,... attempt_time=/ +// total_time=/ calibration=// +// (once at the try's start, then after every reset) +// RngPlan: result attempt= try= outcome= +// starter_rng= nature= ivs= +// pid= next= // RngPlan: confirmed tid= sid= candidate= of= +// Lists that are empty print "-". // "candidates" are in SID Helper's order; "candidate=k" counts from 1 in // it, 0 when the SID wasn't among them. Count each TID's "confirmed" once: // every later run with the SID set logs it again. While the user runs @@ -178,7 +189,30 @@ std::vector rng_attempt_lines(const std::vector& attemp std::string rng_log_tid(uint16_t tid, bool from_card); std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std::vector& candidates); std::string rng_log_attempt(size_t index, const RngAttempt& attempt); -std::string rng_log_result(size_t index, RngAttemptOutcome outcome); +// What the loop does after an attempt's result. +enum class RngPlanNext{ + RETRY, + NEXT_ATTEMPT, + // Shiny: go on with the story. + CONFIRM, + STOP, +}; +// The step after `index` came out as the tracker's `step` and its attempt +// is now `next_index`. +RngPlanNext rng_plan_next(RngPlanStep step, size_t index, size_t next_index); + +struct RngResultLog{ + size_t index = 0; + // From 1. + size_t try_number = 1; + RngAttemptOutcome outcome = RngAttemptOutcome::MISSED; + // How the RNG program put it ("out_of_resets"); empty for none. + std::string detail; + // The Pokemon hit, when known. + std::optional pokemon; + RngPlanNext next = RngPlanNext::STOP; +}; +std::string rng_log_result(const RngResultLog& result); std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp index 3b83e60d78..ad1870f95c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -404,7 +404,23 @@ UnitTestResult test_log_lines(Logger&){ {.got = rng_log_tid(43771, false), .expected = "RngPlan: tid=43771 source=option"}, {.got = rng_log_sid_helper(43771, 3001, CANDIDATES), .expected = "RngPlan: sid_helper tid=43771 target_advances=3001 candidates=100,200,300"}, {.got = rng_log_attempt(0, attempt), .expected = "RngPlan: attempt=1 seed=2EF6 advances=29364 sids=100,200"}, - {.got = rng_log_result(1, RngAttemptOutcome::HIT_NOT_SHINY), .expected = "RngPlan: result attempt=2 outcome=HIT_NOT_SHINY"}, + { + .got = rng_log_result(RngResultLog{ + .index = 1, .try_number = 2, .outcome = RngAttemptOutcome::HIT_NOT_SHINY, .detail = "target_hit", + .pokemon = attempt.target.pokemon, .next = RngPlanNext::NEXT_ATTEMPT, + }), + .expected = std::format( + "RngPlan: result attempt=2 try=2 outcome=hit_not_shiny starter_rng=target_hit nature={} ivs={} pid={:08X} next=next_attempt", + nature_to_string(attempt.target.pokemon.nature), ivs_to_string(attempt.target.pokemon.ivs), attempt.target.pokemon.pid + ), + }, + { + .got = rng_log_result(RngResultLog{ + .index = 0, .try_number = 1, .outcome = RngAttemptOutcome::MISSED, .detail = "out_of_resets", + .pokemon = std::nullopt, .next = RngPlanNext::RETRY, + }), + .expected = "RngPlan: result attempt=1 try=1 outcome=missed starter_rng=out_of_resets nature=- ivs=- pid=- next=retry", + }, {.got = rng_log_confirmed(43771, 300, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=300 candidate=3 of=3"}, {.got = rng_log_confirmed(43771, 7, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=7 candidate=0 of=3"}, }; From df47081adc3903b8d8481af866c7e18196881bf0 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 16:53:52 -0800 Subject: [PATCH 07/13] FRLG AutoStory: only a shiny on the target narrows the SIDs; misses move on A shiny that isn't the planned target (another advance) says nothing about the SID, but RngPlanTracker narrowed the SIDs to the target's for any shiny, which could confirm a wrong SID. A new outcome, SHINY_OFF_TARGET, ends the plan as GOT_SHINY_OFF_TARGET with the SIDs as they were and none confirmed; classify_attempt() returns it for a shiny without the target's nature and IVs. The result line names it shiny_off_target, with next=stop. An attempt missed more often than the retries allow now moves on to the next attempt still shiny for a SID left, since a miss rules nothing out. The plan stops only when there is none: MISSED_EVERY_ATTEMPT_LEFT replaces OUT_OF_RETRIES. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRngPlan.cpp | 47 ++++++++------ .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 29 ++++++--- .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 64 +++++++++++++++++-- 3 files changed, 109 insertions(+), 31 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp index 6b1f6fc51f..3c0269e714 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp @@ -213,16 +213,15 @@ std::vector plan_rng_attempts( RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResult& result){ - if (result.shiny){ - return RngAttemptOutcome::SHINY; - } const AdvIVs& want = target.pokemon.ivs; const AdvIVs& got = result.ivs; const bool same_ivs = want.hp == got.hp && want.attack == got.attack && want.defense == got.defense && want.spatk == got.spatk && want.spdef == got.spdef && want.speed == got.speed; - return result.nature == target.pokemon.nature && same_ivs - ? RngAttemptOutcome::HIT_NOT_SHINY - : RngAttemptOutcome::MISSED; + const bool hit = result.nature == target.pokemon.nature && same_ivs; + if (result.shiny){ + return hit ? RngAttemptOutcome::SHINY : RngAttemptOutcome::SHINY_OFF_TARGET; + } + return hit ? RngAttemptOutcome::HIT_NOT_SHINY : RngAttemptOutcome::MISSED; } @@ -253,6 +252,18 @@ bool RngPlanTracker::is_ruled_out(uint16_t sid) const{ return std::find(m_remaining.begin(), m_remaining.end(), sid) == m_remaining.end(); } +bool RngPlanTracker::move_to_next_attempt(){ + for (size_t next = m_attempt + 1; next < m_attempts.size(); next++){ + const std::vector& next_sids = m_attempts[next].sids; + if (!m_shiny_required || !std::all_of(next_sids.begin(), next_sids.end(), [&](uint16_t sid){ return is_ruled_out(sid); })){ + m_attempt = next; + m_retries = 0; + return true; + } + } + return false; +} + RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ if (m_step != RngPlanStep::RUN_ATTEMPT){ return m_step; @@ -272,6 +283,9 @@ RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ m_step = RngPlanStep::GOT_SHINY; return m_step; } + case RngAttemptOutcome::SHINY_OFF_TARGET: + m_step = RngPlanStep::GOT_SHINY_OFF_TARGET; + return m_step; case RngAttemptOutcome::HIT_NOT_SHINY: if (!m_shiny_required){ m_step = RngPlanStep::GOT_TARGET; @@ -283,21 +297,15 @@ RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ }), m_remaining.end() ); - m_retries = 0; - // The next attempt still shiny for a candidate left. - for (size_t next = m_attempt + 1; next < m_attempts.size(); next++){ - const std::vector& next_sids = m_attempts[next].sids; - if (!std::all_of(next_sids.begin(), next_sids.end(), [&](uint16_t sid){ return is_ruled_out(sid); })){ - m_attempt = next; - return m_step; - } + if (!move_to_next_attempt()){ + m_step = RngPlanStep::OUT_OF_ATTEMPTS; } - m_step = RngPlanStep::OUT_OF_ATTEMPTS; return m_step; case RngAttemptOutcome::MISSED: + // A miss rules nothing out. Too many: try the next attempt instead. m_retries++; - if (m_retries > m_max_retries){ - m_step = RngPlanStep::OUT_OF_RETRIES; + if (m_retries > m_max_retries && !move_to_next_attempt()){ + m_step = RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT; } return m_step; } @@ -355,9 +363,10 @@ RngPlanNext rng_plan_next(RngPlanStep step, size_t index, size_t next_index){ return next_index == index ? RngPlanNext::RETRY : RngPlanNext::NEXT_ATTEMPT; case RngPlanStep::GOT_SHINY: return RngPlanNext::CONFIRM; + case RngPlanStep::GOT_SHINY_OFF_TARGET: case RngPlanStep::GOT_TARGET: case RngPlanStep::OUT_OF_ATTEMPTS: - case RngPlanStep::OUT_OF_RETRIES: + case RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT: return RngPlanNext::STOP; } throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngPlanStep."); @@ -369,6 +378,8 @@ const char* outcome_name(RngAttemptOutcome outcome){ switch (outcome){ case RngAttemptOutcome::SHINY: return "shiny"; + case RngAttemptOutcome::SHINY_OFF_TARGET: + return "shiny_off_target"; case RngAttemptOutcome::HIT_NOT_SHINY: return "hit_not_shiny"; case RngAttemptOutcome::MISSED: diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h index 7487320295..fd6ea5bdff 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h @@ -5,11 +5,15 @@ * Which Starter RNG targets AutoStory asks for, in order, when the SID is * only narrowed down to SID Helper's candidates, and what to do after each * one. The loop is the one PokemonFRLG_RngTargetSearch.h describes: - * - Shiny: the SID is one of those the target is shiny for. With one left, - * it is confirmed and later targets can be searched with it alone. + * - Shiny, and the target was hit: the SID is one of those the target is + * shiny for. With one left, it is confirmed and later targets can be + * searched with it alone. + * - Shiny, but not the target (another advance): it says nothing about + * the SID. The plan ends with the shiny. * - Nature and IVs match, but not shiny: the target was hit, so the SIDs it * is shiny for are wrong. Go on to the next attempt. - * - Nature or IVs differ: the target was missed. Run it again. + * - Nature or IVs differ: the target was missed. Run it again, up to the + * retries, then go on to the next attempt. * */ @@ -76,7 +80,10 @@ std::vector plan_rng_attempts( // What came out of one attempt. enum class RngAttemptOutcome{ + // Shiny, with the target's nature and IVs. SHINY, + // Shiny, but not the target, or not known to be. + SHINY_OFF_TARGET, // The target's nature and IVs, not shiny. HIT_NOT_SHINY, // Another nature or other IVs: not the target. @@ -94,14 +101,17 @@ RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResu enum class RngPlanStep{ // Run current_attempt(): the first, the next, or the same again after a miss. RUN_ATTEMPT, - // Shiny. remaining_sids() are the SIDs it can be. + // Shiny, the target hit. remaining_sids() are the SIDs it can be. GOT_SHINY, + // Shiny, not the target: the SIDs are as they were, none confirmed. + GOT_SHINY_OFF_TARGET, // Shiny not required, and the target was hit. GOT_TARGET, // Every attempt was hit and none was shiny. OUT_OF_ATTEMPTS, - // One attempt was missed more often than the retries allow. - OUT_OF_RETRIES, + // The last attempt still shiny for a SID left was missed more often + // than the retries allow. (Missing an earlier one moves on to the next.) + MISSED_EVERY_ATTEMPT_LEFT, }; // Follows the plan through the attempts' outcomes. @@ -129,6 +139,9 @@ class RngPlanTracker{ private: bool is_ruled_out(uint16_t sid) const; + // Moves to the next attempt still shiny for a SID left (with shiny + // required; else any next one). False if there is none. + bool move_to_next_attempt(); std::vector m_attempts; bool m_shiny_required; @@ -176,7 +189,7 @@ std::vector rng_attempt_lines(const std::vector& attemp // advances= sids=,... ruled_out=,... attempt_time=/ // total_time=/ calibration=// // (once at the try's start, then after every reset) -// RngPlan: result attempt= try= outcome= +// RngPlan: result attempt= try= outcome= // starter_rng= nature= ivs= // pid= next= // RngPlan: confirmed tid= sid= candidate= of= @@ -193,7 +206,7 @@ std::string rng_log_attempt(size_t index, const RngAttempt& attempt); enum class RngPlanNext{ RETRY, NEXT_ATTEMPT, - // Shiny: go on with the story. + // Shiny and the target: go on with the story. CONFIRM, STOP, }; diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp index ad1870f95c..3366a0d964 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -273,7 +273,8 @@ UnitTestResult test_classify(Logger&){ RngAttemptOutcome expected; }; const Case cases[] = { - {.name = "shiny", .result = {.shiny = true, .nature = other_nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::SHINY}, + {.name = "shiny, the target", .result = {.shiny = true, .nature = hit.pokemon.nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::SHINY}, + {.name = "shiny, not the target", .result = {.shiny = true, .nature = other_nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::SHINY_OFF_TARGET}, {.name = "same nature and IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::HIT_NOT_SHINY}, {.name = "other IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::MISSED}, {.name = "other nature", .result = {.shiny = false, .nature = other_nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::MISSED}, @@ -291,9 +292,10 @@ const char* step_name(RngPlanStep step){ switch (step){ case RngPlanStep::RUN_ATTEMPT: return "RUN_ATTEMPT"; case RngPlanStep::GOT_SHINY: return "GOT_SHINY"; + case RngPlanStep::GOT_SHINY_OFF_TARGET: return "GOT_SHINY_OFF_TARGET"; case RngPlanStep::GOT_TARGET: return "GOT_TARGET"; case RngPlanStep::OUT_OF_ATTEMPTS: return "OUT_OF_ATTEMPTS"; - case RngPlanStep::OUT_OF_RETRIES: return "OUT_OF_RETRIES"; + case RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT: return "MISSED_EVERY_ATTEMPT_LEFT"; } return "?"; } @@ -330,12 +332,44 @@ std::vector tracker_cases(){ .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT}, .attempts = {0, 0, 1, 1}, .remaining = {300}, .confirmed = std::nullopt, }, + }; +} +// Misses: retried, then the next attempt, mixed with hits and shinies. +std::vector tracker_retry_cases(){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + return { { - .name = "three misses in a row with two retries", + .name = "retries used up, then the next attempt", .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, SHINY}, - .steps = {RUN_ATTEMPT, RUN_ATTEMPT, OUT_OF_RETRIES, OUT_OF_RETRIES}, .attempts = {0, 0, 0, 0}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 0, 1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "every attempt left missed too often", + .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, MISSED, MISSED, MISSED}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, MISSED_EVERY_ATTEMPT_LEFT}, + .attempts = {0, 0, 1, 1, 1, 1}, .remaining = {100, 200, 300}, .confirmed = std::nullopt, }, + { + .name = "shiny off the target confirms nothing", + .shiny_required = true, .outcomes = {HIT_NOT_SHINY, SHINY_OFF_TARGET, SHINY}, + .steps = {RUN_ATTEMPT, GOT_SHINY_OFF_TARGET, GOT_SHINY_OFF_TARGET}, .attempts = {1, 1, 1}, + .remaining = {300}, .confirmed = std::nullopt, + }, + { + .name = "miss, hit, misses, then shiny", + .shiny_required = true, .outcomes = {MISSED, HIT_NOT_SHINY, MISSED, MISSED, SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 1, 1, 1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "misses move on, then a hit ends the plan", + .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, MISSED, HIT_NOT_SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, OUT_OF_ATTEMPTS}, .attempts = {0, 0, 1, 1, 1}, + .remaining = {100, 200}, .confirmed = std::nullopt, + }, { .name = "every attempt hit, none shiny", .shiny_required = true, .outcomes = {HIT_NOT_SHINY, HIT_NOT_SHINY}, @@ -376,7 +410,11 @@ UnitTestResult check_tracker_case(const TrackerCase& item){ } UnitTestResult test_tracker(Logger&){ - for (const TrackerCase& item : tracker_cases()){ + std::vector cases = tracker_cases(); + for (TrackerCase& item : tracker_retry_cases()){ + cases.push_back(std::move(item)); + } + for (const TrackerCase& item : cases){ const UnitTestResult result = check_tracker_case(item); if (result.result != UnitTestResult::PASSED){ return result; @@ -386,6 +424,15 @@ UnitTestResult test_tracker(Logger&){ if (empty.step() != RngPlanStep::OUT_OF_ATTEMPTS){ return std::string("An empty plan: expected OUT_OF_ATTEMPTS, got ") + step_name(empty.step()) + "."; } + // A shiny hit for a SID that is no candidate: none left, none confirmed. + RngPlanTracker outside({RngAttempt{make_target(1, 50, {}), {999}}}, CANDIDATES, true, 2); + outside.record(RngAttemptOutcome::SHINY); + if (outside.step() != RngPlanStep::GOT_SHINY || !outside.remaining_sids().empty() || outside.confirmed_sid()){ + return std::format( + "Shiny for no candidate: expected GOT_SHINY with no SID left, got {} with SIDs {}.", + step_name(outside.step()), join(outside.remaining_sids()) + ); + } return true; } @@ -421,6 +468,13 @@ UnitTestResult test_log_lines(Logger&){ }), .expected = "RngPlan: result attempt=1 try=1 outcome=missed starter_rng=out_of_resets nature=- ivs=- pid=- next=retry", }, + { + .got = rng_log_result(RngResultLog{ + .index = 2, .try_number = 3, .outcome = RngAttemptOutcome::SHINY_OFF_TARGET, .detail = "shiny_off_target", + .pokemon = std::nullopt, .next = RngPlanNext::STOP, + }), + .expected = "RngPlan: result attempt=3 try=3 outcome=shiny_off_target starter_rng=shiny_off_target nature=- ivs=- pid=- next=stop", + }, {.got = rng_log_confirmed(43771, 300, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=300 candidate=3 of=3"}, {.got = rng_log_confirmed(43771, 7, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=7 candidate=0 of=3"}, }; From 16cacaf25b1907bcb30ca71d50c897fbf4b8238b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 17:07:28 -0800 Subject: [PATCH 08/13] FRLG AutoStory: plan only seeds Starter RNG can wait for on the title screen Starter RNG waits on the title screen for the seed list's delay less 845 ms plus its calibration, and refuses a wait under 29,650 ms. The plan listed seeds whose wait came out just under that: on console B, TID 55740's first attempt was seed 11A3 (30,473 ms in the table, a 29,628 ms wait), and Starter RNG stopped with "The title screen delay cannot be less than 30s". 405C (30,488 ms) fails the same way. plan_starter_rng() now drops seeds whose wait, as prepare_timings() makes it, is under 29,650 ms plus a 1 s margin for the seed calibration (starter_rng_can_wait_for()). Tested on both seeds and on TID 55740's plan. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRngPlan.cpp | 8 ++- .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 17 +++++++ .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 49 +++++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp index 3c0269e714..eb3b073d07 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp @@ -313,11 +313,17 @@ RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ } +bool starter_rng_can_wait_for(const InitialSeedEntry& seed){ + const int64_t wait = int64_t(seed.seed_time_ms) + STARTER_RNG_FIXED_SEED_OFFSET_MS; + return wait >= int64_t(STARTER_RNG_MIN_TITLE_DELAY_MS + STARTER_RNG_SEED_DELAY_MARGIN_MS); +} + std::vector plan_starter_rng(const StarterRngPlanInputs& inputs){ const SeedsDatabase database(seeds_json_path(inputs.firered, inputs.language)); // Only the English lists hold the blackout button's seeds (initial_seed_entries()). const bool english_list = inputs.language != Language::Japanese; - const std::vector seeds = initial_seed_entries(database, inputs.sound, "Help", english_list); + std::vector seeds = initial_seed_entries(database, inputs.sound, "Help", english_list); + std::erase_if(seeds, [](const InitialSeedEntry& seed){ return !starter_rng_can_wait_for(seed); }); RngTargetFilters filters = inputs.filters; filters.min_advances = std::max(filters.min_advances, STARTER_RNG_MIN_ADVANCES); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h index fd6ea5bdff..4359e38bbf 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h @@ -167,6 +167,23 @@ struct StarterRngPlanInputs{ // Starter RNG's smallest Advances setting. inline constexpr uint64_t STARTER_RNG_MIN_ADVANCES = 940; +// Starter RNG waits on the title screen for the seed list's delay plus +// this (FIXED_SEED_OFFSET in StarterRng's attempts) plus its seed +// calibration, and refuses a wait under STARTER_RNG_MIN_TITLE_DELAY_MS +// (check_timings() in PokemonFRLG_BlindNavigation.cpp). +inline constexpr int64_t STARTER_RNG_FIXED_SEED_OFFSET_MS = -845; +inline constexpr uint64_t STARTER_RNG_MIN_TITLE_DELAY_MS = 29650; +// Room for the seed calibration, which Starter RNG learns as it resets and +// nudges by up to 2 frames between resets: 1 s is 60 frames, more than a +// calibrated console drifts. +inline constexpr uint64_t STARTER_RNG_SEED_DELAY_MARGIN_MS = 1000; +// Whether Starter RNG can wait on the title screen for `seed`'s delay, as +// prepare_timings() makes it before any calibration, with the margin +// above. (Console B, 2026-11-02, FireRed English: seeds 11A3 and 405C, at +// 30,473 and 30,488 ms in the table, are 29,628 and 29,643 ms waits, which +// Starter RNG refuses.) +bool starter_rng_can_wait_for(const InitialSeedEntry& seed); + // The attempts for Starter RNG, from the seed list of the version and // language in Help button mode, the one Starter RNG uses. std::vector plan_starter_rng(const StarterRngPlanInputs& inputs); diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp index 3366a0d964..d2ed14f9b8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -490,6 +490,54 @@ UnitTestResult test_log_lines(Logger&){ // A plan from the FireRed (English) list: every attempt starts from a seed // Starter RNG finds, takes at least its smallest Advances, and is shiny for // the SIDs it lists, which no earlier attempt listed. +// Console B, 2026-11-02, FireRed English, Mono, Help: Starter RNG refused +// both of these as title screen waits under 30 s. AutoStory's plan for TID +// 55740 started with 11A3 at 945 advances (29,628 ms after Starter RNG's +// offsets); StarterRng by hand with 405C at 990 advances listed 30,488 ms +// from the table, 29,643 ms after them. Neither seed may start an attempt. +UnitTestResult test_starter_plan_seed_delay(Logger&){ + struct Case{ + uint16_t seed; + uint64_t table_ms; + }; + const Case cases[] = { + {.seed = 0x11a3, .table_ms = 30473}, + {.seed = 0x405c, .table_ms = 30488}, + }; + const SeedsDatabase database(seeds_json_path(true, Language::English)); + const std::vector seeds = initial_seed_entries(database, SoundSetting::Mono, "Help", true); + for (const Case& item : cases){ + auto entry = std::find_if(seeds.begin(), seeds.end(), [&](const InitialSeedEntry& seed){ return seed.seed == item.seed; }); + if (entry == seeds.end() || entry->seed_time_ms != item.table_ms){ + return std::format("Expected seed {:04X} at {} ms in the FireRed English list (Mono, Help).", item.seed, item.table_ms); + } + if (starter_rng_can_wait_for(*entry)){ + return std::format("Seed {:04X} ({} ms): expected too early for Starter RNG's title screen.", item.seed, item.table_ms); + } + } + + const uint16_t tid = 55740; + const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ + .firered = true, + .language = Language::English, + .sound = SoundSetting::Mono, + .tid = tid, + .candidates = sid_helper_candidates(tid, 3001, 5), + .filters = {}, + .max_attempts = 5, + }); + if (attempts.empty()){ + return "Expected attempts for TID 55740, got none."; + } + for (size_t c = 0; c < attempts.size(); c++){ + const InitialSeedEntry& seed = attempts[c].target.seed; + if (seed.seed == 0x11a3 || seed.seed == 0x405c || !starter_rng_can_wait_for(seed)){ + return std::format("Attempt {}: seed {:04X} at {} ms is too early for the title screen.", c + 1, seed.seed, seed.seed_time_ms); + } + } + return true; +} + UnitTestResult test_starter_plan(Logger&){ const uint16_t tid = 43771; StarterRngPlanInputs inputs{ @@ -548,6 +596,7 @@ void add_tests_AutoStoryRngPlan(UnitTestDatabase& database){ database.add("attempt outcome", test_classify); database.add("step after each outcome", test_tracker); database.add("Starter RNG plan from the FireRed list", test_starter_plan); + database.add("Starter RNG plan: no seed too early for the title screen", test_starter_plan_seed_delay); database.add("RngPlan log lines", test_log_lines); } From 282394fcf35bc3834c2a1476afd89168e78e9c8b Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 17:10:18 -0800 Subject: [PATCH 09/13] FRLG AutoStory: an RNG plan attempt that can't run moves on A new outcome, UNUSABLE, is for an attempt Starter RNG refuses to run (its timings): it rules nothing out, and the tracker moves on to the next attempt still shiny for a SID left, or ends as MISSED_EVERY_ATTEMPT_LEFT when there is none. The result line names it "unusable". Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRngPlan.cpp | 21 ++++++------- .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 8 +++-- .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 31 +++++++++++++++++-- 3 files changed, 45 insertions(+), 15 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp index eb3b073d07..e6c4280a2c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp @@ -270,19 +270,12 @@ RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ } const std::vector& sids = m_attempts[m_attempt].sids; switch (outcome){ - case RngAttemptOutcome::SHINY:{ + case RngAttemptOutcome::SHINY: // The SID is one the target is shiny for. None of them: it wasn't // among the candidates. - std::vector narrowed; - for (uint16_t sid : m_remaining){ - if (std::find(sids.begin(), sids.end(), sid) != sids.end()){ - narrowed.push_back(sid); - } - } - m_remaining = std::move(narrowed); + std::erase_if(m_remaining, [&](uint16_t sid){ return std::find(sids.begin(), sids.end(), sid) == sids.end(); }); m_step = RngPlanStep::GOT_SHINY; return m_step; - } case RngAttemptOutcome::SHINY_OFF_TARGET: m_step = RngPlanStep::GOT_SHINY_OFF_TARGET; return m_step; @@ -303,8 +296,12 @@ RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ return m_step; case RngAttemptOutcome::MISSED: // A miss rules nothing out. Too many: try the next attempt instead. - m_retries++; - if (m_retries > m_max_retries && !move_to_next_attempt()){ + if (++m_retries <= m_max_retries){ + return m_step; + } + [[fallthrough]]; + case RngAttemptOutcome::UNUSABLE: + if (!move_to_next_attempt()){ m_step = RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT; } return m_step; @@ -390,6 +387,8 @@ const char* outcome_name(RngAttemptOutcome outcome){ return "hit_not_shiny"; case RngAttemptOutcome::MISSED: return "missed"; + case RngAttemptOutcome::UNUSABLE: + return "unusable"; } throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngAttemptOutcome."); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h index 4359e38bbf..131b735e5b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h @@ -88,6 +88,9 @@ enum class RngAttemptOutcome{ HIT_NOT_SHINY, // Another nature or other IVs: not the target. MISSED, + // The attempt couldn't run (Starter RNG refused its timings). It rules + // nothing out; the next attempt runs instead. + UNUSABLE, }; struct RngAttemptResult{ @@ -110,7 +113,8 @@ enum class RngPlanStep{ // Every attempt was hit and none was shiny. OUT_OF_ATTEMPTS, // The last attempt still shiny for a SID left was missed more often - // than the retries allow. (Missing an earlier one moves on to the next.) + // than the retries allow, or couldn't run. (Missing an earlier one moves + // on to the next.) MISSED_EVERY_ATTEMPT_LEFT, }; @@ -206,7 +210,7 @@ std::vector rng_attempt_lines(const std::vector& attemp // advances= sids=,... ruled_out=,... attempt_time=/ // total_time=/ calibration=// // (once at the try's start, then after every reset) -// RngPlan: result attempt= try= outcome= +// RngPlan: result attempt= try= outcome= // starter_rng= nature= ivs= // pid= next= // RngPlan: confirmed tid= sid= candidate= of= diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp index d2ed14f9b8..c404342122 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -358,6 +358,31 @@ std::vector tracker_retry_cases(){ .steps = {RUN_ATTEMPT, GOT_SHINY_OFF_TARGET, GOT_SHINY_OFF_TARGET}, .attempts = {1, 1, 1}, .remaining = {300}, .confirmed = std::nullopt, }, + }; +} +// Attempts that can't run. +std::vector tracker_unusable_cases(){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + return { + { + .name = "an attempt that can't run moves on", + .shiny_required = true, .outcomes = {MISSED, UNUSABLE, SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "the last attempt can't run", + .shiny_required = true, .outcomes = {UNUSABLE, UNUSABLE}, + .steps = {RUN_ATTEMPT, MISSED_EVERY_ATTEMPT_LEFT}, .attempts = {1, 1}, + .remaining = {100, 200, 300}, .confirmed = std::nullopt, + }, + }; +} +std::vector tracker_mixed_cases(){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + return { { .name = "miss, hit, misses, then shiny", .shiny_required = true, .outcomes = {MISSED, HIT_NOT_SHINY, MISSED, MISSED, SHINY}, @@ -411,8 +436,10 @@ UnitTestResult check_tracker_case(const TrackerCase& item){ UnitTestResult test_tracker(Logger&){ std::vector cases = tracker_cases(); - for (TrackerCase& item : tracker_retry_cases()){ - cases.push_back(std::move(item)); + for (auto* more : {tracker_retry_cases, tracker_unusable_cases, tracker_mixed_cases}){ + for (TrackerCase& item : more()){ + cases.push_back(std::move(item)); + } } for (const TrackerCase& item : cases){ const UnitTestResult result = check_tracker_case(item); From abb34b21c56f2f4b7427dc64c60ba79a21215653 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 17:12:30 -0800 Subject: [PATCH 10/13] FRLG AutoStory: plan Starter RNG targets with room in both waits More data from console B on the plan's targets: - The title screen: Starter RNG's docs warn that waits near 30,500 ms can miss the title press, so the plan now needs a 31,500 ms wait (STARTER_RNG_MIN_SEED_WAIT_MS), not 29,650 ms plus 1 s. - In game: Starter RNG needs at least 7,500 ms, which its 940-advance minimum just makes, and the in-game calibration moves it. Seed B579 at 967 advances (7,725 ms) went to a -720 calibration after one miss and 1,725 ms. Targets now start 720 advances past the minimum (STARTER_RNG_INGAME_MARGIN_ADVANCES, to tune from the logs). The order is unchanged: rank_rng_targets() over the targets that pass. Each "RngPlan: attempt=" line now gives Starter RNG's two waits before calibration (seed_wait_ms, ingame_ms). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRngPlan.cpp | 22 +++++++--- .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 44 +++++++++++++------ .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 38 ++++++++++++++-- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp index e6c4280a2c..e8d734e7aa 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp @@ -8,6 +8,8 @@ #include #include #include "Common/Cpp/Exceptions.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.h" #include "PokemonFRLG_AutoStoryRngPlan.h" namespace PokemonAutomation{ @@ -310,9 +312,18 @@ RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ } +int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed){ + return int64_t(seed.seed_time_ms) + STARTER_RNG_FIXED_SEED_OFFSET_MS; +} + bool starter_rng_can_wait_for(const InitialSeedEntry& seed){ - const int64_t wait = int64_t(seed.seed_time_ms) + STARTER_RNG_FIXED_SEED_OFFSET_MS; - return wait >= int64_t(STARTER_RNG_MIN_TITLE_DELAY_MS + STARTER_RNG_SEED_DELAY_MARGIN_MS); + return starter_rng_seed_wait_ms(seed) >= STARTER_RNG_MIN_SEED_WAIT_MS; +} + +int64_t starter_rng_ingame_ms(uint64_t advances){ + const int64_t frames = int64_t(advances) - int64_t(STARTER_RNG_CONTINUE_SCREEN_FRAMES) + + fixed_advances_offset(PokemonFRLG_RngTarget::starters); + return int64_t(frames * FRLG_FRAME_DURATION / 2); } std::vector plan_starter_rng(const StarterRngPlanInputs& inputs){ @@ -323,7 +334,7 @@ std::vector plan_starter_rng(const StarterRngPlanInputs& inputs){ std::erase_if(seeds, [](const InitialSeedEntry& seed){ return !starter_rng_can_wait_for(seed); }); RngTargetFilters filters = inputs.filters; - filters.min_advances = std::max(filters.min_advances, STARTER_RNG_MIN_ADVANCES); + filters.min_advances = std::max(filters.min_advances, STARTER_RNG_PLAN_MIN_ADVANCES); const std::vector targets = search_rng_targets(seeds, inputs.tid, inputs.candidates, filters); return plan_rng_attempts(targets, inputs.candidates, filters.shiny_required, inputs.max_attempts); } @@ -355,8 +366,9 @@ std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std std::string rng_log_attempt(size_t index, const RngAttempt& attempt){ return std::format( - "RngPlan: attempt={} seed={:04X} advances={} sids={}", - index + 1, attempt.target.seed.seed, attempt.target.advances, join_sids(attempt.sids) + "RngPlan: attempt={} seed={:04X} advances={} sids={} seed_wait_ms={} ingame_ms={}", + index + 1, attempt.target.seed.seed, attempt.target.advances, join_sids(attempt.sids), + starter_rng_seed_wait_ms(attempt.target.seed), starter_rng_ingame_ms(attempt.target.advances) ); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h index 131b735e5b..9ecc96bc1e 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h @@ -171,23 +171,38 @@ struct StarterRngPlanInputs{ // Starter RNG's smallest Advances setting. inline constexpr uint64_t STARTER_RNG_MIN_ADVANCES = 940; -// Starter RNG waits on the title screen for the seed list's delay plus -// this (FIXED_SEED_OFFSET in StarterRng's attempts) plus its seed -// calibration, and refuses a wait under STARTER_RNG_MIN_TITLE_DELAY_MS -// (check_timings() in PokemonFRLG_BlindNavigation.cpp). +// Starter RNG's title screen wait, before calibration: the seed list's +// delay plus this (FIXED_SEED_OFFSET in StarterRng's attempts). inline constexpr int64_t STARTER_RNG_FIXED_SEED_OFFSET_MS = -845; -inline constexpr uint64_t STARTER_RNG_MIN_TITLE_DELAY_MS = 29650; -// Room for the seed calibration, which Starter RNG learns as it resets and -// nudges by up to 2 frames between resets: 1 s is 60 frames, more than a -// calibrated console drifts. -inline constexpr uint64_t STARTER_RNG_SEED_DELAY_MARGIN_MS = 1000; -// Whether Starter RNG can wait on the title screen for `seed`'s delay, as -// prepare_timings() makes it before any calibration, with the margin -// above. (Console B, 2026-11-02, FireRed English: seeds 11A3 and 405C, at -// 30,473 and 30,488 ms in the table, are 29,628 and 29,643 ms waits, which -// Starter RNG refuses.) +// The shortest title screen wait the plan uses. Starter RNG refuses waits +// under 29,650 ms, and its docs (pokemonautomation.github.io, +// docs/Programs/PokemonFRLG/StarterRng.md, "Avoid low seed delays when +// possible") warn that waits near 30,500 ms can miss the title screen +// press. 31,500 ms keeps a second clear of that for the seed calibration. +inline constexpr int64_t STARTER_RNG_MIN_SEED_WAIT_MS = 31500; +// Starter RNG's title screen wait for `seed`, as prepare_timings() makes it +// before calibration. +int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed); +// Whether the wait is at least STARTER_RNG_MIN_SEED_WAIT_MS. (Console B, +// 2026-11-02, FireRed English: seeds 11A3 and 405C, at 30,473 and +// 30,488 ms in the table, are 29,628 and 29,643 ms waits, which Starter +// RNG refuses.) bool starter_rng_can_wait_for(const InitialSeedEntry& seed); +// Starter RNG's Continue Screen frames, taken off the advances before the +// in-game wait (CONTINUE_SCREEN_FRAMES in StarterRng's attempts). +inline constexpr uint64_t STARTER_RNG_CONTINUE_SCREEN_FRAMES = 200; +// Starter RNG's in-game wait for `advances`, before calibration. It +// refuses one under 7,500 ms, which STARTER_RNG_MIN_ADVANCES just makes. +int64_t starter_rng_ingame_ms(uint64_t advances); +// Room for the in-game calibration, in advances (2 per frame of wait): the +// calibration moves the wait, and it went to -720 after one miss on console +// B (2026-11-02, seed B579 at 967 advances: an 8 s wait cut to 1.7 s). The +// plan's targets are at least this many advances past Starter RNG's +// minimum. To tune from the logs' calibrations. +inline constexpr uint64_t STARTER_RNG_INGAME_MARGIN_ADVANCES = 720; +inline constexpr uint64_t STARTER_RNG_PLAN_MIN_ADVANCES = STARTER_RNG_MIN_ADVANCES + STARTER_RNG_INGAME_MARGIN_ADVANCES; + // The attempts for Starter RNG, from the seed list of the version and // language in Help button mode, the one Starter RNG uses. std::vector plan_starter_rng(const StarterRngPlanInputs& inputs); @@ -204,6 +219,7 @@ std::vector rng_attempt_lines(const std::vector& attemp // RngPlan: tid= source= // RngPlan: sid_helper tid= target_advances= candidates=,... // RngPlan: attempt= seed= advances= sids=,... +// seed_wait_ms= ingame_ms= (Starter RNG's waits, before calibration) // then, with Run Starter RNG in AutoStory, for each try of an attempt // (rng_log_state() in PokemonFRLG_AutoStoryRngLoop.h): // RngPlan: state attempt=/ try=/ reset=/ seed= diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp index c404342122..6bc702c9a0 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp @@ -477,7 +477,9 @@ UnitTestResult test_log_lines(Logger&){ {.got = rng_log_tid(43771, true), .expected = "RngPlan: tid=43771 source=card"}, {.got = rng_log_tid(43771, false), .expected = "RngPlan: tid=43771 source=option"}, {.got = rng_log_sid_helper(43771, 3001, CANDIDATES), .expected = "RngPlan: sid_helper tid=43771 target_advances=3001 candidates=100,200,300"}, - {.got = rng_log_attempt(0, attempt), .expected = "RngPlan: attempt=1 seed=2EF6 advances=29364 sids=100,200"}, + {.got = rng_log_attempt(0, attempt), .expected = std::format( + "RngPlan: attempt=1 seed=2EF6 advances=29364 sids=100,200 seed_wait_ms=49155 ingame_ms={}", starter_rng_ingame_ms(29364) + )}, { .got = rng_log_result(RngResultLog{ .index = 1, .try_number = 2, .outcome = RngAttemptOutcome::HIT_NOT_SHINY, .detail = "target_hit", @@ -558,9 +560,36 @@ UnitTestResult test_starter_plan_seed_delay(Logger&){ } for (size_t c = 0; c < attempts.size(); c++){ const InitialSeedEntry& seed = attempts[c].target.seed; - if (seed.seed == 0x11a3 || seed.seed == 0x405c || !starter_rng_can_wait_for(seed)){ + if (seed.seed == 0x11a3 || seed.seed == 0x405c || starter_rng_seed_wait_ms(seed) < 31500){ return std::format("Attempt {}: seed {:04X} at {} ms is too early for the title screen.", c + 1, seed.seed, seed.seed_time_ms); } + if (attempts[c].target.advances < 1660){ + return std::format("Attempt {}: {} advances leave no room for the in-game calibration.", c + 1, attempts[c].target.advances); + } + } + return true; +} + +// Starter RNG's in-game wait: 7,500 ms at its 940-advance minimum; console +// B's B579 target at 967 advances was 7,725 ms, which one miss's +// calibration (-720) cut under the minimum. Targets start 720 advances on. +UnitTestResult test_starter_ingame_wait(Logger&){ + struct Case{ + uint64_t advances; + int64_t ms; + }; + const Case cases[] = { + {.advances = 940, .ms = 7500}, + {.advances = 967, .ms = 7725}, + {.advances = 1660, .ms = 13500}, + }; + for (const Case& item : cases){ + if (starter_rng_ingame_ms(item.advances) != item.ms){ + return std::format("{} advances: expected {} ms in game, got {}.", item.advances, item.ms, starter_rng_ingame_ms(item.advances)); + } + } + if (STARTER_RNG_PLAN_MIN_ADVANCES != 1660){ + return std::format("Expected the plan's minimum at 1660 advances, got {}.", STARTER_RNG_PLAN_MIN_ADVANCES); } return true; } @@ -588,8 +617,8 @@ UnitTestResult test_starter_plan(Logger&){ if (!database.find_seed(attempt.target.seed.seed, SoundSetting::Mono, 5).found){ return std::format("Attempt {}: Starter RNG can't find Target Seed {:04X}.", c + 1, attempt.target.seed.seed); } - if (attempt.target.advances < STARTER_RNG_MIN_ADVANCES){ - return std::format("Attempt {}: Advances {} below Starter RNG's {}.", c + 1, attempt.target.advances, STARTER_RNG_MIN_ADVANCES); + if (attempt.target.advances < STARTER_RNG_PLAN_MIN_ADVANCES){ + return std::format("Attempt {}: Advances {} below the plan's {}.", c + 1, attempt.target.advances, STARTER_RNG_PLAN_MIN_ADVANCES); } if (attempt.sids.empty()){ return std::format("Attempt {}: shiny for no SID candidate.", c + 1); @@ -624,6 +653,7 @@ void add_tests_AutoStoryRngPlan(UnitTestDatabase& database){ database.add("step after each outcome", test_tracker); database.add("Starter RNG plan from the FireRed list", test_starter_plan); database.add("Starter RNG plan: no seed too early for the title screen", test_starter_plan_seed_delay); + database.add("Starter RNG plan: room for the in-game calibration", test_starter_ingame_wait); database.add("RngPlan log lines", test_log_lines); } From 4c6d0df72d4f562b1178e0de270e04aae12754cb Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 18:09:17 -0800 Subject: [PATCH 11/13] FRLG: move the RNG plan into RngManipulation modules The plan doesn't know about the story, so it moves out of AutoStory into four RngManipulation modules, each with its own test suite: RngPlanInputs (TID/IV/nature text), RngPlan (SID Helper's candidates, the attempt order and Starter RNG's timing limits), RngPlanTracker (outcomes, the step after each, rng_plan_next) and RngPlanLog (the attempt and "RngPlan:" lines). The tests' made-up targets move to RngPlanTestTargets. Code is moved unchanged; AutoStory keeps the options group. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/PokemonFRLG_Tests.cpp | 10 +- .../AutoStory/PokemonFRLG_AutoStory.cpp | 1 + .../PokemonFRLG_AutoStoryRngOptions.cpp | 2 + .../PokemonFRLG_AutoStoryRngOptions.h | 5 +- .../PokemonFRLG_AutoStoryRngPlan.cpp | 477 ------------- .../AutoStory/PokemonFRLG_AutoStoryRngPlan.h | 276 -------- .../PokemonFRLG_AutoStoryRngPlan_Tests.cpp | 664 ------------------ .../RngManipulation/PokemonFRLG_RngPlan.cpp | 159 +++++ .../RngManipulation/PokemonFRLG_RngPlan.h | 121 ++++ .../PokemonFRLG_RngPlanInputs.cpp | 106 +++ .../PokemonFRLG_RngPlanInputs.h | 39 + .../PokemonFRLG_RngPlanInputs_Tests.cpp | 137 ++++ .../PokemonFRLG_RngPlanInputs_Tests.h | 23 + .../PokemonFRLG_RngPlanLog.cpp | 137 ++++ .../RngManipulation/PokemonFRLG_RngPlanLog.h | 77 ++ .../PokemonFRLG_RngPlanLog_Tests.cpp | 109 +++ .../PokemonFRLG_RngPlanLog_Tests.h | 23 + .../PokemonFRLG_RngPlanTestTargets.cpp | 61 ++ .../PokemonFRLG_RngPlanTestTargets.h | 46 ++ .../PokemonFRLG_RngPlanTracker.cpp | 134 ++++ .../PokemonFRLG_RngPlanTracker.h | 119 ++++ .../PokemonFRLG_RngPlanTracker_Tests.cpp | 262 +++++++ .../PokemonFRLG_RngPlanTracker_Tests.h | 23 + .../PokemonFRLG_RngPlan_Tests.cpp | 287 ++++++++ .../PokemonFRLG_RngPlan_Tests.h | 23 + SerialPrograms/cmake/SourceFiles.cmake | 21 +- 26 files changed, 1918 insertions(+), 1424 deletions(-) delete mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp delete mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h delete mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.h diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 336d9e24e9..542ced3960 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -40,8 +40,11 @@ #include "Programs/PokemonFRLG_NamingScreenKeyboard.h" #include "PokemonFRLG_Navigation.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryBattle.h" -#include "Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h" #include "Programs/AutoStory/PokemonFRLG_AutoStoryTable.h" +#include "Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h" +#include "Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h" +#include "Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h" +#include "Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngTargetSearch_Tests.h" #include "Tests/TestUtils.h" @@ -703,12 +706,15 @@ void add_tests(UnitTestDatabase& database){ add_tests_AutoStory(database); add_tests_AutoStorySimulation(database); add_tests_AutoStoryItems(database); - add_tests_AutoStoryRngPlan(database); add_tests_NamingCursor(database); add_tests_NamingPlan(database); add_tests_NamingKeys(database); add_tests_NamingText(database); add_tests_RngTargetSearch(database); + add_tests_RngPlanInputs(database); + add_tests_RngPlan(database); + add_tests_RngPlanTracker(database); + add_tests_RngPlanLog(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index a005262e15..f22604bc46 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -17,6 +17,7 @@ #include "VideoGames/PokemonFRLG/PokemonFRLG_Navigation.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_NamingScreenKeyboard.h" #include "VideoGames/PokemonFRLG/Programs/PokemonFRLG_TrainerCard.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h" #include "PokemonFRLG_AutoStoryEvents.h" #include "PokemonFRLG_AutoStoryTable.h" #include "PokemonFRLG_AutoStory.h" diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp index 66b7c02642..5d15a4f79c 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp @@ -5,6 +5,8 @@ */ #include "Common/Cpp/Exceptions.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h" #include "PokemonFRLG_AutoStoryRngOptions.h" namespace PokemonAutomation{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h index 38f4cc0366..1c46aa38a2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h @@ -3,7 +3,7 @@ * From: https://github.com/PokemonAutomation/ * * The RNG plan options: what AutoStory's Starter RNG stop searches targets - * with (PokemonFRLG_AutoStoryRngPlan.h). + * with (RngManipulation/PokemonFRLG_RngPlan.h). * */ @@ -20,8 +20,9 @@ #include "Common/Cpp/Options/SimpleIntegerOption.h" #include "Common/Cpp/Options/StringOption.h" #include "CommonFramework/Language.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" -#include "PokemonFRLG_AutoStoryRngPlan.h" namespace PokemonAutomation{ namespace NintendoSwitch{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp deleted file mode 100644 index e8d734e7aa..0000000000 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp +++ /dev/null @@ -1,477 +0,0 @@ -/* AutoStory RNG Plan - * - * From: https://github.com/PokemonAutomation/ - * - */ - -#include -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h" -#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.h" -#include "PokemonFRLG_AutoStoryRngPlan.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonFRLG{ - -using namespace Pokemon; - - -std::vector sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count){ - // As SidHelper's get_sid_messages(): from "count / 2" steps before the target. - const int start = -int(count) / 2; - const int end = start + int(count); - const int farthest = std::max(-start, end - 1); - - std::vector candidates; - AdvRngSearcher searcher(tid, uint64_t(int64_t(target_advances) + 2 * start)); - for (int i = start; i < end; i++){ - candidates.push_back({uint16_t(searcher.state.s0 >> 16), double(farthest + 1 - std::abs(i))}); - searcher.advance_state(); - searcher.advance_state(); - } - return candidates; -} - - -namespace{ - -std::string trim(const std::string& text){ - const size_t first = text.find_first_not_of(' '); - if (first == std::string::npos){ - return ""; - } - const size_t last = text.find_last_not_of(' '); - return text.substr(first, last - first + 1); -} - -std::vector split(const std::string& text, char separator){ - std::vector parts; - size_t start = 0; - while (true){ - const size_t next = text.find(separator, start); - parts.push_back(trim(text.substr(start, next == std::string::npos ? std::string::npos : next - start))); - if (next == std::string::npos){ - return parts; - } - start = next + 1; - } -} - -} - - -std::optional parse_trainer_id(const std::string& text){ - const std::string digits = trim(text); - if (digits.empty() || digits.size() > 5 || !std::all_of(digits.begin(), digits.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ - return std::nullopt; - } - const int value = std::stoi(digits); - if (value > 0xffff){ - return std::nullopt; - } - return uint16_t(value); -} - -std::optional parse_ivs(const std::string& text){ - const std::vector parts = split(text, '/'); - if (parts.size() != 6){ - return std::nullopt; - } - uint8_t values[6]; - for (size_t c = 0; c < 6; c++){ - const std::string& part = parts[c]; - if (part.empty() || part.size() > 2 || !std::all_of(part.begin(), part.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ - return std::nullopt; - } - const int value = std::stoi(part); - if (value > 31){ - return std::nullopt; - } - values[c] = uint8_t(value); - } - AdvIVs ivs; - ivs.hp = values[0]; - ivs.attack = values[1]; - ivs.defense = values[2]; - ivs.spatk = values[3]; - ivs.spdef = values[4]; - ivs.speed = values[5]; - return ivs; -} - -std::string ivs_to_string(const AdvIVs& ivs){ - return std::format("{}/{}/{}/{}/{}/{}", ivs.hp, ivs.attack, ivs.defense, ivs.spatk, ivs.spdef, ivs.speed); -} - -std::optional> parse_natures(const std::string& text){ - std::vector natures; - if (trim(text).empty()){ - return natures; - } - for (const std::string& name : split(text, ',')){ - const AdvNature nature = string_to_nature(name); - if (nature == AdvNature::Any){ - return std::nullopt; - } - natures.push_back(nature); - } - return natures; -} - - -namespace{ - -// "targets" with only the shiny candidates still "left", and their share of -// the weight left. -std::vector rescore( - const std::vector& targets, - const std::vector& candidates, - const std::vector& left -){ - double total = 0; - for (size_t c = 0; c < candidates.size(); c++){ - if (left[c]){ - total += candidates[c].weight; - } - } - std::vector scored; - scored.reserve(targets.size()); - for (const RngTarget& target : targets){ - RngTarget copy = target; - copy.shiny_for.clear(); - double weight = 0; - for (const ShinyCandidate& shiny : target.shiny_for){ - if (left[shiny.candidate]){ - copy.shiny_for.push_back(shiny); - weight += candidates[shiny.candidate].weight; - } - } - copy.shiny_probability = total > 0 ? weight / total : 0; - scored.emplace_back(std::move(copy)); - } - return scored; -} - -RngAttempt make_attempt(const RngTarget& target, const std::vector& candidates){ - RngAttempt attempt{target, {}}; - for (const ShinyCandidate& shiny : target.shiny_for){ - attempt.sids.push_back(candidates[shiny.candidate].sid); - } - return attempt; -} - -} - - -std::vector plan_rng_attempts( - const std::vector& targets, - const std::vector& candidates, - bool shiny_required, - size_t max_attempts -){ - std::vector attempts; - if (targets.empty() || max_attempts == 0){ - return attempts; - } - std::vector left(candidates.size(), true); - - if (!shiny_required){ - // With no shiny chance, rank_rng_targets() sorts by time alone. - std::vector by_time = targets; - for (RngTarget& target : by_time){ - target.shiny_probability = 0; - } - rank_rng_targets(by_time); - const uint32_t quickest = by_time.front().state; - for (const RngTarget& target : rescore(targets, candidates, left)){ - if (target.state == quickest){ - attempts.push_back(make_attempt(target, candidates)); - break; - } - } - return attempts; - } - - while (attempts.size() < max_attempts){ - std::vector scored = rescore(targets, candidates, left); - rank_rng_targets(scored); - const RngTarget& best = scored.front(); - if (best.shiny_for.empty()){ - break; - } - for (const ShinyCandidate& shiny : best.shiny_for){ - left[shiny.candidate] = false; - } - attempts.push_back(make_attempt(best, candidates)); - if (std::find(left.begin(), left.end(), true) == left.end()){ - break; - } - } - return attempts; -} - - -RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResult& result){ - const AdvIVs& want = target.pokemon.ivs; - const AdvIVs& got = result.ivs; - const bool same_ivs = want.hp == got.hp && want.attack == got.attack && want.defense == got.defense - && want.spatk == got.spatk && want.spdef == got.spdef && want.speed == got.speed; - const bool hit = result.nature == target.pokemon.nature && same_ivs; - if (result.shiny){ - return hit ? RngAttemptOutcome::SHINY : RngAttemptOutcome::SHINY_OFF_TARGET; - } - return hit ? RngAttemptOutcome::HIT_NOT_SHINY : RngAttemptOutcome::MISSED; -} - - -RngPlanTracker::RngPlanTracker( - std::vector attempts, - const std::vector& candidates, - bool shiny_required, - size_t max_retries -) - : m_attempts(std::move(attempts)) - , m_shiny_required(shiny_required) - , m_max_retries(max_retries) - , m_step(m_attempts.empty() ? RngPlanStep::OUT_OF_ATTEMPTS : RngPlanStep::RUN_ATTEMPT) -{ - for (const SidCandidate& candidate : candidates){ - m_remaining.push_back(candidate.sid); - } -} - -std::optional RngPlanTracker::confirmed_sid() const{ - if (m_step == RngPlanStep::GOT_SHINY && m_remaining.size() == 1){ - return m_remaining.front(); - } - return std::nullopt; -} - -bool RngPlanTracker::is_ruled_out(uint16_t sid) const{ - return std::find(m_remaining.begin(), m_remaining.end(), sid) == m_remaining.end(); -} - -bool RngPlanTracker::move_to_next_attempt(){ - for (size_t next = m_attempt + 1; next < m_attempts.size(); next++){ - const std::vector& next_sids = m_attempts[next].sids; - if (!m_shiny_required || !std::all_of(next_sids.begin(), next_sids.end(), [&](uint16_t sid){ return is_ruled_out(sid); })){ - m_attempt = next; - m_retries = 0; - return true; - } - } - return false; -} - -RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ - if (m_step != RngPlanStep::RUN_ATTEMPT){ - return m_step; - } - const std::vector& sids = m_attempts[m_attempt].sids; - switch (outcome){ - case RngAttemptOutcome::SHINY: - // The SID is one the target is shiny for. None of them: it wasn't - // among the candidates. - std::erase_if(m_remaining, [&](uint16_t sid){ return std::find(sids.begin(), sids.end(), sid) == sids.end(); }); - m_step = RngPlanStep::GOT_SHINY; - return m_step; - case RngAttemptOutcome::SHINY_OFF_TARGET: - m_step = RngPlanStep::GOT_SHINY_OFF_TARGET; - return m_step; - case RngAttemptOutcome::HIT_NOT_SHINY: - if (!m_shiny_required){ - m_step = RngPlanStep::GOT_TARGET; - return m_step; - } - m_remaining.erase( - std::remove_if(m_remaining.begin(), m_remaining.end(), [&](uint16_t sid){ - return std::find(sids.begin(), sids.end(), sid) != sids.end(); - }), - m_remaining.end() - ); - if (!move_to_next_attempt()){ - m_step = RngPlanStep::OUT_OF_ATTEMPTS; - } - return m_step; - case RngAttemptOutcome::MISSED: - // A miss rules nothing out. Too many: try the next attempt instead. - if (++m_retries <= m_max_retries){ - return m_step; - } - [[fallthrough]]; - case RngAttemptOutcome::UNUSABLE: - if (!move_to_next_attempt()){ - m_step = RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT; - } - return m_step; - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngAttemptOutcome."); -} - - -int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed){ - return int64_t(seed.seed_time_ms) + STARTER_RNG_FIXED_SEED_OFFSET_MS; -} - -bool starter_rng_can_wait_for(const InitialSeedEntry& seed){ - return starter_rng_seed_wait_ms(seed) >= STARTER_RNG_MIN_SEED_WAIT_MS; -} - -int64_t starter_rng_ingame_ms(uint64_t advances){ - const int64_t frames = int64_t(advances) - int64_t(STARTER_RNG_CONTINUE_SCREEN_FRAMES) - + fixed_advances_offset(PokemonFRLG_RngTarget::starters); - return int64_t(frames * FRLG_FRAME_DURATION / 2); -} - -std::vector plan_starter_rng(const StarterRngPlanInputs& inputs){ - const SeedsDatabase database(seeds_json_path(inputs.firered, inputs.language)); - // Only the English lists hold the blackout button's seeds (initial_seed_entries()). - const bool english_list = inputs.language != Language::Japanese; - std::vector seeds = initial_seed_entries(database, inputs.sound, "Help", english_list); - std::erase_if(seeds, [](const InitialSeedEntry& seed){ return !starter_rng_can_wait_for(seed); }); - - RngTargetFilters filters = inputs.filters; - filters.min_advances = std::max(filters.min_advances, STARTER_RNG_PLAN_MIN_ADVANCES); - const std::vector targets = search_rng_targets(seeds, inputs.tid, inputs.candidates, filters); - return plan_rng_attempts(targets, inputs.candidates, filters.shiny_required, inputs.max_attempts); -} - - -namespace{ - -std::string join_sids(const std::vector& sids){ - std::string text; - for (uint16_t sid : sids){ - text += (text.empty() ? "" : ",") + std::to_string(sid); - } - return text; -} - -} - -std::string rng_log_tid(uint16_t tid, bool from_card){ - return std::format("RngPlan: tid={} source={}", tid, from_card ? "card" : "option"); -} - -std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std::vector& candidates){ - std::vector sids; - for (const SidCandidate& candidate : candidates){ - sids.push_back(candidate.sid); - } - return std::format("RngPlan: sid_helper tid={} target_advances={} candidates={}", tid, target_advances, join_sids(sids)); -} - -std::string rng_log_attempt(size_t index, const RngAttempt& attempt){ - return std::format( - "RngPlan: attempt={} seed={:04X} advances={} sids={} seed_wait_ms={} ingame_ms={}", - index + 1, attempt.target.seed.seed, attempt.target.advances, join_sids(attempt.sids), - starter_rng_seed_wait_ms(attempt.target.seed), starter_rng_ingame_ms(attempt.target.advances) - ); -} - -RngPlanNext rng_plan_next(RngPlanStep step, size_t index, size_t next_index){ - switch (step){ - case RngPlanStep::RUN_ATTEMPT: - return next_index == index ? RngPlanNext::RETRY : RngPlanNext::NEXT_ATTEMPT; - case RngPlanStep::GOT_SHINY: - return RngPlanNext::CONFIRM; - case RngPlanStep::GOT_SHINY_OFF_TARGET: - case RngPlanStep::GOT_TARGET: - case RngPlanStep::OUT_OF_ATTEMPTS: - case RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT: - return RngPlanNext::STOP; - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngPlanStep."); -} - -namespace{ - -const char* outcome_name(RngAttemptOutcome outcome){ - switch (outcome){ - case RngAttemptOutcome::SHINY: - return "shiny"; - case RngAttemptOutcome::SHINY_OFF_TARGET: - return "shiny_off_target"; - case RngAttemptOutcome::HIT_NOT_SHINY: - return "hit_not_shiny"; - case RngAttemptOutcome::MISSED: - return "missed"; - case RngAttemptOutcome::UNUSABLE: - return "unusable"; - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngAttemptOutcome."); -} - -const char* next_name(RngPlanNext next){ - switch (next){ - case RngPlanNext::RETRY: - return "retry"; - case RngPlanNext::NEXT_ATTEMPT: - return "next_attempt"; - case RngPlanNext::CONFIRM: - return "confirm"; - case RngPlanNext::STOP: - return "stop"; - } - throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngPlanNext."); -} - -} - -std::string rng_log_result(const RngResultLog& result){ - const std::optional& pokemon = result.pokemon; - return std::format( - "RngPlan: result attempt={} try={} outcome={} starter_rng={} nature={} ivs={} pid={} next={}", - result.index + 1, result.try_number, outcome_name(result.outcome), - result.detail.empty() ? "-" : result.detail, - pokemon ? nature_to_string(pokemon->nature) : "-", - pokemon ? ivs_to_string(pokemon->ivs) : "-", - pokemon ? std::format("{:08X}", pokemon->pid) : "-", - next_name(result.next) - ); -} - -std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates){ - size_t k = 0; - for (size_t c = 0; c < sid_helper_candidates.size(); c++){ - if (sid_helper_candidates[c].sid == sid){ - k = c + 1; - break; - } - } - return std::format("RngPlan: confirmed tid={} sid={} candidate={} of={}", tid, sid, k, sid_helper_candidates.size()); -} - - -std::vector rng_attempt_lines(const std::vector& attempts){ - std::vector lines; - for (size_t c = 0; c < attempts.size(); c++){ - const RngTarget& target = attempts[c].target; - std::string shiny; - if (attempts[c].sids.empty()){ - shiny = "not shiny for any SID candidate"; - }else{ - shiny = "shiny for SID"; - for (size_t s = 0; s < attempts[c].sids.size(); s++){ - shiny += (s == 0 ? " " : ", ") + std::to_string(attempts[c].sids[s]); - } - shiny += std::format(" ({:.0f}% of the candidates left)", target.shiny_probability * 100); - } - lines.push_back(std::format( - "Attempt {}: Target Seed {:04X}, Advances {}: {}, IVs {}, PID {:08X}, {}.", - c + 1, target.seed.seed, target.advances, - nature_to_string(target.pokemon.nature), ivs_to_string(target.pokemon.ivs), target.pokemon.pid, - shiny - )); - } - return lines; -} - - - -} -} -} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h deleted file mode 100644 index 9ecc96bc1e..0000000000 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h +++ /dev/null @@ -1,276 +0,0 @@ -/* AutoStory RNG Plan - * - * From: https://github.com/PokemonAutomation/ - * - * Which Starter RNG targets AutoStory asks for, in order, when the SID is - * only narrowed down to SID Helper's candidates, and what to do after each - * one. The loop is the one PokemonFRLG_RngTargetSearch.h describes: - * - Shiny, and the target was hit: the SID is one of those the target is - * shiny for. With one left, it is confirmed and later targets can be - * searched with it alone. - * - Shiny, but not the target (another advance): it says nothing about - * the SID. The plan ends with the shiny. - * - Nature and IVs match, but not shiny: the target was hit, so the SIDs it - * is shiny for are wrong. Go on to the next attempt. - * - Nature or IVs differ: the target was missed. Run it again, up to the - * retries, then go on to the next attempt. - * - */ - -#ifndef PokemonAutomation_PokemonFRLG_AutoStoryRngPlan_H -#define PokemonAutomation_PokemonFRLG_AutoStoryRngPlan_H - -#include -#include -#include -#include -#include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "CommonFramework/Language.h" -#include "Pokemon/Pokemon_AdvRng.h" -#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h" -#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonFRLG{ - - -// The SIDs SID Helper lists for "tid": "count" of them, 2 advances apart, -// centered on "target_advances", as its get_sid_messages() lists them. The -// middle one, where SID Helper aims, weighs the most, and each step away one -// less, so the outermost weigh 1. -std::vector sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count); - -// A TID or SID typed as a number from 0 to 65535. Nothing for any other text. -std::optional parse_trainer_id(const std::string& text); - -// "HP/Atk/Def/SpA/SpD/Spe", as Ten Lines and the RNG programs list IVs. -// Nothing for text that isn't six numbers from 0 to 31. -std::optional parse_ivs(const std::string& text); -std::string ivs_to_string(const Pokemon::AdvIVs& ivs); - -// Natures separated by commas ("Modest, Timid"). Empty text is any nature -// (no entries). Nothing for a name that isn't a nature. -std::optional> parse_natures(const std::string& text); - - -struct RngAttempt{ - RngTarget target; - // The SIDs the target is shiny for, of those the attempts before it - // leave. Empty when the target isn't shiny for any of them. - std::vector sids; -}; - -// The attempts to make, in order, for SID candidates "candidates" (the ones -// "targets" were searched with). Each is the best target by -// rank_rng_targets() over the candidates the attempts before it leave, so a -// target shiny for several candidates comes before one shiny for one, unless -// it takes much longer. Stops after "max_attempts", or once no target is -// shiny for a candidate left. -// -// With "shiny_required" off, a hit ends the plan whether it is shiny or not, -// so the plan is the one quickest target. -std::vector plan_rng_attempts( - const std::vector& targets, - const std::vector& candidates, - bool shiny_required, - size_t max_attempts -); - - -// What came out of one attempt. -enum class RngAttemptOutcome{ - // Shiny, with the target's nature and IVs. - SHINY, - // Shiny, but not the target, or not known to be. - SHINY_OFF_TARGET, - // The target's nature and IVs, not shiny. - HIT_NOT_SHINY, - // Another nature or other IVs: not the target. - MISSED, - // The attempt couldn't run (Starter RNG refused its timings). It rules - // nothing out; the next attempt runs instead. - UNUSABLE, -}; - -struct RngAttemptResult{ - bool shiny; - Pokemon::AdvNature nature; - Pokemon::AdvIVs ivs; -}; -RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResult& result); - - -enum class RngPlanStep{ - // Run current_attempt(): the first, the next, or the same again after a miss. - RUN_ATTEMPT, - // Shiny, the target hit. remaining_sids() are the SIDs it can be. - GOT_SHINY, - // Shiny, not the target: the SIDs are as they were, none confirmed. - GOT_SHINY_OFF_TARGET, - // Shiny not required, and the target was hit. - GOT_TARGET, - // Every attempt was hit and none was shiny. - OUT_OF_ATTEMPTS, - // The last attempt still shiny for a SID left was missed more often - // than the retries allow, or couldn't run. (Missing an earlier one moves - // on to the next.) - MISSED_EVERY_ATTEMPT_LEFT, -}; - -// Follows the plan through the attempts' outcomes. -class RngPlanTracker{ -public: - RngPlanTracker( - std::vector attempts, - const std::vector& candidates, - bool shiny_required, - size_t max_retries - ); - - RngPlanStep step() const{ return m_step; } - size_t attempt_index() const{ return m_attempt; } - size_t attempt_count() const{ return m_attempts.size(); } - const RngAttempt& current_attempt() const{ return m_attempts[m_attempt]; } - // The SID candidates not ruled out, in the order given. - const std::vector& remaining_sids() const{ return m_remaining; } - // The SID, once a shiny leaves one candidate. - std::optional confirmed_sid() const; - - // The next step after the current attempt came out as "outcome". Once - // the plan has ended, the step stays as it is. - RngPlanStep record(RngAttemptOutcome outcome); - -private: - bool is_ruled_out(uint16_t sid) const; - // Moves to the next attempt still shiny for a SID left (with shiny - // required; else any next one). False if there is none. - bool move_to_next_attempt(); - - std::vector m_attempts; - bool m_shiny_required; - size_t m_max_retries; - std::vector m_remaining; - size_t m_attempt = 0; - size_t m_retries = 0; - RngPlanStep m_step; -}; - - -// What Starter RNG's targets are searched with. -struct StarterRngPlanInputs{ - bool firered; - Language language; - SoundSetting sound; - uint16_t tid; - std::vector candidates; - RngTargetFilters filters; - size_t max_attempts; -}; - -// Starter RNG's smallest Advances setting. -inline constexpr uint64_t STARTER_RNG_MIN_ADVANCES = 940; - -// Starter RNG's title screen wait, before calibration: the seed list's -// delay plus this (FIXED_SEED_OFFSET in StarterRng's attempts). -inline constexpr int64_t STARTER_RNG_FIXED_SEED_OFFSET_MS = -845; -// The shortest title screen wait the plan uses. Starter RNG refuses waits -// under 29,650 ms, and its docs (pokemonautomation.github.io, -// docs/Programs/PokemonFRLG/StarterRng.md, "Avoid low seed delays when -// possible") warn that waits near 30,500 ms can miss the title screen -// press. 31,500 ms keeps a second clear of that for the seed calibration. -inline constexpr int64_t STARTER_RNG_MIN_SEED_WAIT_MS = 31500; -// Starter RNG's title screen wait for `seed`, as prepare_timings() makes it -// before calibration. -int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed); -// Whether the wait is at least STARTER_RNG_MIN_SEED_WAIT_MS. (Console B, -// 2026-11-02, FireRed English: seeds 11A3 and 405C, at 30,473 and -// 30,488 ms in the table, are 29,628 and 29,643 ms waits, which Starter -// RNG refuses.) -bool starter_rng_can_wait_for(const InitialSeedEntry& seed); - -// Starter RNG's Continue Screen frames, taken off the advances before the -// in-game wait (CONTINUE_SCREEN_FRAMES in StarterRng's attempts). -inline constexpr uint64_t STARTER_RNG_CONTINUE_SCREEN_FRAMES = 200; -// Starter RNG's in-game wait for `advances`, before calibration. It -// refuses one under 7,500 ms, which STARTER_RNG_MIN_ADVANCES just makes. -int64_t starter_rng_ingame_ms(uint64_t advances); -// Room for the in-game calibration, in advances (2 per frame of wait): the -// calibration moves the wait, and it went to -720 after one miss on console -// B (2026-11-02, seed B579 at 967 advances: an 8 s wait cut to 1.7 s). The -// plan's targets are at least this many advances past Starter RNG's -// minimum. To tune from the logs' calibrations. -inline constexpr uint64_t STARTER_RNG_INGAME_MARGIN_ADVANCES = 720; -inline constexpr uint64_t STARTER_RNG_PLAN_MIN_ADVANCES = STARTER_RNG_MIN_ADVANCES + STARTER_RNG_INGAME_MARGIN_ADVANCES; - -// The attempts for Starter RNG, from the seed list of the version and -// language in Help button mode, the one Starter RNG uses. -std::vector plan_starter_rng(const StarterRngPlanInputs& inputs); - -// One line per attempt, for the log and the stop message: the Target Seed -// and Advances to give Starter RNG, the Pokemon, and the SIDs it is shiny for. -std::vector rng_attempt_lines(const std::vector& attempts); - - -// The data to rebuild how likely SID Helper's k-th candidate of N is (the -// 1-2-3-2-1 weights of sid_helper_candidates() are a guess), from the logs -// alone, and to follow AutoStory's Starter RNG loop. Every line starts with -// "RngPlan:"; grep for it. In the order a run logs them: -// RngPlan: tid= source= -// RngPlan: sid_helper tid= target_advances= candidates=,... -// RngPlan: attempt= seed= advances= sids=,... -// seed_wait_ms= ingame_ms= (Starter RNG's waits, before calibration) -// then, with Run Starter RNG in AutoStory, for each try of an attempt -// (rng_log_state() in PokemonFRLG_AutoStoryRngLoop.h): -// RngPlan: state attempt=/ try=/ reset=/ seed= -// advances= sids=,... ruled_out=,... attempt_time=/ -// total_time=/ calibration=// -// (once at the try's start, then after every reset) -// RngPlan: result attempt= try= outcome= -// starter_rng= nature= ivs= -// pid= next= -// RngPlan: confirmed tid= sid= candidate= of= -// Lists that are empty print "-". -// "candidates" are in SID Helper's order; "candidate=k" counts from 1 in -// it, 0 when the SID wasn't among them. Count each TID's "confirmed" once: -// every later run with the SID set logs it again. While the user runs -// Starter RNG by hand, its own "Target Hit!" and "Missed target." lines -// are the attempts' results. -std::string rng_log_tid(uint16_t tid, bool from_card); -std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std::vector& candidates); -std::string rng_log_attempt(size_t index, const RngAttempt& attempt); -// What the loop does after an attempt's result. -enum class RngPlanNext{ - RETRY, - NEXT_ATTEMPT, - // Shiny and the target: go on with the story. - CONFIRM, - STOP, -}; -// The step after `index` came out as the tracker's `step` and its attempt -// is now `next_index`. -RngPlanNext rng_plan_next(RngPlanStep step, size_t index, size_t next_index); - -struct RngResultLog{ - size_t index = 0; - // From 1. - size_t try_number = 1; - RngAttemptOutcome outcome = RngAttemptOutcome::MISSED; - // How the RNG program put it ("out_of_resets"); empty for none. - std::string detail; - // The Pokemon hit, when known. - std::optional pokemon; - RngPlanNext next = RngPlanNext::STOP; -}; -std::string rng_log_result(const RngResultLog& result); -std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates); - - -void add_tests_AutoStoryRngPlan(UnitTestDatabase& database); - - - -} -} -} -#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp deleted file mode 100644 index 6bc702c9a0..0000000000 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp +++ /dev/null @@ -1,664 +0,0 @@ -/* AutoStory RNG Plan Tests - * - * From: https://github.com/PokemonAutomation/ - * - * The attempt order, and the step after each attempt's outcome, on made-up - * targets; SID Helper's candidates and a Starter RNG plan from the FireRed - * seed list. - * - */ - -#include -#include -#include -#include "Common/Cpp/Exceptions.h" -#include "Common/Cpp/Logging/AbstractLogger.h" -#include "Common/Cpp/TestRunners/UnitTest.h" -#include "PokemonFRLG_AutoStoryRngPlan.h" - -namespace PokemonAutomation{ -namespace NintendoSwitch{ -namespace PokemonFRLG{ - -using namespace Pokemon; - - -namespace{ - -class Test_AutoStoryRngPlan : public UnitTest{ -public: - Test_AutoStoryRngPlan(std::string name, std::function body) - : UnitTest("PokemonFRLG::AutoStoryRngPlan - " + 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; -}; - - -std::string join(const std::vector& values){ - std::string text; - for (uint16_t value : values){ - text += (text.empty() ? "" : ",") + std::to_string(value); - } - return "{" + text + "}"; -} - - -// SIDs 100, 200 and 300 weigh 3, 2 and 1. -const std::vector CANDIDATES = {{100, 3}, {200, 2}, {300, 1}}; - -// A target reached "seconds" after the reset, shiny for the candidates at -// "shiny" (indices into CANDIDATES). -RngTarget make_target(uint32_t state, uint64_t seconds, std::vector shiny){ - RngTarget target{}; - target.state = state; - target.seed = {uint16_t(state), seconds * 1000, SeedButton::A, BlackoutButton::None}; - target.advances = 0; - target.pokemon = method1_pokemon(state); - for (size_t candidate : shiny){ - target.shiny_for.push_back({candidate, AdvShinyType::Star}); - } - return target; -} - -// By expected wait per shiny over all three (weight 6): 1 = 50 s / (5/6) = 60, -// 2 = 20 s / (1/6) = 120, 3 = 40 s / (3/6) = 80. Then over SID 300 alone, 2 is -// the only one shiny for it. 4 is the quickest and shiny for none. -const std::vector TARGETS = { - make_target(1, 50, {0, 1}), - make_target(2, 20, {2}), - make_target(3, 40, {0}), - make_target(4, 5, {}), -}; - - -UnitTestResult test_sid_helper_candidates(Logger&){ - const uint16_t tid = 43771; - const uint32_t target_advances = 3001; - const std::vector candidates = sid_helper_candidates(tid, target_advances, 5); - - // The SID is the top half of the state after the TID seed is advanced - // once, then "advances" more times. - auto sid_at = [&](uint32_t advances){ - uint32_t state = tid; - for (uint32_t c = 0; c <= advances; c++){ - state = lcrng_next(state); - } - return uint16_t(state >> 16); - }; - const std::vector advances = {2997, 2999, 3001, 3003, 3005}; - const std::vector weights = {1, 2, 3, 2, 1}; - if (candidates.size() != advances.size()){ - return std::format("Expected {} candidates, got {}.", advances.size(), candidates.size()); - } - for (size_t c = 0; c < candidates.size(); c++){ - if (candidates[c].sid != sid_at(advances[c]) || candidates[c].weight != weights[c]){ - return std::format( - "Candidate {}: expected SID {} weight {}, got SID {} weight {}.", - c, sid_at(advances[c]), weights[c], candidates[c].sid, candidates[c].weight - ); - } - } - - const std::vector even = sid_helper_candidates(tid, target_advances, 4); - const std::vector even_weights = {1, 2, 3, 2}; - for (size_t c = 0; c < even.size() && c < even_weights.size(); c++){ - if (even.size() != even_weights.size() || even[c].sid != candidates[c].sid || even[c].weight != even_weights[c]){ - return std::format("Four candidates: expected the first four of five, weighing 1, 2, 3, 2; candidate {} differs.", c); - } - } - return true; -} - - -UnitTestResult test_parse_trainer_id(Logger&){ - struct Case{ - std::string text; - std::string expected; - }; - const Case cases[] = { - {.text = "64694", .expected = "64694"}, - {.text = " 0 ", .expected = "0"}, - {.text = "65535", .expected = "65535"}, - {.text = "65536", .expected = "none"}, - {.text = "", .expected = "none"}, - {.text = "-1", .expected = "none"}, - {.text = "12a", .expected = "none"}, - }; - for (const Case& item : cases){ - const std::optional sid = parse_trainer_id(item.text); - const std::string got = sid ? std::to_string(*sid) : "none"; - if (got != item.expected){ - return std::format("SID \"{}\": expected {}, got {}.", item.text, item.expected, got); - } - } - return true; -} - -UnitTestResult test_parse_ivs(Logger&){ - struct Case{ - std::string text; - std::string expected; - }; - const Case cases[] = { - {.text = "31/14/7/29/1/27", .expected = "31/14/7/29/1/27"}, - {.text = " 0 / 0/0/0/0/31 ", .expected = "0/0/0/0/0/31"}, - {.text = "31/0/0", .expected = "none"}, - {.text = "32/0/0/0/0/0", .expected = "none"}, - {.text = "a/0/0/0/0/0", .expected = "none"}, - {.text = "-1/0/0/0/0/0", .expected = "none"}, - {.text = "", .expected = "none"}, - }; - for (const Case& item : cases){ - const std::optional ivs = parse_ivs(item.text); - const std::string got = ivs ? ivs_to_string(*ivs) : "none"; - if (got != item.expected){ - return std::format("IVs \"{}\": expected {}, got {}.", item.text, item.expected, got); - } - } - return true; -} - -UnitTestResult test_parse_natures(Logger&){ - struct Case{ - std::string text; - std::string expected; - }; - const Case cases[] = { - {.text = "", .expected = "any"}, - {.text = " ", .expected = "any"}, - {.text = "Modest", .expected = "Modest"}, - {.text = "Modest, Timid", .expected = "Modest Timid"}, - {.text = "Modset", .expected = "none"}, - {.text = "Modest,", .expected = "none"}, - }; - for (const Case& item : cases){ - const std::optional> natures = parse_natures(item.text); - std::string got; - if (!natures){ - got = "none"; - }else if (natures->empty()){ - got = "any"; - }else{ - for (AdvNature nature : *natures){ - got += (got.empty() ? "" : " ") + nature_to_string(nature); - } - } - if (got != item.expected){ - return std::format("Natures \"{}\": expected {}, got {}.", item.text, item.expected, got); - } - } - return true; -} - - -struct PlanCase{ - std::string name; - std::vector targets; - bool shiny_required; - size_t max_attempts; - std::vector states; - std::vector> sids; -}; -std::vector plan_cases(){ - const std::vector no_300 = {TARGETS[0], TARGETS[2], TARGETS[3]}; - return { - { - .name = "two candidates in one target first, then the last", - .targets = TARGETS, .shiny_required = true, .max_attempts = 5, - .states = {1, 2}, .sids = {{100, 200}, {300}}, - }, - { - .name = "capped at one attempt", - .targets = TARGETS, .shiny_required = true, .max_attempts = 1, - .states = {1}, .sids = {{100, 200}}, - }, - { - .name = "no target shiny for SID 300", - .targets = no_300, .shiny_required = true, .max_attempts = 5, - .states = {1}, .sids = {{100, 200}}, - }, - { - .name = "shiny not required: the quickest", - .targets = TARGETS, .shiny_required = false, .max_attempts = 5, - .states = {4}, .sids = {{}}, - }, - { - .name = "no targets", - .targets = {}, .shiny_required = true, .max_attempts = 5, - .states = {}, .sids = {}, - }, - }; -} - -UnitTestResult test_plan_order(Logger&){ - for (const PlanCase& item : plan_cases()){ - const std::vector attempts = plan_rng_attempts(item.targets, CANDIDATES, item.shiny_required, item.max_attempts); - std::vector states; - std::vector> sids; - for (const RngAttempt& attempt : attempts){ - states.push_back(attempt.target.state); - sids.push_back(attempt.sids); - } - if (states != item.states || sids != item.sids){ - std::string got; - for (size_t c = 0; c < attempts.size(); c++){ - got += std::format(" target {} {}", states[c], join(sids[c])); - } - return item.name + ": got" + (got.empty() ? " no attempts" : got) + "."; - } - } - return true; -} - - -UnitTestResult test_classify(Logger&){ - const RngTarget hit = TARGETS[0]; - AdvIVs other_ivs = hit.pokemon.ivs; - other_ivs.speed = uint8_t((other_ivs.speed + 1) % 32); - const AdvNature other_nature = hit.pokemon.nature == AdvNature::Hardy ? AdvNature::Lonely : AdvNature::Hardy; - - struct Case{ - std::string name; - RngAttemptResult result; - RngAttemptOutcome expected; - }; - const Case cases[] = { - {.name = "shiny, the target", .result = {.shiny = true, .nature = hit.pokemon.nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::SHINY}, - {.name = "shiny, not the target", .result = {.shiny = true, .nature = other_nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::SHINY_OFF_TARGET}, - {.name = "same nature and IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::HIT_NOT_SHINY}, - {.name = "other IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::MISSED}, - {.name = "other nature", .result = {.shiny = false, .nature = other_nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::MISSED}, - }; - for (const Case& item : cases){ - if (classify_attempt(hit, item.result) != item.expected){ - return item.name + ": classified wrong."; - } - } - return true; -} - - -const char* step_name(RngPlanStep step){ - switch (step){ - case RngPlanStep::RUN_ATTEMPT: return "RUN_ATTEMPT"; - case RngPlanStep::GOT_SHINY: return "GOT_SHINY"; - case RngPlanStep::GOT_SHINY_OFF_TARGET: return "GOT_SHINY_OFF_TARGET"; - case RngPlanStep::GOT_TARGET: return "GOT_TARGET"; - case RngPlanStep::OUT_OF_ATTEMPTS: return "OUT_OF_ATTEMPTS"; - case RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT: return "MISSED_EVERY_ATTEMPT_LEFT"; - } - return "?"; -} - -struct TrackerCase{ - std::string name; - bool shiny_required; - std::vector outcomes; - // The step after each outcome, and the attempt run next. - std::vector steps; - std::vector attempts; - std::vector remaining; - std::optional confirmed; -}; -std::vector tracker_cases(){ - using enum RngAttemptOutcome; - using enum RngPlanStep; - return { - { - .name = "shiny at once: one of two SIDs", - .shiny_required = true, .outcomes = {SHINY}, - .steps = {GOT_SHINY}, .attempts = {0}, - .remaining = {100, 200}, .confirmed = std::nullopt, - }, - { - .name = "hit, then shiny: SID confirmed", - .shiny_required = true, .outcomes = {HIT_NOT_SHINY, SHINY}, - .steps = {RUN_ATTEMPT, GOT_SHINY}, .attempts = {1, 1}, - .remaining = {300}, .confirmed = uint16_t(300), - }, - { - .name = "a miss retries the same target", - .shiny_required = true, .outcomes = {MISSED, MISSED, HIT_NOT_SHINY, MISSED}, - .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT}, .attempts = {0, 0, 1, 1}, - .remaining = {300}, .confirmed = std::nullopt, - }, - }; -} -// Misses: retried, then the next attempt, mixed with hits and shinies. -std::vector tracker_retry_cases(){ - using enum RngAttemptOutcome; - using enum RngPlanStep; - return { - { - .name = "retries used up, then the next attempt", - .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, SHINY}, - .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 0, 1, 1}, - .remaining = {300}, .confirmed = uint16_t(300), - }, - { - .name = "every attempt left missed too often", - .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, MISSED, MISSED, MISSED}, - .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, MISSED_EVERY_ATTEMPT_LEFT}, - .attempts = {0, 0, 1, 1, 1, 1}, - .remaining = {100, 200, 300}, .confirmed = std::nullopt, - }, - { - .name = "shiny off the target confirms nothing", - .shiny_required = true, .outcomes = {HIT_NOT_SHINY, SHINY_OFF_TARGET, SHINY}, - .steps = {RUN_ATTEMPT, GOT_SHINY_OFF_TARGET, GOT_SHINY_OFF_TARGET}, .attempts = {1, 1, 1}, - .remaining = {300}, .confirmed = std::nullopt, - }, - }; -} -// Attempts that can't run. -std::vector tracker_unusable_cases(){ - using enum RngAttemptOutcome; - using enum RngPlanStep; - return { - { - .name = "an attempt that can't run moves on", - .shiny_required = true, .outcomes = {MISSED, UNUSABLE, SHINY}, - .steps = {RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 1, 1}, - .remaining = {300}, .confirmed = uint16_t(300), - }, - { - .name = "the last attempt can't run", - .shiny_required = true, .outcomes = {UNUSABLE, UNUSABLE}, - .steps = {RUN_ATTEMPT, MISSED_EVERY_ATTEMPT_LEFT}, .attempts = {1, 1}, - .remaining = {100, 200, 300}, .confirmed = std::nullopt, - }, - }; -} -std::vector tracker_mixed_cases(){ - using enum RngAttemptOutcome; - using enum RngPlanStep; - return { - { - .name = "miss, hit, misses, then shiny", - .shiny_required = true, .outcomes = {MISSED, HIT_NOT_SHINY, MISSED, MISSED, SHINY}, - .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 1, 1, 1, 1}, - .remaining = {300}, .confirmed = uint16_t(300), - }, - { - .name = "misses move on, then a hit ends the plan", - .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, MISSED, HIT_NOT_SHINY}, - .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, OUT_OF_ATTEMPTS}, .attempts = {0, 0, 1, 1, 1}, - .remaining = {100, 200}, .confirmed = std::nullopt, - }, - { - .name = "every attempt hit, none shiny", - .shiny_required = true, .outcomes = {HIT_NOT_SHINY, HIT_NOT_SHINY}, - .steps = {RUN_ATTEMPT, OUT_OF_ATTEMPTS}, .attempts = {1, 1}, - .remaining = {}, .confirmed = std::nullopt, - }, - { - .name = "shiny not required: a hit ends it", - .shiny_required = false, .outcomes = {HIT_NOT_SHINY}, - .steps = {GOT_TARGET}, .attempts = {0}, - .remaining = {100, 200, 300}, .confirmed = std::nullopt, - }, - }; -} - -UnitTestResult check_tracker_case(const TrackerCase& item){ - RngPlanTracker tracker(plan_rng_attempts(TARGETS, CANDIDATES, true, 5), CANDIDATES, item.shiny_required, 2); - if (tracker.step() != RngPlanStep::RUN_ATTEMPT){ - return item.name + ": expected to start with RUN_ATTEMPT, got " + step_name(tracker.step()) + "."; - } - for (size_t c = 0; c < item.outcomes.size(); c++){ - const RngPlanStep step = tracker.record(item.outcomes[c]); - if (step != item.steps[c] || tracker.attempt_index() != item.attempts[c]){ - return std::format( - "{}: after outcome {}, expected {} at attempt {}, got {} at attempt {}.", - item.name, c, step_name(item.steps[c]), item.attempts[c], step_name(step), tracker.attempt_index() - ); - } - } - if (tracker.remaining_sids() != item.remaining || tracker.confirmed_sid() != item.confirmed){ - return std::format( - "{}: expected SIDs {} (confirmed {}), got {} (confirmed {}).", - item.name, join(item.remaining), item.confirmed ? std::to_string(*item.confirmed) : "none", - join(tracker.remaining_sids()), tracker.confirmed_sid() ? std::to_string(*tracker.confirmed_sid()) : "none" - ); - } - return true; -} - -UnitTestResult test_tracker(Logger&){ - std::vector cases = tracker_cases(); - for (auto* more : {tracker_retry_cases, tracker_unusable_cases, tracker_mixed_cases}){ - for (TrackerCase& item : more()){ - cases.push_back(std::move(item)); - } - } - for (const TrackerCase& item : cases){ - const UnitTestResult result = check_tracker_case(item); - if (result.result != UnitTestResult::PASSED){ - return result; - } - } - RngPlanTracker empty({}, CANDIDATES, true, 2); - if (empty.step() != RngPlanStep::OUT_OF_ATTEMPTS){ - return std::string("An empty plan: expected OUT_OF_ATTEMPTS, got ") + step_name(empty.step()) + "."; - } - // A shiny hit for a SID that is no candidate: none left, none confirmed. - RngPlanTracker outside({RngAttempt{make_target(1, 50, {}), {999}}}, CANDIDATES, true, 2); - outside.record(RngAttemptOutcome::SHINY); - if (outside.step() != RngPlanStep::GOT_SHINY || !outside.remaining_sids().empty() || outside.confirmed_sid()){ - return std::format( - "Shiny for no candidate: expected GOT_SHINY with no SID left, got {} with SIDs {}.", - step_name(outside.step()), join(outside.remaining_sids()) - ); - } - return true; -} - - -// The "RngPlan:" lines the candidate odds are rebuilt from. -UnitTestResult test_log_lines(Logger&){ - RngAttempt attempt{make_target(1, 50, {0, 1}), {100, 200}}; - attempt.target.seed.seed = 0x2ef6; - attempt.target.advances = 29364; - struct Case{ - std::string got; - std::string expected; - }; - const Case cases[] = { - {.got = rng_log_tid(43771, true), .expected = "RngPlan: tid=43771 source=card"}, - {.got = rng_log_tid(43771, false), .expected = "RngPlan: tid=43771 source=option"}, - {.got = rng_log_sid_helper(43771, 3001, CANDIDATES), .expected = "RngPlan: sid_helper tid=43771 target_advances=3001 candidates=100,200,300"}, - {.got = rng_log_attempt(0, attempt), .expected = std::format( - "RngPlan: attempt=1 seed=2EF6 advances=29364 sids=100,200 seed_wait_ms=49155 ingame_ms={}", starter_rng_ingame_ms(29364) - )}, - { - .got = rng_log_result(RngResultLog{ - .index = 1, .try_number = 2, .outcome = RngAttemptOutcome::HIT_NOT_SHINY, .detail = "target_hit", - .pokemon = attempt.target.pokemon, .next = RngPlanNext::NEXT_ATTEMPT, - }), - .expected = std::format( - "RngPlan: result attempt=2 try=2 outcome=hit_not_shiny starter_rng=target_hit nature={} ivs={} pid={:08X} next=next_attempt", - nature_to_string(attempt.target.pokemon.nature), ivs_to_string(attempt.target.pokemon.ivs), attempt.target.pokemon.pid - ), - }, - { - .got = rng_log_result(RngResultLog{ - .index = 0, .try_number = 1, .outcome = RngAttemptOutcome::MISSED, .detail = "out_of_resets", - .pokemon = std::nullopt, .next = RngPlanNext::RETRY, - }), - .expected = "RngPlan: result attempt=1 try=1 outcome=missed starter_rng=out_of_resets nature=- ivs=- pid=- next=retry", - }, - { - .got = rng_log_result(RngResultLog{ - .index = 2, .try_number = 3, .outcome = RngAttemptOutcome::SHINY_OFF_TARGET, .detail = "shiny_off_target", - .pokemon = std::nullopt, .next = RngPlanNext::STOP, - }), - .expected = "RngPlan: result attempt=3 try=3 outcome=shiny_off_target starter_rng=shiny_off_target nature=- ivs=- pid=- next=stop", - }, - {.got = rng_log_confirmed(43771, 300, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=300 candidate=3 of=3"}, - {.got = rng_log_confirmed(43771, 7, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=7 candidate=0 of=3"}, - }; - for (const Case& item : cases){ - if (item.got != item.expected){ - return "Expected \"" + item.expected + "\", got \"" + item.got + "\"."; - } - } - return true; -} - - -// A plan from the FireRed (English) list: every attempt starts from a seed -// Starter RNG finds, takes at least its smallest Advances, and is shiny for -// the SIDs it lists, which no earlier attempt listed. -// Console B, 2026-11-02, FireRed English, Mono, Help: Starter RNG refused -// both of these as title screen waits under 30 s. AutoStory's plan for TID -// 55740 started with 11A3 at 945 advances (29,628 ms after Starter RNG's -// offsets); StarterRng by hand with 405C at 990 advances listed 30,488 ms -// from the table, 29,643 ms after them. Neither seed may start an attempt. -UnitTestResult test_starter_plan_seed_delay(Logger&){ - struct Case{ - uint16_t seed; - uint64_t table_ms; - }; - const Case cases[] = { - {.seed = 0x11a3, .table_ms = 30473}, - {.seed = 0x405c, .table_ms = 30488}, - }; - const SeedsDatabase database(seeds_json_path(true, Language::English)); - const std::vector seeds = initial_seed_entries(database, SoundSetting::Mono, "Help", true); - for (const Case& item : cases){ - auto entry = std::find_if(seeds.begin(), seeds.end(), [&](const InitialSeedEntry& seed){ return seed.seed == item.seed; }); - if (entry == seeds.end() || entry->seed_time_ms != item.table_ms){ - return std::format("Expected seed {:04X} at {} ms in the FireRed English list (Mono, Help).", item.seed, item.table_ms); - } - if (starter_rng_can_wait_for(*entry)){ - return std::format("Seed {:04X} ({} ms): expected too early for Starter RNG's title screen.", item.seed, item.table_ms); - } - } - - const uint16_t tid = 55740; - const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ - .firered = true, - .language = Language::English, - .sound = SoundSetting::Mono, - .tid = tid, - .candidates = sid_helper_candidates(tid, 3001, 5), - .filters = {}, - .max_attempts = 5, - }); - if (attempts.empty()){ - return "Expected attempts for TID 55740, got none."; - } - for (size_t c = 0; c < attempts.size(); c++){ - const InitialSeedEntry& seed = attempts[c].target.seed; - if (seed.seed == 0x11a3 || seed.seed == 0x405c || starter_rng_seed_wait_ms(seed) < 31500){ - return std::format("Attempt {}: seed {:04X} at {} ms is too early for the title screen.", c + 1, seed.seed, seed.seed_time_ms); - } - if (attempts[c].target.advances < 1660){ - return std::format("Attempt {}: {} advances leave no room for the in-game calibration.", c + 1, attempts[c].target.advances); - } - } - return true; -} - -// Starter RNG's in-game wait: 7,500 ms at its 940-advance minimum; console -// B's B579 target at 967 advances was 7,725 ms, which one miss's -// calibration (-720) cut under the minimum. Targets start 720 advances on. -UnitTestResult test_starter_ingame_wait(Logger&){ - struct Case{ - uint64_t advances; - int64_t ms; - }; - const Case cases[] = { - {.advances = 940, .ms = 7500}, - {.advances = 967, .ms = 7725}, - {.advances = 1660, .ms = 13500}, - }; - for (const Case& item : cases){ - if (starter_rng_ingame_ms(item.advances) != item.ms){ - return std::format("{} advances: expected {} ms in game, got {}.", item.advances, item.ms, starter_rng_ingame_ms(item.advances)); - } - } - if (STARTER_RNG_PLAN_MIN_ADVANCES != 1660){ - return std::format("Expected the plan's minimum at 1660 advances, got {}.", STARTER_RNG_PLAN_MIN_ADVANCES); - } - return true; -} - -UnitTestResult test_starter_plan(Logger&){ - const uint16_t tid = 43771; - StarterRngPlanInputs inputs{ - .firered = true, - .language = Language::English, - .sound = SoundSetting::Mono, - .tid = tid, - .candidates = sid_helper_candidates(tid, 3001, 5), - .filters = {}, - .max_attempts = 5, - }; - const std::vector attempts = plan_starter_rng(inputs); - if (attempts.empty()){ - return "Expected attempts for TID 43771 with five SID candidates, got none."; - } - - const SeedsDatabase database(seeds_json_path(true, Language::English)); - std::vector listed; - for (size_t c = 0; c < attempts.size(); c++){ - const RngAttempt& attempt = attempts[c]; - if (!database.find_seed(attempt.target.seed.seed, SoundSetting::Mono, 5).found){ - return std::format("Attempt {}: Starter RNG can't find Target Seed {:04X}.", c + 1, attempt.target.seed.seed); - } - if (attempt.target.advances < STARTER_RNG_PLAN_MIN_ADVANCES){ - return std::format("Attempt {}: Advances {} below the plan's {}.", c + 1, attempt.target.advances, STARTER_RNG_PLAN_MIN_ADVANCES); - } - if (attempt.sids.empty()){ - return std::format("Attempt {}: shiny for no SID candidate.", c + 1); - } - for (uint16_t sid : attempt.sids){ - if (shiny_type(attempt.target.pokemon.pid, tid, sid) == AdvShinyType::Normal){ - return std::format("Attempt {}: PID {:08X} not shiny for SID {}.", c + 1, attempt.target.pokemon.pid, sid); - } - if (std::find(listed.begin(), listed.end(), sid) != listed.end()){ - return std::format("Attempt {}: SID {} already listed by an earlier attempt.", c + 1, sid); - } - listed.push_back(sid); - } - } - if (rng_attempt_lines(attempts).size() != attempts.size()){ - return "Expected one line per attempt."; - } - return true; -} - - -} - - -void add_tests_AutoStoryRngPlan(UnitTestDatabase& database){ - database.add("SID Helper's candidates and weights", test_sid_helper_candidates); - database.add("TID and SID text", test_parse_trainer_id); - database.add("IVs text", test_parse_ivs); - database.add("natures text", test_parse_natures); - database.add("attempt order", test_plan_order); - database.add("attempt outcome", test_classify); - database.add("step after each outcome", test_tracker); - database.add("Starter RNG plan from the FireRed list", test_starter_plan); - database.add("Starter RNG plan: no seed too early for the title screen", test_starter_plan_seed_delay); - database.add("Starter RNG plan: room for the in-game calibration", test_starter_ingame_wait); - database.add("RngPlan log lines", test_log_lines); -} - - - -} -} -} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.cpp new file mode 100644 index 0000000000..b462362ec9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.cpp @@ -0,0 +1,159 @@ +/* RNG Plan + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_BlindNavigation.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.h" +#include "PokemonFRLG_RngPlan.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +std::vector sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count){ + // As SidHelper's get_sid_messages(): from "count / 2" steps before the target. + const int start = -int(count) / 2; + const int end = start + int(count); + const int farthest = std::max(-start, end - 1); + + std::vector candidates; + AdvRngSearcher searcher(tid, uint64_t(int64_t(target_advances) + 2 * start)); + for (int i = start; i < end; i++){ + candidates.push_back({uint16_t(searcher.state.s0 >> 16), double(farthest + 1 - std::abs(i))}); + searcher.advance_state(); + searcher.advance_state(); + } + return candidates; +} + + +namespace{ + +// "targets" with only the shiny candidates still "left", and their share of +// the weight left. +std::vector rescore( + const std::vector& targets, + const std::vector& candidates, + const std::vector& left +){ + double total = 0; + for (size_t c = 0; c < candidates.size(); c++){ + if (left[c]){ + total += candidates[c].weight; + } + } + std::vector scored; + scored.reserve(targets.size()); + for (const RngTarget& target : targets){ + RngTarget copy = target; + copy.shiny_for.clear(); + double weight = 0; + for (const ShinyCandidate& shiny : target.shiny_for){ + if (left[shiny.candidate]){ + copy.shiny_for.push_back(shiny); + weight += candidates[shiny.candidate].weight; + } + } + copy.shiny_probability = total > 0 ? weight / total : 0; + scored.emplace_back(std::move(copy)); + } + return scored; +} + +RngAttempt make_attempt(const RngTarget& target, const std::vector& candidates){ + RngAttempt attempt{target, {}}; + for (const ShinyCandidate& shiny : target.shiny_for){ + attempt.sids.push_back(candidates[shiny.candidate].sid); + } + return attempt; +} + +} + + +std::vector plan_rng_attempts( + const std::vector& targets, + const std::vector& candidates, + bool shiny_required, + size_t max_attempts +){ + std::vector attempts; + if (targets.empty() || max_attempts == 0){ + return attempts; + } + std::vector left(candidates.size(), true); + + if (!shiny_required){ + // With no shiny chance, rank_rng_targets() sorts by time alone. + std::vector by_time = targets; + for (RngTarget& target : by_time){ + target.shiny_probability = 0; + } + rank_rng_targets(by_time); + const uint32_t quickest = by_time.front().state; + for (const RngTarget& target : rescore(targets, candidates, left)){ + if (target.state == quickest){ + attempts.push_back(make_attempt(target, candidates)); + break; + } + } + return attempts; + } + + while (attempts.size() < max_attempts){ + std::vector scored = rescore(targets, candidates, left); + rank_rng_targets(scored); + const RngTarget& best = scored.front(); + if (best.shiny_for.empty()){ + break; + } + for (const ShinyCandidate& shiny : best.shiny_for){ + left[shiny.candidate] = false; + } + attempts.push_back(make_attempt(best, candidates)); + if (std::find(left.begin(), left.end(), true) == left.end()){ + break; + } + } + return attempts; +} + + +int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed){ + return int64_t(seed.seed_time_ms) + STARTER_RNG_FIXED_SEED_OFFSET_MS; +} + +bool starter_rng_can_wait_for(const InitialSeedEntry& seed){ + return starter_rng_seed_wait_ms(seed) >= STARTER_RNG_MIN_SEED_WAIT_MS; +} + +int64_t starter_rng_ingame_ms(uint64_t advances){ + const int64_t frames = int64_t(advances) - int64_t(STARTER_RNG_CONTINUE_SCREEN_FRAMES) + + fixed_advances_offset(PokemonFRLG_RngTarget::starters); + return int64_t(frames * FRLG_FRAME_DURATION / 2); +} + +std::vector plan_starter_rng(const StarterRngPlanInputs& inputs){ + const SeedsDatabase database(seeds_json_path(inputs.firered, inputs.language)); + // Only the English lists hold the blackout button's seeds (initial_seed_entries()). + const bool english_list = inputs.language != Language::Japanese; + std::vector seeds = initial_seed_entries(database, inputs.sound, "Help", english_list); + std::erase_if(seeds, [](const InitialSeedEntry& seed){ return !starter_rng_can_wait_for(seed); }); + + RngTargetFilters filters = inputs.filters; + filters.min_advances = std::max(filters.min_advances, STARTER_RNG_PLAN_MIN_ADVANCES); + const std::vector targets = search_rng_targets(seeds, inputs.tid, inputs.candidates, filters); + return plan_rng_attempts(targets, inputs.candidates, filters.shiny_required, inputs.max_attempts); +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h new file mode 100644 index 0000000000..3cc5276748 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h @@ -0,0 +1,121 @@ +/* RNG Plan + * + * From: https://github.com/PokemonAutomation/ + * + * Which Starter RNG targets to ask for, in order, when the SID is + * only narrowed down to SID Helper's candidates, and what to do after each + * one. The loop is the one PokemonFRLG_RngTargetSearch.h describes: + * - Shiny, and the target was hit: the SID is one of those the target is + * shiny for. With one left, it is confirmed and later targets can be + * searched with it alone. + * - Shiny, but not the target (another advance): it says nothing about + * the SID. The plan ends with the shiny. + * - Nature and IVs match, but not shiny: the target was hit, so the SIDs it + * is shiny for are wrong. Go on to the next attempt. + * - Nature or IVs differ: the target was missed. Run it again, up to the + * retries, then go on to the next attempt. + * + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlan_H +#define PokemonAutomation_PokemonFRLG_RngPlan_H + +#include +#include +#include +#include "CommonFramework/Language.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The SIDs SID Helper lists for "tid": "count" of them, 2 advances apart, +// centered on "target_advances", as its get_sid_messages() lists them. The +// middle one, where SID Helper aims, weighs the most, and each step away one +// less, so the outermost weigh 1. +std::vector sid_helper_candidates(uint16_t tid, uint32_t target_advances, uint8_t count); + + +struct RngAttempt{ + RngTarget target; + // The SIDs the target is shiny for, of those the attempts before it + // leave. Empty when the target isn't shiny for any of them. + std::vector sids; +}; + +// The attempts to make, in order, for SID candidates "candidates" (the ones +// "targets" were searched with). Each is the best target by +// rank_rng_targets() over the candidates the attempts before it leave, so a +// target shiny for several candidates comes before one shiny for one, unless +// it takes much longer. Stops after "max_attempts", or once no target is +// shiny for a candidate left. +// +// With "shiny_required" off, a hit ends the plan whether it is shiny or not, +// so the plan is the one quickest target. +std::vector plan_rng_attempts( + const std::vector& targets, + const std::vector& candidates, + bool shiny_required, + size_t max_attempts +); + + +// What Starter RNG's targets are searched with. +struct StarterRngPlanInputs{ + bool firered; + Language language; + SoundSetting sound; + uint16_t tid; + std::vector candidates; + RngTargetFilters filters; + size_t max_attempts; +}; + +// Starter RNG's smallest Advances setting. +inline constexpr uint64_t STARTER_RNG_MIN_ADVANCES = 940; + +// Starter RNG's title screen wait, before calibration: the seed list's +// delay plus this (FIXED_SEED_OFFSET in StarterRng's attempts). +inline constexpr int64_t STARTER_RNG_FIXED_SEED_OFFSET_MS = -845; +// The shortest title screen wait the plan uses. Starter RNG refuses waits +// under 29,650 ms, and its docs (pokemonautomation.github.io, +// docs/Programs/PokemonFRLG/StarterRng.md, "Avoid low seed delays when +// possible") warn that waits near 30,500 ms can miss the title screen +// press. 31,500 ms keeps a second clear of that for the seed calibration. +inline constexpr int64_t STARTER_RNG_MIN_SEED_WAIT_MS = 31500; +// Starter RNG's title screen wait for `seed`, as prepare_timings() makes it +// before calibration. +int64_t starter_rng_seed_wait_ms(const InitialSeedEntry& seed); +// Whether the wait is at least STARTER_RNG_MIN_SEED_WAIT_MS. (Console B, +// 2026-11-02, FireRed English: seeds 11A3 and 405C, at 30,473 and +// 30,488 ms in the table, are 29,628 and 29,643 ms waits, which Starter +// RNG refuses.) +bool starter_rng_can_wait_for(const InitialSeedEntry& seed); + +// Starter RNG's Continue Screen frames, taken off the advances before the +// in-game wait (CONTINUE_SCREEN_FRAMES in StarterRng's attempts). +inline constexpr uint64_t STARTER_RNG_CONTINUE_SCREEN_FRAMES = 200; +// Starter RNG's in-game wait for `advances`, before calibration. It +// refuses one under 7,500 ms, which STARTER_RNG_MIN_ADVANCES just makes. +int64_t starter_rng_ingame_ms(uint64_t advances); +// Room for the in-game calibration, in advances (2 per frame of wait): the +// calibration moves the wait, and it went to -720 after one miss on console +// B (2026-11-02, seed B579 at 967 advances: an 8 s wait cut to 1.7 s). The +// plan's targets are at least this many advances past Starter RNG's +// minimum. To tune from the logs' calibrations. +inline constexpr uint64_t STARTER_RNG_INGAME_MARGIN_ADVANCES = 720; +inline constexpr uint64_t STARTER_RNG_PLAN_MIN_ADVANCES = STARTER_RNG_MIN_ADVANCES + STARTER_RNG_INGAME_MARGIN_ADVANCES; + +// The attempts for Starter RNG, from the seed list of the version and +// language in Help button mode, the one Starter RNG uses. +std::vector plan_starter_rng(const StarterRngPlanInputs& inputs); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp new file mode 100644 index 0000000000..9e0d1a087f --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp @@ -0,0 +1,106 @@ +/* RNG Plan Inputs + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include "PokemonFRLG_RngPlanInputs.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +std::string trim(const std::string& text){ + const size_t first = text.find_first_not_of(' '); + if (first == std::string::npos){ + return ""; + } + const size_t last = text.find_last_not_of(' '); + return text.substr(first, last - first + 1); +} + +std::vector split(const std::string& text, char separator){ + std::vector parts; + size_t start = 0; + while (true){ + const size_t next = text.find(separator, start); + parts.push_back(trim(text.substr(start, next == std::string::npos ? std::string::npos : next - start))); + if (next == std::string::npos){ + return parts; + } + start = next + 1; + } +} + +} + + +std::optional parse_trainer_id(const std::string& text){ + const std::string digits = trim(text); + if (digits.empty() || digits.size() > 5 || !std::all_of(digits.begin(), digits.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ + return std::nullopt; + } + const int value = std::stoi(digits); + if (value > 0xffff){ + return std::nullopt; + } + return uint16_t(value); +} + +std::optional parse_ivs(const std::string& text){ + const std::vector parts = split(text, '/'); + if (parts.size() != 6){ + return std::nullopt; + } + uint8_t values[6]; + for (size_t c = 0; c < 6; c++){ + const std::string& part = parts[c]; + if (part.empty() || part.size() > 2 || !std::all_of(part.begin(), part.end(), [](char ch){ return ch >= '0' && ch <= '9'; })){ + return std::nullopt; + } + const int value = std::stoi(part); + if (value > 31){ + return std::nullopt; + } + values[c] = uint8_t(value); + } + AdvIVs ivs; + ivs.hp = values[0]; + ivs.attack = values[1]; + ivs.defense = values[2]; + ivs.spatk = values[3]; + ivs.spdef = values[4]; + ivs.speed = values[5]; + return ivs; +} + +std::string ivs_to_string(const AdvIVs& ivs){ + return std::format("{}/{}/{}/{}/{}/{}", ivs.hp, ivs.attack, ivs.defense, ivs.spatk, ivs.spdef, ivs.speed); +} + +std::optional> parse_natures(const std::string& text){ + std::vector natures; + if (trim(text).empty()){ + return natures; + } + for (const std::string& name : split(text, ',')){ + const AdvNature nature = string_to_nature(name); + if (nature == AdvNature::Any){ + return std::nullopt; + } + natures.push_back(nature); + } + return natures; +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h new file mode 100644 index 0000000000..da7fe44525 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h @@ -0,0 +1,39 @@ +/* RNG Plan Inputs + * + * From: https://github.com/PokemonAutomation/ + * + * The TID, IVs and natures an RNG plan is typed in with, as text. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanInputs_H +#define PokemonAutomation_PokemonFRLG_RngPlanInputs_H + +#include +#include +#include +#include +#include "Pokemon/Pokemon_AdvRng.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// A TID or SID typed as a number from 0 to 65535. Nothing for any other text. +std::optional parse_trainer_id(const std::string& text); + +// "HP/Atk/Def/SpA/SpD/Spe", as Ten Lines and the RNG programs list IVs. +// Nothing for text that isn't six numbers from 0 to 31. +std::optional parse_ivs(const std::string& text); +std::string ivs_to_string(const Pokemon::AdvIVs& ivs); + +// Natures separated by commas ("Modest, Timid"). Empty text is any nature +// (no entries). Nothing for a name that isn't a nature. +std::optional> parse_natures(const std::string& text); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp new file mode 100644 index 0000000000..6cf8e40bde --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp @@ -0,0 +1,137 @@ +/* RNG Plan Inputs Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_RngPlanInputs.h" +#include "PokemonFRLG_RngPlanInputs_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +class Test_RngPlanInputs : public UnitTest{ +public: + Test_RngPlanInputs(std::string name, std::function body) + : UnitTest("PokemonFRLG::RngPlanInputs - " + 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; +}; + + +UnitTestResult test_parse_trainer_id(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "64694", .expected = "64694"}, + {.text = " 0 ", .expected = "0"}, + {.text = "65535", .expected = "65535"}, + {.text = "65536", .expected = "none"}, + {.text = "", .expected = "none"}, + {.text = "-1", .expected = "none"}, + {.text = "12a", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional sid = parse_trainer_id(item.text); + const std::string got = sid ? std::to_string(*sid) : "none"; + if (got != item.expected){ + return std::format("SID \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +UnitTestResult test_parse_ivs(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "31/14/7/29/1/27", .expected = "31/14/7/29/1/27"}, + {.text = " 0 / 0/0/0/0/31 ", .expected = "0/0/0/0/0/31"}, + {.text = "31/0/0", .expected = "none"}, + {.text = "32/0/0/0/0/0", .expected = "none"}, + {.text = "a/0/0/0/0/0", .expected = "none"}, + {.text = "-1/0/0/0/0/0", .expected = "none"}, + {.text = "", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional ivs = parse_ivs(item.text); + const std::string got = ivs ? ivs_to_string(*ivs) : "none"; + if (got != item.expected){ + return std::format("IVs \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +UnitTestResult test_parse_natures(Logger&){ + struct Case{ + std::string text; + std::string expected; + }; + const Case cases[] = { + {.text = "", .expected = "any"}, + {.text = " ", .expected = "any"}, + {.text = "Modest", .expected = "Modest"}, + {.text = "Modest, Timid", .expected = "Modest Timid"}, + {.text = "Modset", .expected = "none"}, + {.text = "Modest,", .expected = "none"}, + }; + for (const Case& item : cases){ + const std::optional> natures = parse_natures(item.text); + std::string got; + if (!natures){ + got = "none"; + }else if (natures->empty()){ + got = "any"; + }else{ + for (AdvNature nature : *natures){ + got += (got.empty() ? "" : " ") + nature_to_string(nature); + } + } + if (got != item.expected){ + return std::format("Natures \"{}\": expected {}, got {}.", item.text, item.expected, got); + } + } + return true; +} + +} + + +void add_tests_RngPlanInputs(UnitTestDatabase& database){ + database.add("TID and SID text", test_parse_trainer_id); + database.add("IVs text", test_parse_ivs); + database.add("natures text", test_parse_natures); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h new file mode 100644 index 0000000000..5da86a73d4 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h @@ -0,0 +1,23 @@ +/* RNG Plan Inputs Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanInputs_Tests_H +#define PokemonAutomation_PokemonFRLG_RngPlanInputs_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +void add_tests_RngPlanInputs(UnitTestDatabase& database); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.cpp new file mode 100644 index 0000000000..a10f229e73 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.cpp @@ -0,0 +1,137 @@ +/* RNG Plan Log + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h" +#include "PokemonFRLG_RngPlanLog.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace{ + +std::string join_sids(const std::vector& sids){ + std::string text; + for (uint16_t sid : sids){ + text += (text.empty() ? "" : ",") + std::to_string(sid); + } + return text; +} + +} + +std::string rng_log_tid(uint16_t tid, bool from_card){ + return std::format("RngPlan: tid={} source={}", tid, from_card ? "card" : "option"); +} + +std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std::vector& candidates){ + std::vector sids; + for (const SidCandidate& candidate : candidates){ + sids.push_back(candidate.sid); + } + return std::format("RngPlan: sid_helper tid={} target_advances={} candidates={}", tid, target_advances, join_sids(sids)); +} + +std::string rng_log_attempt(size_t index, const RngAttempt& attempt){ + return std::format( + "RngPlan: attempt={} seed={:04X} advances={} sids={} seed_wait_ms={} ingame_ms={}", + index + 1, attempt.target.seed.seed, attempt.target.advances, join_sids(attempt.sids), + starter_rng_seed_wait_ms(attempt.target.seed), starter_rng_ingame_ms(attempt.target.advances) + ); +} + +namespace{ + +const char* outcome_name(RngAttemptOutcome outcome){ + switch (outcome){ + case RngAttemptOutcome::SHINY: + return "shiny"; + case RngAttemptOutcome::SHINY_OFF_TARGET: + return "shiny_off_target"; + case RngAttemptOutcome::HIT_NOT_SHINY: + return "hit_not_shiny"; + case RngAttemptOutcome::MISSED: + return "missed"; + case RngAttemptOutcome::UNUSABLE: + return "unusable"; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngAttemptOutcome."); +} + +const char* next_name(RngPlanNext next){ + switch (next){ + case RngPlanNext::RETRY: + return "retry"; + case RngPlanNext::NEXT_ATTEMPT: + return "next_attempt"; + case RngPlanNext::CONFIRM: + return "confirm"; + case RngPlanNext::STOP: + return "stop"; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngPlanNext."); +} + +} + +std::string rng_log_result(const RngResultLog& result){ + const std::optional& pokemon = result.pokemon; + return std::format( + "RngPlan: result attempt={} try={} outcome={} starter_rng={} nature={} ivs={} pid={} next={}", + result.index + 1, result.try_number, outcome_name(result.outcome), + result.detail.empty() ? "-" : result.detail, + pokemon ? nature_to_string(pokemon->nature) : "-", + pokemon ? ivs_to_string(pokemon->ivs) : "-", + pokemon ? std::format("{:08X}", pokemon->pid) : "-", + next_name(result.next) + ); +} + +std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates){ + size_t k = 0; + for (size_t c = 0; c < sid_helper_candidates.size(); c++){ + if (sid_helper_candidates[c].sid == sid){ + k = c + 1; + break; + } + } + return std::format("RngPlan: confirmed tid={} sid={} candidate={} of={}", tid, sid, k, sid_helper_candidates.size()); +} + + +std::vector rng_attempt_lines(const std::vector& attempts){ + std::vector lines; + for (size_t c = 0; c < attempts.size(); c++){ + const RngTarget& target = attempts[c].target; + std::string shiny; + if (attempts[c].sids.empty()){ + shiny = "not shiny for any SID candidate"; + }else{ + shiny = "shiny for SID"; + for (size_t s = 0; s < attempts[c].sids.size(); s++){ + shiny += (s == 0 ? " " : ", ") + std::to_string(attempts[c].sids[s]); + } + shiny += std::format(" ({:.0f}% of the candidates left)", target.shiny_probability * 100); + } + lines.push_back(std::format( + "Attempt {}: Target Seed {:04X}, Advances {}: {}, IVs {}, PID {:08X}, {}.", + c + 1, target.seed.seed, target.advances, + nature_to_string(target.pokemon.nature), ivs_to_string(target.pokemon.ivs), target.pokemon.pid, + shiny + )); + } + return lines; +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h new file mode 100644 index 0000000000..f46954234b --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h @@ -0,0 +1,77 @@ +/* RNG Plan Log + * + * From: https://github.com/PokemonAutomation/ + * + * The lines an RNG plan logs and shows: its attempts, and the "RngPlan:" + * lines below. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanLog_H +#define PokemonAutomation_PokemonFRLG_RngPlanLog_H + +#include +#include +#include +#include +#include "Pokemon/Pokemon_AdvRng.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// One line per attempt, for the log and the stop message: the Target Seed +// and Advances to give Starter RNG, the Pokemon, and the SIDs it is shiny for. +std::vector rng_attempt_lines(const std::vector& attempts); + + +// The data to rebuild how likely SID Helper's k-th candidate of N is (the +// 1-2-3-2-1 weights of sid_helper_candidates() are a guess), from the logs +// alone, and to follow AutoStory's Starter RNG loop. Every line starts with +// "RngPlan:"; grep for it. In the order a run logs them: +// RngPlan: tid= source= +// RngPlan: sid_helper tid= target_advances= candidates=,... +// RngPlan: attempt= seed= advances= sids=,... +// seed_wait_ms= ingame_ms= (Starter RNG's waits, before calibration) +// then, with Run Starter RNG in AutoStory, for each try of an attempt +// (rng_log_state() in PokemonFRLG_StarterRngLoop.h): +// RngPlan: state attempt=/ try=/ reset=/ seed= +// advances= sids=,... ruled_out=,... attempt_time=/ +// total_time=/ calibration=// +// (once at the try's start, then after every reset) +// RngPlan: result attempt= try= outcome= +// starter_rng= nature= ivs= +// pid= next= +// RngPlan: confirmed tid= sid= candidate= of= +// Lists that are empty print "-". +// "candidates" are in SID Helper's order; "candidate=k" counts from 1 in +// it, 0 when the SID wasn't among them. Count each TID's "confirmed" once: +// every later run with the SID set logs it again. While the user runs +// Starter RNG by hand, its own "Target Hit!" and "Missed target." lines +// are the attempts' results. +std::string rng_log_tid(uint16_t tid, bool from_card); +std::string rng_log_sid_helper(uint16_t tid, uint32_t target_advances, const std::vector& candidates); +std::string rng_log_attempt(size_t index, const RngAttempt& attempt); + +struct RngResultLog{ + size_t index = 0; + // From 1. + size_t try_number = 1; + RngAttemptOutcome outcome = RngAttemptOutcome::MISSED; + // How the RNG program put it ("out_of_resets"); empty for none. + std::string detail; + // The Pokemon hit, when known. + std::optional pokemon; + RngPlanNext next = RngPlanNext::STOP; +}; +std::string rng_log_result(const RngResultLog& result); +std::string rng_log_confirmed(uint16_t tid, uint16_t sid, const std::vector& sid_helper_candidates); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.cpp new file mode 100644 index 0000000000..c69e725da9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.cpp @@ -0,0 +1,109 @@ +/* RNG Plan Log Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_RngPlanTestTargets.h" +#include "PokemonFRLG_RngPlanInputs.h" +#include "PokemonFRLG_RngPlanLog.h" +#include "PokemonFRLG_RngPlanLog_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; +using namespace RngPlanTestTargets; + + +namespace{ + +class Test_RngPlanLog : public UnitTest{ +public: + Test_RngPlanLog(std::string name, std::function body) + : UnitTest("PokemonFRLG::RngPlanLog - " + 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; +}; + + +// The "RngPlan:" lines the candidate odds are rebuilt from. +UnitTestResult test_log_lines(Logger&){ + RngAttempt attempt{make_target(1, 50, {0, 1}), {100, 200}}; + attempt.target.seed.seed = 0x2ef6; + attempt.target.advances = 29364; + struct Case{ + std::string got; + std::string expected; + }; + const Case cases[] = { + {.got = rng_log_tid(43771, true), .expected = "RngPlan: tid=43771 source=card"}, + {.got = rng_log_tid(43771, false), .expected = "RngPlan: tid=43771 source=option"}, + {.got = rng_log_sid_helper(43771, 3001, CANDIDATES), .expected = "RngPlan: sid_helper tid=43771 target_advances=3001 candidates=100,200,300"}, + {.got = rng_log_attempt(0, attempt), .expected = std::format( + "RngPlan: attempt=1 seed=2EF6 advances=29364 sids=100,200 seed_wait_ms=49155 ingame_ms={}", starter_rng_ingame_ms(29364) + )}, + { + .got = rng_log_result(RngResultLog{ + .index = 1, .try_number = 2, .outcome = RngAttemptOutcome::HIT_NOT_SHINY, .detail = "target_hit", + .pokemon = attempt.target.pokemon, .next = RngPlanNext::NEXT_ATTEMPT, + }), + .expected = std::format( + "RngPlan: result attempt=2 try=2 outcome=hit_not_shiny starter_rng=target_hit nature={} ivs={} pid={:08X} next=next_attempt", + nature_to_string(attempt.target.pokemon.nature), ivs_to_string(attempt.target.pokemon.ivs), attempt.target.pokemon.pid + ), + }, + { + .got = rng_log_result(RngResultLog{ + .index = 0, .try_number = 1, .outcome = RngAttemptOutcome::MISSED, .detail = "out_of_resets", + .pokemon = std::nullopt, .next = RngPlanNext::RETRY, + }), + .expected = "RngPlan: result attempt=1 try=1 outcome=missed starter_rng=out_of_resets nature=- ivs=- pid=- next=retry", + }, + { + .got = rng_log_result(RngResultLog{ + .index = 2, .try_number = 3, .outcome = RngAttemptOutcome::SHINY_OFF_TARGET, .detail = "shiny_off_target", + .pokemon = std::nullopt, .next = RngPlanNext::STOP, + }), + .expected = "RngPlan: result attempt=3 try=3 outcome=shiny_off_target starter_rng=shiny_off_target nature=- ivs=- pid=- next=stop", + }, + {.got = rng_log_confirmed(43771, 300, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=300 candidate=3 of=3"}, + {.got = rng_log_confirmed(43771, 7, CANDIDATES), .expected = "RngPlan: confirmed tid=43771 sid=7 candidate=0 of=3"}, + }; + for (const Case& item : cases){ + if (item.got != item.expected){ + return "Expected \"" + item.expected + "\", got \"" + item.got + "\"."; + } + } + return true; +} + +} + + +void add_tests_RngPlanLog(UnitTestDatabase& database){ + database.add("RngPlan log lines", test_log_lines); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h new file mode 100644 index 0000000000..9f62eb0b0a --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h @@ -0,0 +1,23 @@ +/* RNG Plan Log Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanLog_Tests_H +#define PokemonAutomation_PokemonFRLG_RngPlanLog_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +void add_tests_RngPlanLog(UnitTestDatabase& database); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.cpp new file mode 100644 index 0000000000..5e5bc27838 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.cpp @@ -0,0 +1,61 @@ +/* RNG Plan Test Targets + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include "PokemonFRLG_RngPlanTestTargets.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +namespace RngPlanTestTargets{ + + +std::string join(const std::vector& values){ + std::string text; + for (uint16_t value : values){ + text += (text.empty() ? "" : ",") + std::to_string(value); + } + return "{" + text + "}"; +} + + +// SIDs 100, 200 and 300 weigh 3, 2 and 1. +const std::vector CANDIDATES = {{100, 3}, {200, 2}, {300, 1}}; + +// A target reached "seconds" after the reset, shiny for the candidates at +// "shiny" (indices into CANDIDATES). +RngTarget make_target(uint32_t state, uint64_t seconds, std::vector shiny){ + RngTarget target{}; + target.state = state; + target.seed = {uint16_t(state), seconds * 1000, SeedButton::A, BlackoutButton::None}; + target.advances = 0; + target.pokemon = method1_pokemon(state); + for (size_t candidate : shiny){ + target.shiny_for.push_back({candidate, AdvShinyType::Star}); + } + return target; +} + +// By expected wait per shiny over all three (weight 6): 1 = 50 s / (5/6) = 60, +// 2 = 20 s / (1/6) = 120, 3 = 40 s / (3/6) = 80. Then over SID 300 alone, 2 is +// the only one shiny for it. 4 is the quickest and shiny for none. +const std::vector TARGETS = { + make_target(1, 50, {0, 1}), + make_target(2, 20, {2}), + make_target(3, 40, {0}), + make_target(4, 5, {}), +}; + + +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.h new file mode 100644 index 0000000000..017d9cd6a8 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.h @@ -0,0 +1,46 @@ +/* RNG Plan Test Targets + * + * From: https://github.com/PokemonAutomation/ + * + * Made-up SID candidates and targets the RNG plan tests share. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanTestTargets_H +#define PokemonAutomation_PokemonFRLG_RngPlanTestTargets_H + +#include +#include +#include +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace RngPlanTestTargets{ + + +std::string join(const std::vector& values); + +// SIDs 100, 200 and 300 weigh 3, 2 and 1. +extern const std::vector CANDIDATES; + +// A target reached "seconds" after the reset, shiny for the candidates at +// "shiny" (indices into CANDIDATES). +RngTarget make_target(uint32_t state, uint64_t seconds, std::vector shiny); + +// By expected wait per shiny over all three (weight 6): 1 = 50 s / (5/6) = 60, +// 2 = 20 s / (1/6) = 120, 3 = 40 s / (3/6) = 80. Then over SID 300 alone, 2 is +// the only one shiny for it. 4 is the quickest and shiny for none. +extern const std::vector TARGETS; + + +} + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.cpp new file mode 100644 index 0000000000..230a25a433 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.cpp @@ -0,0 +1,134 @@ +/* RNG Plan Tracker + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include "Common/Cpp/Exceptions.h" +#include "PokemonFRLG_RngPlanTracker.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; + + +RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResult& result){ + const AdvIVs& want = target.pokemon.ivs; + const AdvIVs& got = result.ivs; + const bool same_ivs = want.hp == got.hp && want.attack == got.attack && want.defense == got.defense + && want.spatk == got.spatk && want.spdef == got.spdef && want.speed == got.speed; + const bool hit = result.nature == target.pokemon.nature && same_ivs; + if (result.shiny){ + return hit ? RngAttemptOutcome::SHINY : RngAttemptOutcome::SHINY_OFF_TARGET; + } + return hit ? RngAttemptOutcome::HIT_NOT_SHINY : RngAttemptOutcome::MISSED; +} + + +RngPlanTracker::RngPlanTracker( + std::vector attempts, + const std::vector& candidates, + bool shiny_required, + size_t max_retries +) + : m_attempts(std::move(attempts)) + , m_shiny_required(shiny_required) + , m_max_retries(max_retries) + , m_step(m_attempts.empty() ? RngPlanStep::OUT_OF_ATTEMPTS : RngPlanStep::RUN_ATTEMPT) +{ + for (const SidCandidate& candidate : candidates){ + m_remaining.push_back(candidate.sid); + } +} + +std::optional RngPlanTracker::confirmed_sid() const{ + if (m_step == RngPlanStep::GOT_SHINY && m_remaining.size() == 1){ + return m_remaining.front(); + } + return std::nullopt; +} + +bool RngPlanTracker::is_ruled_out(uint16_t sid) const{ + return std::find(m_remaining.begin(), m_remaining.end(), sid) == m_remaining.end(); +} + +bool RngPlanTracker::move_to_next_attempt(){ + for (size_t next = m_attempt + 1; next < m_attempts.size(); next++){ + const std::vector& next_sids = m_attempts[next].sids; + if (!m_shiny_required || !std::all_of(next_sids.begin(), next_sids.end(), [&](uint16_t sid){ return is_ruled_out(sid); })){ + m_attempt = next; + m_retries = 0; + return true; + } + } + return false; +} + +RngPlanStep RngPlanTracker::record(RngAttemptOutcome outcome){ + if (m_step != RngPlanStep::RUN_ATTEMPT){ + return m_step; + } + const std::vector& sids = m_attempts[m_attempt].sids; + switch (outcome){ + case RngAttemptOutcome::SHINY: + // The SID is one the target is shiny for. None of them: it wasn't + // among the candidates. + std::erase_if(m_remaining, [&](uint16_t sid){ return std::find(sids.begin(), sids.end(), sid) == sids.end(); }); + m_step = RngPlanStep::GOT_SHINY; + return m_step; + case RngAttemptOutcome::SHINY_OFF_TARGET: + m_step = RngPlanStep::GOT_SHINY_OFF_TARGET; + return m_step; + case RngAttemptOutcome::HIT_NOT_SHINY: + if (!m_shiny_required){ + m_step = RngPlanStep::GOT_TARGET; + return m_step; + } + m_remaining.erase( + std::remove_if(m_remaining.begin(), m_remaining.end(), [&](uint16_t sid){ + return std::find(sids.begin(), sids.end(), sid) != sids.end(); + }), + m_remaining.end() + ); + if (!move_to_next_attempt()){ + m_step = RngPlanStep::OUT_OF_ATTEMPTS; + } + return m_step; + case RngAttemptOutcome::MISSED: + // A miss rules nothing out. Too many: try the next attempt instead. + if (++m_retries <= m_max_retries){ + return m_step; + } + [[fallthrough]]; + case RngAttemptOutcome::UNUSABLE: + if (!move_to_next_attempt()){ + m_step = RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT; + } + return m_step; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngAttemptOutcome."); +} + + +RngPlanNext rng_plan_next(RngPlanStep step, size_t index, size_t next_index){ + switch (step){ + case RngPlanStep::RUN_ATTEMPT: + return next_index == index ? RngPlanNext::RETRY : RngPlanNext::NEXT_ATTEMPT; + case RngPlanStep::GOT_SHINY: + return RngPlanNext::CONFIRM; + case RngPlanStep::GOT_SHINY_OFF_TARGET: + case RngPlanStep::GOT_TARGET: + case RngPlanStep::OUT_OF_ATTEMPTS: + case RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT: + return RngPlanNext::STOP; + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "Unknown RngPlanStep."); +} + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.h new file mode 100644 index 0000000000..3875f3bdbf --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.h @@ -0,0 +1,119 @@ +/* RNG Plan Tracker + * + * From: https://github.com/PokemonAutomation/ + * + * The step after each attempt of an RNG plan (PokemonFRLG_RngPlan.h), + * from what came out of it. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanTracker_H +#define PokemonAutomation_PokemonFRLG_RngPlanTracker_H + +#include +#include +#include +#include "Pokemon/Pokemon_AdvRng.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// What came out of one attempt. +enum class RngAttemptOutcome{ + // Shiny, with the target's nature and IVs. + SHINY, + // Shiny, but not the target, or not known to be. + SHINY_OFF_TARGET, + // The target's nature and IVs, not shiny. + HIT_NOT_SHINY, + // Another nature or other IVs: not the target. + MISSED, + // The attempt couldn't run (Starter RNG refused its timings). It rules + // nothing out; the next attempt runs instead. + UNUSABLE, +}; + +struct RngAttemptResult{ + bool shiny; + Pokemon::AdvNature nature; + Pokemon::AdvIVs ivs; +}; +RngAttemptOutcome classify_attempt(const RngTarget& target, const RngAttemptResult& result); + + +enum class RngPlanStep{ + // Run current_attempt(): the first, the next, or the same again after a miss. + RUN_ATTEMPT, + // Shiny, the target hit. remaining_sids() are the SIDs it can be. + GOT_SHINY, + // Shiny, not the target: the SIDs are as they were, none confirmed. + GOT_SHINY_OFF_TARGET, + // Shiny not required, and the target was hit. + GOT_TARGET, + // Every attempt was hit and none was shiny. + OUT_OF_ATTEMPTS, + // The last attempt still shiny for a SID left was missed more often + // than the retries allow, or couldn't run. (Missing an earlier one moves + // on to the next.) + MISSED_EVERY_ATTEMPT_LEFT, +}; + +// Follows the plan through the attempts' outcomes. +class RngPlanTracker{ +public: + RngPlanTracker( + std::vector attempts, + const std::vector& candidates, + bool shiny_required, + size_t max_retries + ); + + RngPlanStep step() const{ return m_step; } + size_t attempt_index() const{ return m_attempt; } + size_t attempt_count() const{ return m_attempts.size(); } + const RngAttempt& current_attempt() const{ return m_attempts[m_attempt]; } + // The SID candidates not ruled out, in the order given. + const std::vector& remaining_sids() const{ return m_remaining; } + // The SID, once a shiny leaves one candidate. + std::optional confirmed_sid() const; + + // The next step after the current attempt came out as "outcome". Once + // the plan has ended, the step stays as it is. + RngPlanStep record(RngAttemptOutcome outcome); + +private: + bool is_ruled_out(uint16_t sid) const; + // Moves to the next attempt still shiny for a SID left (with shiny + // required; else any next one). False if there is none. + bool move_to_next_attempt(); + + std::vector m_attempts; + bool m_shiny_required; + size_t m_max_retries; + std::vector m_remaining; + size_t m_attempt = 0; + size_t m_retries = 0; + RngPlanStep m_step; +}; + + +// What the loop does after an attempt's result. +enum class RngPlanNext{ + RETRY, + NEXT_ATTEMPT, + // Shiny and the target: go on with the story. + CONFIRM, + STOP, +}; +// The step after `index` came out as the tracker's `step` and its attempt +// is now `next_index`. +RngPlanNext rng_plan_next(RngPlanStep step, size_t index, size_t next_index); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.cpp new file mode 100644 index 0000000000..cd4ed2b5ce --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.cpp @@ -0,0 +1,262 @@ +/* RNG Plan Tracker Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The outcome of an attempt, and the step after each, on made-up targets. + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_RngPlanTestTargets.h" +#include "PokemonFRLG_RngPlanTracker.h" +#include "PokemonFRLG_RngPlanTracker_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; +using namespace RngPlanTestTargets; + + +namespace{ + +class Test_RngPlanTracker : public UnitTest{ +public: + Test_RngPlanTracker(std::string name, std::function body) + : UnitTest("PokemonFRLG::RngPlanTracker - " + 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; +}; + + +UnitTestResult test_classify(Logger&){ + const RngTarget hit = TARGETS[0]; + AdvIVs other_ivs = hit.pokemon.ivs; + other_ivs.speed = uint8_t((other_ivs.speed + 1) % 32); + const AdvNature other_nature = hit.pokemon.nature == AdvNature::Hardy ? AdvNature::Lonely : AdvNature::Hardy; + + struct Case{ + std::string name; + RngAttemptResult result; + RngAttemptOutcome expected; + }; + const Case cases[] = { + {.name = "shiny, the target", .result = {.shiny = true, .nature = hit.pokemon.nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::SHINY}, + {.name = "shiny, not the target", .result = {.shiny = true, .nature = other_nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::SHINY_OFF_TARGET}, + {.name = "same nature and IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::HIT_NOT_SHINY}, + {.name = "other IVs", .result = {.shiny = false, .nature = hit.pokemon.nature, .ivs = other_ivs}, .expected = RngAttemptOutcome::MISSED}, + {.name = "other nature", .result = {.shiny = false, .nature = other_nature, .ivs = hit.pokemon.ivs}, .expected = RngAttemptOutcome::MISSED}, + }; + for (const Case& item : cases){ + if (classify_attempt(hit, item.result) != item.expected){ + return item.name + ": classified wrong."; + } + } + return true; +} + + +const char* step_name(RngPlanStep step){ + switch (step){ + case RngPlanStep::RUN_ATTEMPT: return "RUN_ATTEMPT"; + case RngPlanStep::GOT_SHINY: return "GOT_SHINY"; + case RngPlanStep::GOT_SHINY_OFF_TARGET: return "GOT_SHINY_OFF_TARGET"; + case RngPlanStep::GOT_TARGET: return "GOT_TARGET"; + case RngPlanStep::OUT_OF_ATTEMPTS: return "OUT_OF_ATTEMPTS"; + case RngPlanStep::MISSED_EVERY_ATTEMPT_LEFT: return "MISSED_EVERY_ATTEMPT_LEFT"; + } + return "?"; +} + +struct TrackerCase{ + std::string name; + bool shiny_required; + std::vector outcomes; + // The step after each outcome, and the attempt run next. + std::vector steps; + std::vector attempts; + std::vector remaining; + std::optional confirmed; +}; +std::vector tracker_cases(){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + return { + { + .name = "shiny at once: one of two SIDs", + .shiny_required = true, .outcomes = {SHINY}, + .steps = {GOT_SHINY}, .attempts = {0}, + .remaining = {100, 200}, .confirmed = std::nullopt, + }, + { + .name = "hit, then shiny: SID confirmed", + .shiny_required = true, .outcomes = {HIT_NOT_SHINY, SHINY}, + .steps = {RUN_ATTEMPT, GOT_SHINY}, .attempts = {1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "a miss retries the same target", + .shiny_required = true, .outcomes = {MISSED, MISSED, HIT_NOT_SHINY, MISSED}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT}, .attempts = {0, 0, 1, 1}, + .remaining = {300}, .confirmed = std::nullopt, + }, + }; +} +// Misses: retried, then the next attempt, mixed with hits and shinies. +std::vector tracker_retry_cases(){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + return { + { + .name = "retries used up, then the next attempt", + .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 0, 1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "every attempt left missed too often", + .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, MISSED, MISSED, MISSED}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, MISSED_EVERY_ATTEMPT_LEFT}, + .attempts = {0, 0, 1, 1, 1, 1}, + .remaining = {100, 200, 300}, .confirmed = std::nullopt, + }, + { + .name = "shiny off the target confirms nothing", + .shiny_required = true, .outcomes = {HIT_NOT_SHINY, SHINY_OFF_TARGET, SHINY}, + .steps = {RUN_ATTEMPT, GOT_SHINY_OFF_TARGET, GOT_SHINY_OFF_TARGET}, .attempts = {1, 1, 1}, + .remaining = {300}, .confirmed = std::nullopt, + }, + }; +} +// Attempts that can't run. +std::vector tracker_unusable_cases(){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + return { + { + .name = "an attempt that can't run moves on", + .shiny_required = true, .outcomes = {MISSED, UNUSABLE, SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "the last attempt can't run", + .shiny_required = true, .outcomes = {UNUSABLE, UNUSABLE}, + .steps = {RUN_ATTEMPT, MISSED_EVERY_ATTEMPT_LEFT}, .attempts = {1, 1}, + .remaining = {100, 200, 300}, .confirmed = std::nullopt, + }, + }; +} +std::vector tracker_mixed_cases(){ + using enum RngAttemptOutcome; + using enum RngPlanStep; + return { + { + .name = "miss, hit, misses, then shiny", + .shiny_required = true, .outcomes = {MISSED, HIT_NOT_SHINY, MISSED, MISSED, SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, GOT_SHINY}, .attempts = {0, 1, 1, 1, 1}, + .remaining = {300}, .confirmed = uint16_t(300), + }, + { + .name = "misses move on, then a hit ends the plan", + .shiny_required = true, .outcomes = {MISSED, MISSED, MISSED, MISSED, HIT_NOT_SHINY}, + .steps = {RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, RUN_ATTEMPT, OUT_OF_ATTEMPTS}, .attempts = {0, 0, 1, 1, 1}, + .remaining = {100, 200}, .confirmed = std::nullopt, + }, + { + .name = "every attempt hit, none shiny", + .shiny_required = true, .outcomes = {HIT_NOT_SHINY, HIT_NOT_SHINY}, + .steps = {RUN_ATTEMPT, OUT_OF_ATTEMPTS}, .attempts = {1, 1}, + .remaining = {}, .confirmed = std::nullopt, + }, + { + .name = "shiny not required: a hit ends it", + .shiny_required = false, .outcomes = {HIT_NOT_SHINY}, + .steps = {GOT_TARGET}, .attempts = {0}, + .remaining = {100, 200, 300}, .confirmed = std::nullopt, + }, + }; +} + +UnitTestResult check_tracker_case(const TrackerCase& item){ + RngPlanTracker tracker(plan_rng_attempts(TARGETS, CANDIDATES, true, 5), CANDIDATES, item.shiny_required, 2); + if (tracker.step() != RngPlanStep::RUN_ATTEMPT){ + return item.name + ": expected to start with RUN_ATTEMPT, got " + step_name(tracker.step()) + "."; + } + for (size_t c = 0; c < item.outcomes.size(); c++){ + const RngPlanStep step = tracker.record(item.outcomes[c]); + if (step != item.steps[c] || tracker.attempt_index() != item.attempts[c]){ + return std::format( + "{}: after outcome {}, expected {} at attempt {}, got {} at attempt {}.", + item.name, c, step_name(item.steps[c]), item.attempts[c], step_name(step), tracker.attempt_index() + ); + } + } + if (tracker.remaining_sids() != item.remaining || tracker.confirmed_sid() != item.confirmed){ + return std::format( + "{}: expected SIDs {} (confirmed {}), got {} (confirmed {}).", + item.name, join(item.remaining), item.confirmed ? std::to_string(*item.confirmed) : "none", + join(tracker.remaining_sids()), tracker.confirmed_sid() ? std::to_string(*tracker.confirmed_sid()) : "none" + ); + } + return true; +} + +UnitTestResult test_tracker(Logger&){ + std::vector cases = tracker_cases(); + for (auto* more : {tracker_retry_cases, tracker_unusable_cases, tracker_mixed_cases}){ + for (TrackerCase& item : more()){ + cases.push_back(std::move(item)); + } + } + for (const TrackerCase& item : cases){ + const UnitTestResult result = check_tracker_case(item); + if (result.result != UnitTestResult::PASSED){ + return result; + } + } + RngPlanTracker empty({}, CANDIDATES, true, 2); + if (empty.step() != RngPlanStep::OUT_OF_ATTEMPTS){ + return std::string("An empty plan: expected OUT_OF_ATTEMPTS, got ") + step_name(empty.step()) + "."; + } + // A shiny hit for a SID that is no candidate: none left, none confirmed. + RngPlanTracker outside({RngAttempt{make_target(1, 50, {}), {999}}}, CANDIDATES, true, 2); + outside.record(RngAttemptOutcome::SHINY); + if (outside.step() != RngPlanStep::GOT_SHINY || !outside.remaining_sids().empty() || outside.confirmed_sid()){ + return std::format( + "Shiny for no candidate: expected GOT_SHINY with no SID left, got {} with SIDs {}.", + step_name(outside.step()), join(outside.remaining_sids()) + ); + } + return true; +} + +} + + +void add_tests_RngPlanTracker(UnitTestDatabase& database){ + database.add("attempt outcome", test_classify); + database.add("step after each outcome", test_tracker); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h new file mode 100644 index 0000000000..725491c896 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h @@ -0,0 +1,23 @@ +/* RNG Plan Tracker Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlanTracker_Tests_H +#define PokemonAutomation_PokemonFRLG_RngPlanTracker_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +void add_tests_RngPlanTracker(UnitTestDatabase& database); + + +} +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.cpp new file mode 100644 index 0000000000..508ba5fa34 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.cpp @@ -0,0 +1,287 @@ +/* RNG Plan Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The attempt order on made-up targets; SID Helper's candidates and a + * Starter RNG plan from the FireRed seed list. + * + */ + +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_RngPlanTestTargets.h" +#include "PokemonFRLG_RngPlan.h" +#include "PokemonFRLG_RngPlanLog.h" +#include "PokemonFRLG_RngPlan_Tests.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + +using namespace Pokemon; +using namespace RngPlanTestTargets; + + +namespace{ + +class Test_RngPlan : public UnitTest{ +public: + Test_RngPlan(std::string name, std::function body) + : UnitTest("PokemonFRLG::RngPlan - " + 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; +}; + + +UnitTestResult test_sid_helper_candidates(Logger&){ + const uint16_t tid = 43771; + const uint32_t target_advances = 3001; + const std::vector candidates = sid_helper_candidates(tid, target_advances, 5); + + // The SID is the top half of the state after the TID seed is advanced + // once, then "advances" more times. + auto sid_at = [&](uint32_t advances){ + uint32_t state = tid; + for (uint32_t c = 0; c <= advances; c++){ + state = lcrng_next(state); + } + return uint16_t(state >> 16); + }; + const std::vector advances = {2997, 2999, 3001, 3003, 3005}; + const std::vector weights = {1, 2, 3, 2, 1}; + if (candidates.size() != advances.size()){ + return std::format("Expected {} candidates, got {}.", advances.size(), candidates.size()); + } + for (size_t c = 0; c < candidates.size(); c++){ + if (candidates[c].sid != sid_at(advances[c]) || candidates[c].weight != weights[c]){ + return std::format( + "Candidate {}: expected SID {} weight {}, got SID {} weight {}.", + c, sid_at(advances[c]), weights[c], candidates[c].sid, candidates[c].weight + ); + } + } + + const std::vector even = sid_helper_candidates(tid, target_advances, 4); + const std::vector even_weights = {1, 2, 3, 2}; + for (size_t c = 0; c < even.size() && c < even_weights.size(); c++){ + if (even.size() != even_weights.size() || even[c].sid != candidates[c].sid || even[c].weight != even_weights[c]){ + return std::format("Four candidates: expected the first four of five, weighing 1, 2, 3, 2; candidate {} differs.", c); + } + } + return true; +} + + +struct PlanCase{ + std::string name; + std::vector targets; + bool shiny_required; + size_t max_attempts; + std::vector states; + std::vector> sids; +}; +std::vector plan_cases(){ + const std::vector no_300 = {TARGETS[0], TARGETS[2], TARGETS[3]}; + return { + { + .name = "two candidates in one target first, then the last", + .targets = TARGETS, .shiny_required = true, .max_attempts = 5, + .states = {1, 2}, .sids = {{100, 200}, {300}}, + }, + { + .name = "capped at one attempt", + .targets = TARGETS, .shiny_required = true, .max_attempts = 1, + .states = {1}, .sids = {{100, 200}}, + }, + { + .name = "no target shiny for SID 300", + .targets = no_300, .shiny_required = true, .max_attempts = 5, + .states = {1}, .sids = {{100, 200}}, + }, + { + .name = "shiny not required: the quickest", + .targets = TARGETS, .shiny_required = false, .max_attempts = 5, + .states = {4}, .sids = {{}}, + }, + { + .name = "no targets", + .targets = {}, .shiny_required = true, .max_attempts = 5, + .states = {}, .sids = {}, + }, + }; +} + +UnitTestResult test_plan_order(Logger&){ + for (const PlanCase& item : plan_cases()){ + const std::vector attempts = plan_rng_attempts(item.targets, CANDIDATES, item.shiny_required, item.max_attempts); + std::vector states; + std::vector> sids; + for (const RngAttempt& attempt : attempts){ + states.push_back(attempt.target.state); + sids.push_back(attempt.sids); + } + if (states != item.states || sids != item.sids){ + std::string got; + for (size_t c = 0; c < attempts.size(); c++){ + got += std::format(" target {} {}", states[c], join(sids[c])); + } + return item.name + ": got" + (got.empty() ? " no attempts" : got) + "."; + } + } + return true; +} + + +// A plan from the FireRed (English) list: every attempt starts from a seed +// Starter RNG finds, takes at least its smallest Advances, and is shiny for +// the SIDs it lists, which no earlier attempt listed. +// Console B, 2026-11-02, FireRed English, Mono, Help: Starter RNG refused +// both of these as title screen waits under 30 s. AutoStory's plan for TID +// 55740 started with 11A3 at 945 advances (29,628 ms after Starter RNG's +// offsets); StarterRng by hand with 405C at 990 advances listed 30,488 ms +// from the table, 29,643 ms after them. Neither seed may start an attempt. +UnitTestResult test_starter_plan_seed_delay(Logger&){ + struct Case{ + uint16_t seed; + uint64_t table_ms; + }; + const Case cases[] = { + {.seed = 0x11a3, .table_ms = 30473}, + {.seed = 0x405c, .table_ms = 30488}, + }; + const SeedsDatabase database(seeds_json_path(true, Language::English)); + const std::vector seeds = initial_seed_entries(database, SoundSetting::Mono, "Help", true); + for (const Case& item : cases){ + auto entry = std::find_if(seeds.begin(), seeds.end(), [&](const InitialSeedEntry& seed){ return seed.seed == item.seed; }); + if (entry == seeds.end() || entry->seed_time_ms != item.table_ms){ + return std::format("Expected seed {:04X} at {} ms in the FireRed English list (Mono, Help).", item.seed, item.table_ms); + } + if (starter_rng_can_wait_for(*entry)){ + return std::format("Seed {:04X} ({} ms): expected too early for Starter RNG's title screen.", item.seed, item.table_ms); + } + } + + const uint16_t tid = 55740; + const std::vector attempts = plan_starter_rng(StarterRngPlanInputs{ + .firered = true, + .language = Language::English, + .sound = SoundSetting::Mono, + .tid = tid, + .candidates = sid_helper_candidates(tid, 3001, 5), + .filters = {}, + .max_attempts = 5, + }); + if (attempts.empty()){ + return "Expected attempts for TID 55740, got none."; + } + for (size_t c = 0; c < attempts.size(); c++){ + const InitialSeedEntry& seed = attempts[c].target.seed; + if (seed.seed == 0x11a3 || seed.seed == 0x405c || starter_rng_seed_wait_ms(seed) < 31500){ + return std::format("Attempt {}: seed {:04X} at {} ms is too early for the title screen.", c + 1, seed.seed, seed.seed_time_ms); + } + if (attempts[c].target.advances < 1660){ + return std::format("Attempt {}: {} advances leave no room for the in-game calibration.", c + 1, attempts[c].target.advances); + } + } + return true; +} + +// Starter RNG's in-game wait: 7,500 ms at its 940-advance minimum; console +// B's B579 target at 967 advances was 7,725 ms, which one miss's +// calibration (-720) cut under the minimum. Targets start 720 advances on. +UnitTestResult test_starter_ingame_wait(Logger&){ + struct Case{ + uint64_t advances; + int64_t ms; + }; + const Case cases[] = { + {.advances = 940, .ms = 7500}, + {.advances = 967, .ms = 7725}, + {.advances = 1660, .ms = 13500}, + }; + for (const Case& item : cases){ + if (starter_rng_ingame_ms(item.advances) != item.ms){ + return std::format("{} advances: expected {} ms in game, got {}.", item.advances, item.ms, starter_rng_ingame_ms(item.advances)); + } + } + if (STARTER_RNG_PLAN_MIN_ADVANCES != 1660){ + return std::format("Expected the plan's minimum at 1660 advances, got {}.", STARTER_RNG_PLAN_MIN_ADVANCES); + } + return true; +} + +UnitTestResult test_starter_plan(Logger&){ + const uint16_t tid = 43771; + StarterRngPlanInputs inputs{ + .firered = true, + .language = Language::English, + .sound = SoundSetting::Mono, + .tid = tid, + .candidates = sid_helper_candidates(tid, 3001, 5), + .filters = {}, + .max_attempts = 5, + }; + const std::vector attempts = plan_starter_rng(inputs); + if (attempts.empty()){ + return "Expected attempts for TID 43771 with five SID candidates, got none."; + } + + const SeedsDatabase database(seeds_json_path(true, Language::English)); + std::vector listed; + for (size_t c = 0; c < attempts.size(); c++){ + const RngAttempt& attempt = attempts[c]; + if (!database.find_seed(attempt.target.seed.seed, SoundSetting::Mono, 5).found){ + return std::format("Attempt {}: Starter RNG can't find Target Seed {:04X}.", c + 1, attempt.target.seed.seed); + } + if (attempt.target.advances < STARTER_RNG_PLAN_MIN_ADVANCES){ + return std::format("Attempt {}: Advances {} below the plan's {}.", c + 1, attempt.target.advances, STARTER_RNG_PLAN_MIN_ADVANCES); + } + if (attempt.sids.empty()){ + return std::format("Attempt {}: shiny for no SID candidate.", c + 1); + } + for (uint16_t sid : attempt.sids){ + if (shiny_type(attempt.target.pokemon.pid, tid, sid) == AdvShinyType::Normal){ + return std::format("Attempt {}: PID {:08X} not shiny for SID {}.", c + 1, attempt.target.pokemon.pid, sid); + } + if (std::find(listed.begin(), listed.end(), sid) != listed.end()){ + return std::format("Attempt {}: SID {} already listed by an earlier attempt.", c + 1, sid); + } + listed.push_back(sid); + } + } + if (rng_attempt_lines(attempts).size() != attempts.size()){ + return "Expected one line per attempt."; + } + return true; +} + +} + + +void add_tests_RngPlan(UnitTestDatabase& database){ + database.add("SID Helper's candidates and weights", test_sid_helper_candidates); + database.add("attempt order", test_plan_order); + database.add("Starter RNG plan from the FireRed list", test_starter_plan); + database.add("Starter RNG plan: no seed too early for the title screen", test_starter_plan_seed_delay); + database.add("Starter RNG plan: room for the in-game calibration", test_starter_ingame_wait); +} + + + +} +} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.h new file mode 100644 index 0000000000..80d2408130 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.h @@ -0,0 +1,23 @@ +/* RNG Plan Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_RngPlan_Tests_H +#define PokemonAutomation_PokemonFRLG_RngPlan_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +void add_tests_RngPlan(UnitTestDatabase& database); + + +} +} +} +#endif diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index d249925455..3df5863f59 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1842,9 +1842,6 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryItems_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h - Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.cpp - Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan.h - Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngPlan_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryTable.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStorySimulation_Tests.cpp @@ -1895,6 +1892,24 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngLoopRoutines.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngNavigation.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan_Tests.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs_Tests.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog_Tests.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTestTargets.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker.h + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngStatsDatabase.cpp Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngStatsDatabase.h Source/VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.cpp From 0399aefb0b53cc1825a6d4cca4ebfa786b053ef3 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 2 Nov 2026 20:56:41 -0800 Subject: [PATCH 12/13] FRLG AutoStory: keep this console's RNG calibration in the RNG plan The RNG plan gets an "RNG Calibration (this console)" group with the seed and Continue Screen Frames offsets, in Starter RNG's units, for AutoStory's RNG stops to start from and save back to. Each stop keeps its own in-game advances offset, added with the stop. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG_AutoStoryRngOptions.cpp | 39 +++++++++++++++++-- .../PokemonFRLG_AutoStoryRngOptions.h | 24 ++++++++++++ 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp index 5d15a4f79c..676ec0d9f9 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.cpp @@ -4,6 +4,7 @@ * */ +#include #include "Common/Cpp/Exceptions.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanInputs.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlanLog.h" @@ -14,6 +15,38 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ +RngStopCalibrationOptions::RngStopCalibrationOptions() + : GroupOption("RNG Calibration (this console)", LockMode::LOCK_WHILE_RUNNING) + , SEED( + "Seed Calibration (ms):
As in Starter RNG's RNG Calibration. Each RNG stop starts from it, " + "and saves what it learns after every reset.", + LockMode::LOCK_WHILE_RUNNING, 0 + ) + , CSF( + "Continue Screen Frames Calibration:
As in Starter RNG's RNG Calibration, kept as the seed's is.", + LockMode::LOCK_WHILE_RUNNING, 0.0 + ) +{ + PA_ADD_OPTION(SEED); + PA_ADD_OPTION(CSF); +} + +RngCalibrations RngStopCalibrationOptions::calibration(const FloatingPointOption& ingame) const{ + return RngCalibrations{ + .seed_offset = double(int64_t(SEED)) / FRLG_FRAME_DURATION, + .csf_offset = CSF, + .ingame_offset = ingame, + }; +} + +void RngStopCalibrationOptions::save(const RngCalibrations& calibration, FloatingPointOption& ingame){ + // In ms, as RngCalibrationDisplay::set_calibrations() shows it. + SEED.set(int64_t(std::round(calibration.seed_offset * FRLG_FRAME_DURATION))); + CSF.set(calibration.csf_offset); + ingame.set(calibration.ingame_offset); +} + + StarterRngPlanOptions::StarterRngPlanOptions() : GroupOption("RNG Plan (Stop for Starter RNG)", LockMode::LOCK_WHILE_RUNNING) , SOUND( @@ -43,10 +76,7 @@ StarterRngPlanOptions::StarterRngPlanOptions() "Off: the quickest target that passes the natures and IVs.", LockMode::LOCK_WHILE_RUNNING, true ) - , NATURES( - false, "Natures:
Separated by commas. Empty: any nature.", - LockMode::LOCK_WHILE_RUNNING, "", "Modest, Timid" - ) + , NATURES(false, "Natures:
Separated by commas. Empty: any nature.", LockMode::LOCK_WHILE_RUNNING, "", "Modest, Timid") , MIN_IVS( false, "Minimum IVs:
HP/Atk/Def/SpA/SpD/Spe.", LockMode::LOCK_WHILE_RUNNING, "0/0/0/0/0/0", "0/0/0/0/0/0" @@ -62,6 +92,7 @@ StarterRngPlanOptions::StarterRngPlanOptions() PA_ADD_OPTION(NATURES); PA_ADD_OPTION(MIN_IVS); PA_ADD_OPTION(MAX_ATTEMPTS); + PA_ADD_OPTION(CALIBRATION); } std::string StarterRngPlanOptions::sound_name() const{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h index 1c46aa38a2..3e08f2b94f 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryRngOptions.h @@ -16,10 +16,12 @@ #include "Common/Cpp/Logging/AbstractLogger.h" #include "Common/Cpp/Options/BooleanCheckBoxOption.h" #include "Common/Cpp/Options/EnumDropdownOption.h" +#include "Common/Cpp/Options/FloatingPointOption.h" #include "Common/Cpp/Options/GroupOption.h" #include "Common/Cpp/Options/SimpleIntegerOption.h" #include "Common/Cpp/Options/StringOption.h" #include "CommonFramework/Language.h" +#include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngCalibration.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngPlan.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_RngTargetSearch.h" #include "VideoGames/PokemonFRLG/Programs/RngManipulation/PokemonFRLG_SeedsDatabase.h" @@ -29,6 +31,27 @@ namespace NintendoSwitch{ namespace PokemonFRLG{ +// This console's RNG calibration, as Starter RNG's RNG Calibration shows +// it, for AutoStory's RNG stops to start from: the seed and Continue +// Screen Frames offsets, the same for every stop, and an in-game advances +// offset for each stop. +class RngStopCalibrationOptions : public GroupOption{ +public: + RngStopCalibrationOptions(); + + // The calibration in frames for the stop whose in-game advances offset + // `ingame` keeps. + RngCalibrations calibration(const FloatingPointOption& ingame) const; + // Keeps `calibration` as calibration() returns it. Called after each + // reset, so a stopped run keeps what it learned. + void save(const RngCalibrations& calibration, FloatingPointOption& ingame); + +public: + SimpleIntegerOption SEED; + FloatingPointOption CSF; +}; + + class StarterRngPlanOptions : public GroupOption{ public: StarterRngPlanOptions(); @@ -56,6 +79,7 @@ class StarterRngPlanOptions : public GroupOption{ StringOption NATURES; StringOption MIN_IVS; SimpleIntegerOption MAX_ATTEMPTS; + RngStopCalibrationOptions CALIBRATION; private: RngTargetFilters filters(Logger& logger) const; From e767ab112537944485e876f6c6b49607b5ee9dbf Mon Sep 17 00:00:00 2001 From: dayfine Date: Tue, 3 Nov 2026 09:22:23 -0800 Subject: [PATCH 13/13] FRLG AutoStory: program() within code health's 50 lines Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01AKYzhrMnPK61XHWEch4a6E --- .../PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp index 8a6b2d7ad5..93171350f8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStory.cpp @@ -404,7 +404,6 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex StoryEnv story{env.console, context, checkpoints[first].supplies, checkpoints[first].supplies, false, RUN.checks()}; story.player_name = player_name; prepare_start(env, context, checkpoints, first, story); - StoryExecutor executor(story); executor.set_checks(RUN.checks()); executor.set_language(RUN.language()); @@ -416,7 +415,6 @@ void AutoStory::program(SingleSwitchProgramEnvironment& env, ProControllerContex }); } Pokemon::SegmentHooks hooks = story_hooks(env, story); - Pokemon::RunState state; const Pokemon::SegmentReport report = run_segment( state, segment, Pokemon::SegmentRange{plan.rows.front().id, plan.rows.back().id}, RUN.policy(), hooks