diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp index 43487142ae..3f4456f144 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/PokemonFRLG_Tests.cpp @@ -28,6 +28,7 @@ #include "Inference/PokemonFRLG_TrainerIdReader.h" #include "Inference/PokemonFRLG_WildEncounterReader.h" #include "PokemonFRLG_Navigation.h" +#include "Programs/RngManipulation/PokemonFRLG_RngPlanTracker_Tests.h" #include "Programs/RngManipulation/PokemonFRLG_RngTargetSearch_Tests.h" #include "Tests/TestUtils.h" @@ -290,6 +291,7 @@ void add_tests(UnitTestDatabase& database){ add_tests_BagDetector(database); add_tests_ExitWildBattle(database); add_tests_RngTargetSearch(database); + add_tests_RngPlanTracker(database); } 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_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/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 1237dddaf9..724b5ceab4 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1809,6 +1809,14 @@ 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_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