diff --git a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp index 5b3e421dfd..4511bbfa3b 100644 --- a/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp +++ b/SerialPrograms/Source/ComputerPrograms/ComputerPrograms_UnitTestRunner.cpp @@ -13,6 +13,7 @@ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "Common/Qt/QtThreadPool_Tests.h" #include "Kernels/Kernels_Tests.h" +#include "Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h" #include "CommonFramework/AudioPipeline/AudioInfo_Tests.h" #include "CommonFramework/AudioPipeline/Tools/ChannelMixer.h" #include "CommonFramework/AudioPipeline/SinkPipeline_Tests.h" @@ -64,6 +65,7 @@ UnitTestDatabase make_UNIT_TESTS_ALL(){ add_tests_LabControl(ret); OCR::add_tests(ret); Kernels::add_tests(ret); + Pokemon::add_tests_AutoStorySelect(ret); NintendoSwitch::add_tests_CheckOnlineDetector(ret); NintendoSwitch::add_tests_FailedToConnectDetector(ret); NintendoSwitch::add_tests_UpdatePopupDetector(ret); diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h index 80b4f94338..84276b0b86 100644 --- a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h @@ -32,6 +32,18 @@ StringSelectDatabase make_story_select_database( } +// The index in the whole story list of entry `index_in_section` of the +// tutorial or the main story, whose first entry is `first_main_story_index`. +// NOTE: a section ends where the user must step in (e.g. level up the +// Pokemon between the tutorial and the main story). SV has two sections. +// Later content (postgame, DLC) appends to the last section, run only after +// the main story; if it needs its own break (e.g. grinding before a DLC), +// it becomes a third section and `main_story` here becomes a section index. +inline size_t story_index(bool main_story, size_t index_in_section, size_t first_main_story_index){ + return main_story ? first_main_story_index + index_in_section : index_in_section; +} + + } } #endif diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.cpp b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.cpp new file mode 100644 index 0000000000..efdd49bd8f --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.cpp @@ -0,0 +1,173 @@ +/* AutoStory Select Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Pokemon_AutoStorySelect.h" +#include "Pokemon_AutoStorySelect_Tests.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + +namespace{ + +// An entry of a story section, its index in the whole story list, and the +// index of the first main story entry. +struct AutoStoryIndexScenario{ + std::string name; + bool main_story; + size_t index_in_section; + size_t first_main_story_index; + size_t expected; +}; + +class Test_AutoStoryIndex : public UnitTest{ +public: + Test_AutoStoryIndex(AutoStoryIndexScenario scenario) + : UnitTest("Pokemon::AutoStoryIndex - " + scenario.name) + , m_scenario(std::move(scenario)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + const size_t got = story_index( + m_scenario.main_story, m_scenario.index_in_section, m_scenario.first_main_story_index + ); + if (got != m_scenario.expected){ + return "Expected index " + std::to_string(m_scenario.expected) + ", got " + std::to_string(got); + } + return true; + } + +private: + const AutoStoryIndexScenario m_scenario; +}; + +// A stand-in for a story segment or checkpoint: only its name is read. +struct NamedEntry{ + std::string label; + std::string name() const{ return label; } +}; + +// A dropdown built from [begin, end) of a story list of two tutorial entries, +// then two of the main story (which starts at index 2), as SV's lists are +// laid out, and its (slug, label) entries: slugs are indices in the whole +// list, not in the dropdown. +struct AutoStorySelectScenario{ + std::string name; + size_t begin; + size_t end; + std::vector> expected; +}; + +class Test_AutoStorySelectDatabase : public UnitTest{ +public: + Test_AutoStorySelectDatabase(AutoStorySelectScenario scenario) + : UnitTest("Pokemon::AutoStorySelectDatabase - " + scenario.name) + , m_scenario(std::move(scenario)) + {} + + virtual UnitTestResult run(Logger&, CancellableScope&) const override{ + // unique_ptr cannot be copied out of a braced list: build it in place. + const std::vector> list = []{ + std::vector> list; + for (const char* label : {"tutorial 0", "tutorial 1", "main story 0", "main story 1"}){ + list.emplace_back(std::make_unique(NamedEntry{label})); + } + return list; + }(); + const StringSelectDatabase database = make_story_select_database(list, m_scenario.begin, m_scenario.end); + std::vector> got; + for (const StringSelectEntry& entry : database.case_list()){ + got.emplace_back(entry.slug, entry.display_name); + } + if (got != m_scenario.expected){ + return "Expected " + describe(m_scenario.expected) + ", got " + describe(got); + } + return true; + } + +private: + static std::string describe(const std::vector>& entries){ + std::string text; + for (const auto& [slug, label] : entries){ + text += (text.empty() ? "" : ", ") + slug + " \"" + label + "\""; + } + return "[" + text + "]"; + } + + const AutoStorySelectScenario m_scenario; +}; + +void add_index_tests(UnitTestDatabase& database){ + const AutoStoryIndexScenario scenarios[] = { + { + .name = "a tutorial entry is its own index", + .main_story = false, + .index_in_section = 3, + .first_main_story_index = 10, + .expected = 3, + }, + { + // SV: segment 10 is the first of the main story. + .name = "the first main story entry follows the tutorial", + .main_story = true, + .index_in_section = 0, + .first_main_story_index = 10, + .expected = 10, + }, + { + // SV: checkpoint 21 is the first of the main story. + .name = "a later main story checkpoint", + .main_story = true, + .index_in_section = 5, + .first_main_story_index = 21, + .expected = 26, + }, + }; + for (const AutoStoryIndexScenario& scenario : scenarios){ + database.add(scenario); + } +} + +void add_select_database_tests(UnitTestDatabase& database){ + const AutoStorySelectScenario select_scenarios[] = { + { + .name = "the tutorial dropdown", + .begin = 0, + .end = 2, + .expected = {{"0", "tutorial 0"}, {"1", "tutorial 1"}}, + }, + { + .name = "the main story dropdown keeps whole-list slugs", + .begin = 2, + .end = 4, + .expected = {{"2", "main story 0"}, {"3", "main story 1"}}, + }, + { + .name = "an end past the list is clamped to it", + .begin = 2, + .end = 10, + .expected = {{"2", "main story 0"}, {"3", "main story 1"}}, + }, + }; + for (const AutoStorySelectScenario& scenario : select_scenarios){ + database.add(scenario); + } +} + +} + +void add_tests_AutoStorySelect(UnitTestDatabase& database){ + add_index_tests(database); + add_select_database_tests(database); +} + + + +} +} diff --git a/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h new file mode 100644 index 0000000000..5132ce6335 --- /dev/null +++ b/SerialPrograms/Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h @@ -0,0 +1,22 @@ +/* AutoStory Select Tests + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#ifndef PokemonAutomation_Pokemon_AutoStorySelect_Tests_H +#define PokemonAutomation_Pokemon_AutoStorySelect_Tests_H + +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" + +namespace PokemonAutomation{ +namespace Pokemon{ + + +void add_tests_AutoStorySelect(UnitTestDatabase& database); + + + +} +} +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp index 95ea9b1525..80f1d0b6f8 100644 --- a/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonSV/PokemonSV_Tests.cpp @@ -5,7 +5,6 @@ */ #include "Common/Cpp/TestRunners/UnitTestDatabase.h" -#include "Pokemon/AutoStory/Pokemon_AutoStorySelect.h" #include "Inference/PokemonSV_WhiteButtonDetector.h" #include "Inference/Boxes/PokemonSV_BoxDetection.h" #include "Inference/Boxes/PokemonSV_BoxEggDetector.h" @@ -149,46 +148,7 @@ class Test_BlueberryPoints : public ImageUnitTest{ } -namespace{ - -// A stand-in for a story segment or checkpoint: only its name is read. -struct NamedEntry{ - std::string label; - std::string name() const{ return label; } -}; - -class Test_AutoStorySelectDatabase : public UnitTest{ -public: - Test_AutoStorySelectDatabase() - : UnitTest("Pokemon::AutoStorySelectDatabase - a section keeps whole-list slugs") - {} - - virtual UnitTestResult run(Logger&, CancellableScope&) const override{ - std::vector> list; - for (const char* label : {"a", "b", "c", "d"}){ - list.emplace_back(std::make_unique(NamedEntry{label})); - } - const StringSelectDatabase database = Pokemon::make_story_select_database(list, 2, 10); - const std::vector& entries = database.case_list(); - if (entries.size() != 2){ - return "Expected 2 entries (the end is clamped to the list), got " + std::to_string(entries.size()); - } - if (entries[0].slug != "2" || entries[0].display_name != "c" || entries[1].slug != "3" || entries[1].display_name != "d"){ - return "Expected slugs 2, 3 labeled c, d; got " + entries[0].slug + " " + entries[0].display_name - + ", " + entries[1].slug + " " + entries[1].display_name; - } - return true; - } -}; - -void add_tests_AutoStory(UnitTestDatabase& database){ - database.add(); -} - -} - void add_tests(UnitTestDatabase& database){ - add_tests_AutoStory(database); database.add(); // Full-frame references, in the CommandLineTests repo: they exercise the production diff --git a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStory.cpp b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStory.cpp index 17b378372b..130271139b 100644 --- a/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStory.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonSV/Programs/AutoStory/PokemonSV_AutoStory.cpp @@ -1028,91 +1028,57 @@ void AutoStory::test_checkpoints( } std::string AutoStory::start_segment_description(){ - size_t segment_index = 0; - if (STORY_SECTION == StorySection::TUTORIAL){ - segment_index = STARTPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - segment_index = STARTPOINT_MAINSTORY.index() + (INDEX_OF_LAST_TUTORIAL_SEGMENT + 1); - } - return " " + ALL_AUTO_STORY_SEGMENT_LIST()[segment_index]->start_text(); + return " " + ALL_AUTO_STORY_SEGMENT_LIST()[get_start_segment_index()]->start_text(); } std::string AutoStory::end_segment_description(){ - size_t segment_index = 0; - if (STORY_SECTION == StorySection::TUTORIAL){ - segment_index = ENDPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - segment_index = ENDPOINT_MAINSTORY.index() + (INDEX_OF_LAST_TUTORIAL_SEGMENT + 1); - } - return " " + ALL_AUTO_STORY_SEGMENT_LIST()[segment_index]->end_text(); + return " " + ALL_AUTO_STORY_SEGMENT_LIST()[get_end_segment_index()]->end_text(); } size_t AutoStory::get_start_segment_index(){ - size_t start = 0; - - if (STORY_SECTION == StorySection::TUTORIAL){ - start = STARTPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - start = (INDEX_OF_LAST_TUTORIAL_SEGMENT + 1) + STARTPOINT_MAINSTORY.index(); - } - - return start; + const bool main_story = STORY_SECTION == StorySection::MAIN_STORY; + return story_index( + main_story, + main_story ? STARTPOINT_MAINSTORY.index() : STARTPOINT_TUTORIAL.index(), + INDEX_OF_LAST_TUTORIAL_SEGMENT + 1 + ); } size_t AutoStory::get_end_segment_index(){ - size_t end = 0; - - if (STORY_SECTION == StorySection::TUTORIAL){ - end = ENDPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - end = (INDEX_OF_LAST_TUTORIAL_SEGMENT + 1) + ENDPOINT_MAINSTORY.index(); - } - - return end; + const bool main_story = STORY_SECTION == StorySection::MAIN_STORY; + return story_index( + main_story, + main_story ? ENDPOINT_MAINSTORY.index() : ENDPOINT_TUTORIAL.index(), + INDEX_OF_LAST_TUTORIAL_SEGMENT + 1 + ); } std::string AutoStory::start_checkpoint_description(){ - size_t checkpoint_index = 0; - if (STORY_SECTION == StorySection::TUTORIAL){ - checkpoint_index = START_CHECKPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - checkpoint_index = START_CHECKPOINT_MAINSTORY.index() + (INDEX_OF_LAST_TUTORIAL_CHECKPOINT + 1); - } + const size_t checkpoint_index = get_start_checkpoint_index(); return " Start: " + ALL_AUTO_STORY_CHECKPOINT_LIST()[checkpoint_index]->start_text(); } std::string AutoStory::end_checkpoint_description(){ - size_t checkpoint_index = 0; - if (STORY_SECTION == StorySection::TUTORIAL){ - checkpoint_index = END_CHECKPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - checkpoint_index = END_CHECKPOINT_MAINSTORY.index() + (INDEX_OF_LAST_TUTORIAL_CHECKPOINT + 1); - } + const size_t checkpoint_index = get_end_checkpoint_index(); return " End: " + ALL_AUTO_STORY_CHECKPOINT_LIST()[checkpoint_index]->end_text(); } size_t AutoStory::get_start_checkpoint_index(){ - size_t start = 0; - - if (STORY_SECTION == StorySection::TUTORIAL){ - start = START_CHECKPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - start = (INDEX_OF_LAST_TUTORIAL_CHECKPOINT + 1) + START_CHECKPOINT_MAINSTORY.index(); - } - - return start; + const bool main_story = STORY_SECTION == StorySection::MAIN_STORY; + return story_index( + main_story, + main_story ? START_CHECKPOINT_MAINSTORY.index() : START_CHECKPOINT_TUTORIAL.index(), + INDEX_OF_LAST_TUTORIAL_CHECKPOINT + 1 + ); } size_t AutoStory::get_end_checkpoint_index(){ - size_t end = 0; - - if (STORY_SECTION == StorySection::TUTORIAL){ - end = END_CHECKPOINT_TUTORIAL.index(); - }else if (STORY_SECTION == StorySection::MAIN_STORY){ - end = (INDEX_OF_LAST_TUTORIAL_CHECKPOINT + 1) + END_CHECKPOINT_MAINSTORY.index(); - } - - return end; + const bool main_story = STORY_SECTION == StorySection::MAIN_STORY; + return story_index( + main_story, + main_story ? END_CHECKPOINT_MAINSTORY.index() : END_CHECKPOINT_TUTORIAL.index(), + INDEX_OF_LAST_TUTORIAL_CHECKPOINT + 1 + ); } diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index d5cb3516c6..6185819de9 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1468,6 +1468,8 @@ file(GLOB LIBRARY_SOURCES Source/Pokemon/Options/Pokemon_StatsHuntFilter.cpp Source/Pokemon/Options/Pokemon_StatsHuntFilter.h Source/Pokemon/AutoStory/Pokemon_AutoStorySelect.h + Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.cpp + Source/Pokemon/AutoStory/Pokemon_AutoStorySelect_Tests.h Source/Pokemon/Pokemon_AdvRng.cpp Source/Pokemon/Pokemon_AdvRng.h Source/Pokemon/Pokemon_BdspRng.cpp