diff --git a/.circleci/config.yml b/.circleci/config.yml index 34385a079..598ce00b7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,7 +24,7 @@ jobs: steps: - checkout - run: apt-get update -y - - run: apt-get install -y cmake curl g++ git lcov libgtest-dev libfmt-dev libsdl2-dev libsdl2-ttf-dev libsodium-dev + - run: apt-get install -y cmake curl g++ git lcov libgtest-dev libgmock-dev libfmt-dev libsdl2-dev libsdl2-ttf-dev libsodium-dev - run: cmake -S. -Bbuild -DRUN_TESTS=ON -DENABLE_CODECOVERAGE=ON - run: cmake --build build -j 2 - run: cmake --build build -j 2 --target test diff --git a/CMakeLists.txt b/CMakeLists.txt index 9912c47da..6ee761571 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -532,6 +532,7 @@ if(RUN_TESTS) test/missiles_test.cpp test/pack_test.cpp test/player_test.cpp + test/quests_test.cpp test/random_test.cpp test/scrollrt_test.cpp test/stores_test.cpp diff --git a/Source/quests.cpp b/Source/quests.cpp index c30b59977..21190c856 100644 --- a/Source/quests.cpp +++ b/Source/quests.cpp @@ -161,16 +161,8 @@ void InitQuests() } if (!gbIsMultiplayer && sgOptions.Gameplay.bRandomizeQuests) { - SetRndSeed(glSeedTbl[15]); - if (GenerateRnd(2) != 0) - Quests[Q_PWATER]._qactive = QUEST_NOTAVAIL; - else - Quests[Q_SKELKING]._qactive = QUEST_NOTAVAIL; - - Quests[QuestGroup1[GenerateRnd(sizeof(QuestGroup1) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; - Quests[QuestGroup2[GenerateRnd(sizeof(QuestGroup2) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; - Quests[QuestGroup3[GenerateRnd(sizeof(QuestGroup3) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; - Quests[QuestGroup4[GenerateRnd(sizeof(QuestGroup4) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; + // Quests are set from the seed used to generate level 16. + InitialiseQuestPools(glSeedTbl[15], Quests); } #ifdef _DEBUG if (questdebug != -1) @@ -192,6 +184,20 @@ void InitQuests() Quests[Q_BETRAYER]._qvar1 = 2; } +void InitialiseQuestPools(uint32_t seed, QuestStruct quests[]) +{ + SetRndSeed(seed); + if (GenerateRnd(2) != 0) + quests[Q_PWATER]._qactive = QUEST_NOTAVAIL; + else + quests[Q_SKELKING]._qactive = QUEST_NOTAVAIL; + + quests[QuestGroup1[GenerateRnd(sizeof(QuestGroup1) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; + quests[QuestGroup2[GenerateRnd(sizeof(QuestGroup2) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; + quests[QuestGroup3[GenerateRnd(sizeof(QuestGroup3) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; + quests[QuestGroup4[GenerateRnd(sizeof(QuestGroup4) / sizeof(int))]]._qactive = QUEST_NOTAVAIL; +} + void CheckQuests() { if (gbIsSpawn) diff --git a/Source/quests.h b/Source/quests.h index 73d5a4f0f..dfde7b4c2 100644 --- a/Source/quests.h +++ b/Source/quests.h @@ -78,6 +78,13 @@ extern dungeon_type ReturnLevelType; extern int ReturnLevel; void InitQuests(); + +/** + * @brief Deactivates quests from each quest pool at random to provide variety for single player games + * @param seed The seed used to control which quests are deactivated + * @param quests The available quest list, this function will make some of them inactive by the time it returns +*/ +void InitialiseQuestPools(uint32_t seed, QuestStruct quests[]); void CheckQuests(); bool ForceQuests(); bool QuestStatus(int i); diff --git a/test/quests_test.cpp b/test/quests_test.cpp new file mode 100644 index 000000000..1b54f0a8c --- /dev/null +++ b/test/quests_test.cpp @@ -0,0 +1,73 @@ +#include +#include + +#include "quests.h" + +#include "objdat.h" // For quest IDs + +namespace devilution { +void ResetQuests() +{ + for (auto &quest : Quests) + quest._qactive = QUEST_INIT; +} + +std::vector GetActiveFlagsForSlice(std::initializer_list ids) +{ + std::vector temp; + + for (auto id : ids) + temp.push_back(Quests[id]._qactive); + + return temp; +} + +TEST(QuestTest, SinglePlayerBadPools) +{ + ResetQuests(); + + // (INT_MIN >> 16) % 2 = 0, so the times when the RNG calls GenerateRnd(2) don't end up with a negative value. + InitialiseQuestPools(1457187811, Quests); + EXPECT_EQ(Quests[Q_SKELKING]._qactive, QUEST_NOTAVAIL) << "Skeleton King quest is deactivated with 'bad' seed"; + ResetQuests(); + + InitialiseQuestPools(988045466, Quests); + EXPECT_THAT(GetActiveFlagsForSlice({ Q_BUTCHER, Q_LTBANNER, Q_GARBUD }), ::testing::Each(QUEST_INIT)) << "All quests in pool 2 remain active with 'bad' seed"; + ResetQuests(); + + InitialiseQuestPools(4203210069U, Quests); + EXPECT_THAT(GetActiveFlagsForSlice({ Q_BLIND, Q_ROCK, Q_BLOOD }), ::testing::Each(QUEST_INIT)) << "All quests in pool 3 remain active with 'bad' seed"; + ResetQuests(); + + InitialiseQuestPools(2557708932U, Quests); + EXPECT_THAT(GetActiveFlagsForSlice({ Q_MUSHROOM, Q_ZHAR, Q_ANVIL }), ::testing::Each(QUEST_INIT)) << "All quests in pool 4 remain active with 'bad' seed"; + ResetQuests(); + + InitialiseQuestPools(1272442071, Quests); + EXPECT_EQ(Quests[Q_VEIL]._qactive, QUEST_NOTAVAIL) << "Lachdan quest is deactivated with 'bad' seed"; +} + +TEST(QuestTest, SinglePlayerGoodPools) +{ + ResetQuests(); + + InitialiseQuestPools(509604, Quests); + EXPECT_EQ(Quests[Q_SKELKING]._qactive, QUEST_INIT) << "Expected Skeleton King quest to be available with the given seed"; + EXPECT_EQ(Quests[Q_PWATER]._qactive, QUEST_NOTAVAIL) << "Expected Poison Water quest to be deactivated with the given seed"; + + EXPECT_EQ(Quests[Q_BUTCHER]._qactive, QUEST_INIT) << "Expected Butcher quest to be available with the given seed"; + EXPECT_EQ(Quests[Q_LTBANNER]._qactive, QUEST_INIT) << "Expected Ogden's Sign quest to be available with the given seed"; + EXPECT_EQ(Quests[Q_GARBUD]._qactive, QUEST_NOTAVAIL) << "Expected Gharbad the Weak quest to be deactivated with the given seed"; + + EXPECT_EQ(Quests[Q_BLIND]._qactive, QUEST_INIT) << "Expected Halls of the Blind quest to be available with the given seed"; + EXPECT_EQ(Quests[Q_ROCK]._qactive, QUEST_NOTAVAIL) << "Expected Magic Rock quest to be deactivated with the given seed"; + EXPECT_EQ(Quests[Q_BLOOD]._qactive, QUEST_INIT) << "Expected Valor quest to be available with the given seed"; + + EXPECT_EQ(Quests[Q_MUSHROOM]._qactive, QUEST_INIT) << "Expected Black Mushroom quest to be available with the given seed"; + EXPECT_EQ(Quests[Q_ZHAR]._qactive, QUEST_NOTAVAIL) << "Expected Zhar the Mad quest to be deactivated with the given seed"; + EXPECT_EQ(Quests[Q_ANVIL]._qactive, QUEST_INIT) << "Expected Anvil of Fury quest to be available with the given seed"; + + EXPECT_EQ(Quests[Q_VEIL]._qactive, QUEST_NOTAVAIL) << "Expected Lachdanan quest to be deactivated with the given seed"; + EXPECT_EQ(Quests[Q_WARLORD]._qactive, QUEST_INIT) << "Expected Warlord of Blood quest to be available with the given seed"; +} +} // namespace devilution